using System;
using System.Linq;
using System.Text;
namespace BizHawk.Common.StringExtensions
{
/// TODO how many of these Is*/Only* methods' callers can use int.TryParse or similar instead? --yoshi
public static class StringExtensions
{
#pragma warning disable CS8602 // no idea --yoshi
/// the substring of before the first occurrence of , or if not found
public static string? GetPrecedingString(this string str, string value)
{
var index = str.IndexOf(value);
return index < 0 ? null : str.Substring(0, index);
}
/// iff appears in (case-insensitive)
public static bool In(this string str, params string[] options) => options.Any(opt => string.Equals(opt, str, StringComparison.InvariantCultureIgnoreCase));
/// how many times appears in , or 0 if is null
public static int HowMany(this string? str, char c) => string.IsNullOrEmpty(str) ? 0 : str.Count(t => t == c);
/// how many times appears in , or 0 if is null
///
/// occurrences may overlap, for example "AAA".HowMany("AA") returns 2
/// TODO except it doesn't, but "AAAB".HowMany("AA") does. I left this bug in so as to not break anything. --yoshi
///
public static int HowMany(this string? str, string sub)
{
if (string.IsNullOrEmpty(str)) return 0;
var count = 0;
var substrLength = sub.Length;
for (int i = 0, l = str.Length - substrLength; i < l; i++)
{
if (string.Equals(str.Substring(i, substrLength), sub, StringComparison.InvariantCulture)) count++;
}
return count;
}
/// iff is not and all chars of are digits
public static bool IsUnsigned(this string? str) => !string.IsNullOrWhiteSpace(str) && str.All(IsUnsigned);
/// iff is a digit
public static bool IsUnsigned(this char c) => char.IsDigit(c);
///
/// iff is not ,
/// the first char of is '-' or a digit, and
/// all subsequent chars of are digits
///
public static bool IsSigned(this string? str) => !string.IsNullOrWhiteSpace(str) && str[0].IsSigned() && str.Substring(1).All(IsUnsigned);
/// iff is '-' or a digit
public static bool IsSigned(this char c) => IsUnsigned(c) || c == '-';
/// iff is not and all chars of are hex digits ([0-9A-Fa-f])
/// should exclude the prefix 0x
public static bool IsHex(this string? str) => !string.IsNullOrWhiteSpace(str) && str.All(IsHex);
/// iff is a hex digit ([0-9A-Fa-f])
public static bool IsHex(this char c) => IsUnsigned(c) || 'A' <= char.ToUpperInvariant(c) && char.ToUpperInvariant(c) <= 'F';
/// iff is not and all chars of are either '0' or '1'
/// should exclude the prefix 0b
public static bool IsBinary(this string? str) => !string.IsNullOrWhiteSpace(str) && str.All(IsBinary);
/// iff is either '0' or '1'
public static bool IsBinary(this char c) => c == '0' || c == '1';
///
/// iff is not ,
/// all chars of are '.' or a digit, and
/// contains at most 1 decimal separator '.'
///
///
/// should exclude the suffix M.
/// This method returning for some does not imply that float.TryParse will also return .
/// Also this has nothing to do with fixed- vs. floating-point numbers, a better name would be IsUnsignedDecimal.
///
public static bool IsFixedPoint(this string? str) => !string.IsNullOrWhiteSpace(str) && str.HowMany('.') <= 1 && str.All(IsFixedPoint);
/// iff is '.' or a digit
/// Also this has nothing to do with fixed- vs. floating-point numbers, a better name would be IsUnsignedDecimal.
public static bool IsFixedPoint(this char c) => IsUnsigned(c) || c == '.';
///
/// iff is not ,
/// the first char of is '-', '.', or a digit,
/// all subsequent chars of are '.' or a digit, and
/// contains at most 1 decimal separator '.'
///
///
/// should exclude the suffix f.
/// This method returning for some does not imply that float.TryParse will also return .
/// Also this has nothing to do with fixed- vs. floating-point numbers, a better name would be IsSignedDecimal.
///
public static bool IsFloat(this string? str) => !string.IsNullOrWhiteSpace(str) && str.HowMany('.') <= 1 && str[0].IsFloat() && str.Substring(1).All(IsFixedPoint);
/// iff is '-', '.', or a digit
/// Also this has nothing to do with fixed- vs. floating-point numbers, a better name would be IsSignedDecimal.
public static bool IsFloat(this char c) => c.IsFixedPoint() || c == '-';
///
/// A copy of with characters removed so that the whole thing passes IsBinary.
/// That is, all chars of the copy will be either '0' or '1'.
///
public static string OnlyBinary(this string? raw) => string.IsNullOrWhiteSpace(raw) ? string.Empty : string.Concat(raw.Where(IsBinary));
///
/// A copy of with characters removed so that the whole thing passes IsUnsigned.
/// That is, all chars of the copy will be digits.
///
public static string OnlyUnsigned(this string? raw) => string.IsNullOrWhiteSpace(raw) ? string.Empty : string.Concat(raw.Where(IsUnsigned));
///
/// A copy of with characters removed so that the whole thing passes IsSigned.
/// That is, the first char of the copy will be '-' or a digit, and all subsequent chars of the copy will be digits.
///
/// If contains a serialized negative integer, it must be at the start ([0] == '-') or the sign will be dropped.
public static string OnlySigned(this string? raw)
{
if (string.IsNullOrWhiteSpace(raw)) return string.Empty;
return raw[0].IsSigned()
? raw[0] + string.Concat(raw.Skip(1).Where(IsUnsigned))
: string.Concat(raw.Skip(1).Where(IsUnsigned));
}
///
/// A copy of with characters removed so that the whole thing passes IsHex.
/// That is, all chars of the copy will be hex digits ([0-9A-F]).
///
public static string OnlyHex(this string? raw) => string.IsNullOrWhiteSpace(raw) ? string.Empty : string.Concat(raw.Where(IsHex)).ToUpperInvariant();
///
/// A copy of with characters removed so that the whole thing passes IsFixedPoint.
/// That is, the all chars of the copy will be '.' or a digit and the copy will contain at most 1 decimal separator '.'.
///
///
/// The returned value may not be parseable by float.TryParse.
/// Also this has nothing to do with fixed- vs. floating-point numbers, a better name would be IsUnsignedDecimal.
///
public static string OnlyFixedPoint(this string? raw)
{
if (string.IsNullOrWhiteSpace(raw)) return string.Empty;
var output = new StringBuilder();
var usedDot = false;
foreach (var chr in raw)
{
if (chr == '.')
{
if (usedDot) continue;
output.Append(chr);
usedDot = true;
}
else if (chr.IsUnsigned()) output.Append(chr);
}
return output.ToString();
}
///
/// A copy of with characters removed so that the whole thing passes IsFloat.
/// That is, the first char of the copy will be '-', '.', or a digit,
/// all subsequent chars of the copy will be '.' or a digit, and
/// the copy will contain at most 1 decimal separator '.'.
///
///
/// If contains a serialized negative decimal, it must be at the start ([0] == '-') or the sign will be dropped.
/// The returned value may not be parseable by float.TryParse.
/// Also this has nothing to do with fixed- vs. floating-point numbers, a better name would be IsSignedDecimal.
///
public static string OnlyFloat(this string? raw)
{
if (string.IsNullOrWhiteSpace(raw)) return string.Empty;
var output = new StringBuilder();
var usedDot = false;
var first = raw[0];
if (first.IsFloat())
{
output.Append(first);
if (first == '.') usedDot = true;
}
for (int i = 1, l = raw.Length; i < l; i++)
{
var chr = raw[i];
if (chr == '.')
{
if (usedDot) continue;
output.Append(chr);
usedDot = true;
}
else if (chr.IsUnsigned()) output.Append(chr);
}
return output.ToString();
}
#pragma warning restore CS8602
}
}