Allow negative values to be entered in ram watch for fixed-point watches (#3181)

* structurize NumericStringExtensions better, allow negative fixedpoint numbers

* Tune MaxLength values to make more sense

* Use Invariant culture to prevent ","<->"." issue in decimal recognition for some locales

* Cleanup more watch stuff

- simplify Poke functions by removing redundant text checking
- make fixed-point constants const
- remove OnKeyPress overload; let the user type whatever and check it later, similarly in OnTextChanged
- more invariant NumberFormatInfo use for ","<->"." issues, also do a basic fixup on OnKeyDown
- cleanup ToRawInt and SetFromRawInt, there are likely still issues remaining but I have no idea whether they realistically affect anything with the way those functions are used
- remove now unused functions in NumericStringExtensions
This commit is contained in:
Moritz Bender 2022-03-23 18:19:49 +01:00 committed by GitHub
parent 64ac7253f6
commit 6db532fb84
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 92 additions and 539 deletions

View File

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using BizHawk.Common.StringExtensions;
using BizHawk.Emulation.Common;
namespace BizHawk.Client.Common
@ -76,54 +74,14 @@ namespace BizHawk.Client.Common
{
try
{
byte val = 0;
switch (Type)
byte val = Type switch
{
case WatchDisplayType.Unsigned:
if (value.IsUnsigned())
{
val = (byte)int.Parse(value);
}
else
{
return false;
}
break;
case WatchDisplayType.Signed:
if (value.IsSigned())
{
val = (byte)(sbyte)int.Parse(value);
}
else
{
return false;
}
break;
case WatchDisplayType.Hex:
if (value.IsHex())
{
val = (byte)int.Parse(value, NumberStyles.HexNumber);
}
else
{
return false;
}
break;
case WatchDisplayType.Binary:
if (value.IsBinary())
{
val = (byte)Convert.ToInt32(value, 2);
}
else
{
return false;
}
break;
}
WatchDisplayType.Unsigned => byte.Parse(value),
WatchDisplayType.Signed => (byte)sbyte.Parse(value),
WatchDisplayType.Hex => byte.Parse(value, NumberStyles.HexNumber),
WatchDisplayType.Binary => Convert.ToByte(value, 2),
_ => 0
};
PokeByte(val);
return true;

View File

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using BizHawk.Common.StringExtensions;
using BizHawk.Emulation.Common;
namespace BizHawk.Client.Common
@ -79,77 +77,16 @@ namespace BizHawk.Client.Common
{
try
{
uint val = 0;
switch (Type)
uint val = Type switch
{
case WatchDisplayType.Unsigned:
if (value.IsUnsigned())
{
val = (uint)int.Parse(value);
}
else
{
return false;
}
break;
case WatchDisplayType.Signed:
if (value.IsSigned())
{
val = (uint)int.Parse(value);
}
else
{
return false;
}
break;
case WatchDisplayType.Hex:
if (value.IsHex())
{
val = (uint)int.Parse(value, NumberStyles.HexNumber);
}
else
{
return false;
}
break;
case WatchDisplayType.FixedPoint_20_12:
if (value.IsFixedPoint())
{
val = (uint)(int)(double.Parse(value) * 4096.0);
}
else
{
return false;
}
break;
case WatchDisplayType.FixedPoint_16_16:
if (value.IsFixedPoint())
{
val = (uint)(int)(double.Parse(value) * 65536.0);
}
else
{
return false;
}
break;
case WatchDisplayType.Float:
if (value.IsFloat())
{
var bytes = BitConverter.GetBytes(float.Parse(value));
val = BitConverter.ToUInt32(bytes, 0);
}
else
{
return false;
}
break;
}
WatchDisplayType.Unsigned => uint.Parse(value),
WatchDisplayType.Signed => (uint)int.Parse(value),
WatchDisplayType.Hex => uint.Parse(value, NumberStyles.HexNumber),
WatchDisplayType.FixedPoint_20_12 => (uint)(double.Parse(value, NumberFormatInfo.InvariantInfo) * 4096.0),
WatchDisplayType.FixedPoint_16_16 => (uint)(double.Parse(value, NumberFormatInfo.InvariantInfo) * 65536.0),
WatchDisplayType.Float => BitConverter.ToUInt32(BitConverter.GetBytes(float.Parse(value, NumberFormatInfo.InvariantInfo)), 0),
_ => 0
};
PokeDWord(val);
return true;
@ -198,7 +135,7 @@ namespace BizHawk.Client.Common
{
var bytes = BitConverter.GetBytes(val);
var _float = BitConverter.ToSingle(bytes, 0);
return _float.ToString();
return _float.ToString(NumberFormatInfo.InvariantInfo);
}
string FormatBinary()
@ -217,8 +154,8 @@ namespace BizHawk.Client.Common
WatchDisplayType.Unsigned => val.ToString(),
WatchDisplayType.Signed => ((int)val).ToString(),
WatchDisplayType.Hex => $"{val:X8}",
WatchDisplayType.FixedPoint_20_12 => $"{(int)val / 4096.0:0.######}",
WatchDisplayType.FixedPoint_16_16 => $"{(int)val / 65536.0:0.######}",
WatchDisplayType.FixedPoint_20_12 => ((int)val / 4096.0).ToString("0.######", NumberFormatInfo.InvariantInfo),
WatchDisplayType.FixedPoint_16_16 => ((int)val / 65536.0).ToString("0.######", NumberFormatInfo.InvariantInfo),
WatchDisplayType.Float => FormatFloat(),
WatchDisplayType.Binary => FormatBinary(),
_ => val.ToString()

View File

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using BizHawk.Common.StringExtensions;
using BizHawk.Emulation.Common;
namespace BizHawk.Client.Common
@ -74,65 +72,15 @@ namespace BizHawk.Client.Common
{
try
{
ushort val = 0;
switch (Type)
ushort val = Type switch
{
case WatchDisplayType.Unsigned:
if (value.IsUnsigned())
{
val = (ushort)int.Parse(value);
}
else
{
return false;
}
break;
case WatchDisplayType.Signed:
if (value.IsSigned())
{
val = (ushort)(short)int.Parse(value);
}
else
{
return false;
}
break;
case WatchDisplayType.Hex:
if (value.IsHex())
{
val = (ushort)int.Parse(value, NumberStyles.HexNumber);
}
else
{
return false;
}
break;
case WatchDisplayType.Binary:
if (value.IsBinary())
{
val = (ushort)Convert.ToInt32(value, 2);
}
else
{
return false;
}
break;
case WatchDisplayType.FixedPoint_12_4:
if (value.IsFixedPoint())
{
val = (ushort)(double.Parse(value) * 16.0);
}
else
{
return false;
}
break;
}
WatchDisplayType.Unsigned => ushort.Parse(value),
WatchDisplayType.Signed => (ushort)short.Parse(value),
WatchDisplayType.Hex => ushort.Parse(value, NumberStyles.HexNumber),
WatchDisplayType.Binary => Convert.ToUInt16(value, 2),
WatchDisplayType.FixedPoint_12_4 => (ushort)(double.Parse(value, NumberFormatInfo.InvariantInfo) * 16.0),
_ => 0
};
PokeWord(val);
return true;
@ -183,7 +131,7 @@ namespace BizHawk.Client.Common
_ when !IsValid => "-",
WatchDisplayType.Unsigned => val.ToString(),
WatchDisplayType.Signed => ((short) val).ToString(), WatchDisplayType.Hex => $"{val:X4}",
WatchDisplayType.FixedPoint_12_4 => $"{val / 16.0:F4}",
WatchDisplayType.FixedPoint_12_4 => ((short)val / 16.0).ToString("F4", NumberFormatInfo.InvariantInfo),
WatchDisplayType.Binary => Convert
.ToString(val, 2)
.PadLeft(16, '0')

View File

@ -2,10 +2,8 @@
using System.Globalization;
using System.Linq;
using System.Windows.Forms;
using BizHawk.Common.StringExtensions;
using BizHawk.Common.NumberExtensions;
using BizHawk.Client.Common;
using BizHawk.Common.NumberExtensions;
namespace BizHawk.Client.EmuHawk
{
@ -27,7 +25,7 @@ namespace BizHawk.Client.EmuHawk
set
{
var changed = _size != value;
_size = value;
if (changed)
{
@ -87,17 +85,20 @@ namespace BizHawk.Client.EmuHawk
_ => sbyte.MinValue
};
private double Max12_4 => MaxUnsignedInt / 16.0;
private const double Max12_4 = short.MaxValue / 16.0;
private const double Min12_4 = short.MinValue / 16.0;
private double Max20_12 => MaxUnsignedInt / 4096.0;
private const double Max20_12 = int.MaxValue / 4096.0;
private const double Min20_12 = int.MinValue / 4096.0;
private double Max16_16 => MaxUnsignedInt / 65536.0;
private const double Max16_16 = int.MaxValue / 65536.0;
private const double Min16_16 = int.MinValue / 65536.0;
private static double _12_4_Unit => 1 / 16.0;
private const double _12_4_Unit = 1 / 16.0;
private static double _20_12_Unit => 1 / 4096.0;
private const double _20_12_Unit = 1 / 4096.0;
private static double _16_16_Unit => 1 / 65536.0;
private const double _16_16_Unit = 1 / 65536.0;
public override void ResetText()
{
@ -173,88 +174,14 @@ namespace BizHawk.Client.EmuHawk
};
break;
case WatchDisplayType.FixedPoint_12_4:
MaxLength = 9;
MaxLength = 10;
break;
case WatchDisplayType.Float:
MaxLength = 21;
MaxLength = 40;
break;
case WatchDisplayType.FixedPoint_20_12:
case WatchDisplayType.FixedPoint_16_16:
MaxLength = 64;
break;
}
}
protected override void OnKeyPress(KeyPressEventArgs e)
{
if (e.KeyChar == '\b' || e.KeyChar == 22 || e.KeyChar == 1 || e.KeyChar == 3)
{
return;
}
if (e.KeyChar == '.')
{
if (Text.Contains(".") && !SelectedText.Contains("."))
{
e.Handled = true;
return;
}
}
else if (e.KeyChar == '-')
{
if (Text.Contains("-") && !SelectedText.Contains("-"))
{
e.Handled = true;
return;
}
}
switch (_type)
{
default:
case WatchDisplayType.Binary:
if (!e.KeyChar.IsBinary())
{
e.Handled = true;
}
break;
case WatchDisplayType.FixedPoint_12_4:
case WatchDisplayType.FixedPoint_20_12:
case WatchDisplayType.FixedPoint_16_16:
if (!e.KeyChar.IsFixedPoint())
{
e.Handled = true;
}
break;
case WatchDisplayType.Float:
if (!e.KeyChar.IsFloat())
{
e.Handled = true;
}
break;
case WatchDisplayType.Hex:
if (!e.KeyChar.IsHex())
{
e.Handled = true;
}
break;
case WatchDisplayType.Signed:
if (!e.KeyChar.IsSigned())
{
e.Handled = true;
}
break;
case WatchDisplayType.Unsigned:
if (!e.KeyChar.IsUnsigned())
{
e.Handled = true;
}
MaxLength = 24;
break;
}
}
@ -321,47 +248,47 @@ namespace BizHawk.Client.EmuHawk
Text = hexVal.ToHexString(MaxLength);
break;
case WatchDisplayType.FixedPoint_12_4:
var f12val = double.Parse(text);
var f12val = double.Parse(text, NumberFormatInfo.InvariantInfo);
if (f12val > Max12_4 - _12_4_Unit)
{
f12val = 0;
f12val = Min12_4;
}
else
{
f12val += _12_4_Unit;
}
Text = f12val.ToString();
Text = f12val.ToString(NumberFormatInfo.InvariantInfo);
break;
case WatchDisplayType.FixedPoint_20_12:
var f24val = double.Parse(text);
if (f24val >= Max20_12 - _20_12_Unit)
var f20val = double.Parse(text, NumberFormatInfo.InvariantInfo);
if (f20val > Max20_12 - _20_12_Unit)
{
f24val = 0;
f20val = Min20_12;
}
else
{
f24val += _20_12_Unit;
f20val += _20_12_Unit;
}
Text = f24val.ToString();
Text = f20val.ToString(NumberFormatInfo.InvariantInfo);
break;
case WatchDisplayType.FixedPoint_16_16:
var f16val = double.Parse(text);
if (f16val >= Max16_16 - _16_16_Unit)
var f16val = double.Parse(text, NumberFormatInfo.InvariantInfo);
if (f16val > Max16_16 - _16_16_Unit)
{
f16val = 0;
f16val = Min16_16;
}
else
{
f16val += _16_16_Unit;
}
Text = f16val.ToString();
Text = f16val.ToString(NumberFormatInfo.InvariantInfo);
break;
case WatchDisplayType.Float:
var dVal = double.Parse(text);
if (dVal > double.MaxValue - 1)
var dVal = double.Parse(text, NumberFormatInfo.InvariantInfo);
if (dVal > float.MaxValue - 1)
{
dVal = 0;
}
@ -370,7 +297,7 @@ namespace BizHawk.Client.EmuHawk
dVal++;
}
Text = dVal.ToString();
Text = dVal.ToString(NumberFormatInfo.InvariantInfo);
break;
}
}
@ -437,8 +364,8 @@ namespace BizHawk.Client.EmuHawk
Text = hexVal.ToHexString(MaxLength);
break;
case WatchDisplayType.FixedPoint_12_4:
var f12val = double.Parse(text);
if (f12val < 0 + _12_4_Unit)
var f12val = double.Parse(text, NumberFormatInfo.InvariantInfo);
if (f12val < Min12_4 + _12_4_Unit)
{
f12val = Max12_4;
}
@ -447,24 +374,24 @@ namespace BizHawk.Client.EmuHawk
f12val -= _12_4_Unit;
}
Text = f12val.ToString();
Text = f12val.ToString(NumberFormatInfo.InvariantInfo);
break;
case WatchDisplayType.FixedPoint_20_12:
var f24val = double.Parse(text);
if (f24val < 0 + _20_12_Unit)
var f20val = double.Parse(text, NumberFormatInfo.InvariantInfo);
if (f20val < Min20_12 + _20_12_Unit)
{
f24val = Max20_12;
f20val = Max20_12;
}
else
{
f24val -= _20_12_Unit;
f20val -= _20_12_Unit;
}
Text = f24val.ToString();
Text = f20val.ToString(NumberFormatInfo.InvariantInfo);
break;
case WatchDisplayType.FixedPoint_16_16:
var f16val = double.Parse(text);
if (f16val < 0 + _16_16_Unit)
var f16val = double.Parse(text, NumberFormatInfo.InvariantInfo);
if (f16val < Min16_16 + _16_16_Unit)
{
f16val = Max16_16;
}
@ -473,11 +400,11 @@ namespace BizHawk.Client.EmuHawk
f16val -= _16_16_Unit;
}
Text = f16val.ToString();
Text = f16val.ToString(NumberFormatInfo.InvariantInfo);
break;
case WatchDisplayType.Float:
var dval = double.Parse(text);
if (dval > double.MaxValue - 1)
var dval = double.Parse(text, NumberFormatInfo.InvariantInfo);
if (dval < float.MinValue + 1)
{
dval = 0;
}
@ -486,7 +413,7 @@ namespace BizHawk.Client.EmuHawk
dval--;
}
Text = dval.ToString();
Text = dval.ToString(NumberFormatInfo.InvariantInfo);
break;
}
}
@ -505,113 +432,32 @@ namespace BizHawk.Client.EmuHawk
return;
}
switch (_type)
{
case WatchDisplayType.Signed:
Text = Text.OnlySigned();
break;
case WatchDisplayType.Unsigned:
Text = Text.OnlyUnsigned();
break;
case WatchDisplayType.Binary:
Text = Text.OnlyBinary();
break;
case WatchDisplayType.Hex:
Text = Text.OnlyHex();
break;
case WatchDisplayType.FixedPoint_12_4:
case WatchDisplayType.FixedPoint_20_12:
case WatchDisplayType.FixedPoint_16_16:
Text = Text.OnlyFixedPoint();
break;
case WatchDisplayType.Float:
Text = Text.OnlyFloat();
break;
}
base.OnTextChanged(e);
}
public int? ToRawInt()
{
if (string.IsNullOrWhiteSpace(Text))
try
{
if (Nullable)
return _type switch
{
return null;
}
return 0;
WatchDisplayType.Signed => int.Parse(Text),
WatchDisplayType.Unsigned => (int)uint.Parse(Text),
WatchDisplayType.Binary => Convert.ToInt32(Text, 2),
WatchDisplayType.Hex => int.Parse(Text, NumberStyles.HexNumber),
WatchDisplayType.FixedPoint_12_4 => (int)(double.Parse(Text, NumberFormatInfo.InvariantInfo) * 16.0),
WatchDisplayType.FixedPoint_20_12 => (int)(double.Parse(Text, NumberFormatInfo.InvariantInfo) * 4096.0),
WatchDisplayType.FixedPoint_16_16 => (int)(double.Parse(Text, NumberFormatInfo.InvariantInfo) * 65536.0),
WatchDisplayType.Float => BitConverter.ToInt32(BitConverter.GetBytes(float.Parse(Text, NumberFormatInfo.InvariantInfo)), 0),
_ => int.Parse(Text)
};
}
switch (_type)
catch
{
case WatchDisplayType.Signed:
if (Text.IsSigned())
{
return Text == "-" ? 0 : int.Parse(Text);
}
break;
case WatchDisplayType.Unsigned:
if (Text.IsUnsigned())
{
return (int)uint.Parse(Text);
}
break;
case WatchDisplayType.Binary:
if (Text.IsBinary())
{
return Convert.ToInt32(Text, 2);
}
break;
case WatchDisplayType.Hex:
if (Text.IsHex())
{
return int.Parse(Text, NumberStyles.HexNumber);
}
break;
case WatchDisplayType.FixedPoint_12_4:
if (Text.IsFixedPoint())
{
return (int)(double.Parse(Text) * 16.0);
}
break;
case WatchDisplayType.FixedPoint_20_12:
if (Text.IsFixedPoint())
{
return (int)(double.Parse(Text) * 4096.0);
}
break;
case WatchDisplayType.FixedPoint_16_16:
if (Text.IsFixedPoint())
{
return (int)(double.Parse(Text) * 65536.0);
}
break;
case WatchDisplayType.Float:
if (Text.IsFloat())
{
if (Text == "-" || Text == ".")
{
return 0;
}
float val = float.Parse(Text);
var bytes = BitConverter.GetBytes(val);
return BitConverter.ToInt32(bytes, 0);
}
break;
// ignored
}
return 0;
return Nullable ? null : 0;
}
public void SetFromRawInt(int? val)
@ -637,18 +483,18 @@ namespace BizHawk.Client.EmuHawk
Text = val.Value.ToHexString(MaxLength);
break;
case WatchDisplayType.FixedPoint_12_4:
Text = $"{val.Value / 16.0:F5}";
Text = (val.Value / 16.0).ToString("F5", NumberFormatInfo.InvariantInfo);
break;
case WatchDisplayType.FixedPoint_20_12:
Text = $"{val.Value / 4096.0:F5}";
Text = (val.Value / 4096.0).ToString("F5", NumberFormatInfo.InvariantInfo);
break;
case WatchDisplayType.FixedPoint_16_16:
Text = $"{val.Value / 65536.0:F5}";
Text = (val.Value / 65536.0).ToString("F5", NumberFormatInfo.InvariantInfo);
break;
case WatchDisplayType.Float:
var bytes = BitConverter.GetBytes(val.Value);
float _float = BitConverter.ToSingle(bytes, 0);
Text = $"{_float:F6}";
Text = _float.ToString("F6", NumberFormatInfo.InvariantInfo);
break;
}
}

View File

@ -1,5 +1,4 @@
using System.Linq;
using System.Text;
namespace BizHawk.Common.StringExtensions
{
@ -13,26 +12,6 @@ namespace BizHawk.Common.StringExtensions
/// <remarks><paramref name="str"/> should exclude the prefix <c>0b</c></remarks>
public static bool IsBinary(this string? str) => !string.IsNullOrWhiteSpace(str) && str.All(IsBinary);
/// <returns><see langword="true"/> iff <paramref name="c"/> is <c>'.'</c> or a digit</returns>
/// <remarks>Also this has nothing to do with fixed- vs. floating-point numbers, a better name would be <c>IsUnsignedDecimal</c>.</remarks>
public static bool IsFixedPoint(this char c) => IsUnsigned(c) || c == '.';
/// <returns>
/// <see langword="true"/> iff <paramref name="str"/> is not <see langword="null"/>,<br/>
/// all chars of <paramref name="str"/> are <c>'.'</c> or a digit, and<br/>
/// <paramref name="str"/> contains at most <c>1</c> decimal separator <c>'.'</c>
/// </returns>
/// <remarks>
/// <paramref name="str"/> should exclude the suffix <c>M</c>.<br/>
/// This method returning <see langword="true"/> for some <paramref name="str"/> does not imply that <see cref="float.TryParse(string,out float)">float.TryParse</see> will also return <see langword="true"/>.<br/>
/// Also this has nothing to do with fixed- vs. floating-point numbers, a better name would be <c>IsUnsignedDecimal</c>.
/// </remarks>
public static bool IsFixedPoint(this string? str) => !string.IsNullOrWhiteSpace(str) && str.Count(c => c == '.') <= 1 && str.All(IsFixedPoint);
/// <returns><see langword="true"/> iff <paramref name="c"/> is <c>'-'</c>, <c>'.'</c>, or a digit</returns>
/// <remarks>Also this has nothing to do with fixed- vs. floating-point numbers, a better name would be <c>IsSignedDecimal</c>.</remarks>
public static bool IsFloat(this char c) => c.IsFixedPoint() || c == '-';
/// <returns><see langword="true"/> iff <paramref name="c"/> is a hex digit (<c>[0-9A-Fa-f]</c>)</returns>
public static bool IsHex(this char c) => IsUnsigned(c) || 'A' <= char.ToUpperInvariant(c) && char.ToUpperInvariant(c) <= 'F';
@ -46,128 +25,13 @@ namespace BizHawk.Common.StringExtensions
/// <returns><see langword="true"/> iff <paramref name="c"/> is a digit</returns>
public static bool IsUnsigned(this char c) => char.IsDigit(c);
/// <returns><see langword="true"/> iff <paramref name="str"/> is not <see langword="null"/> and all chars of <paramref name="str"/> are digits</returns>
public static bool IsUnsigned(this string? str) => !string.IsNullOrWhiteSpace(str) && str.All(IsUnsigned);
/// <returns>
/// A copy of <paramref name="raw"/> with characters removed so that the whole thing passes <see cref="IsBinary(string?)">IsBinary</see>.<br/>
/// That is, all chars of the copy will be either <c>'0'</c> or <c>'1'</c>.
/// </returns>
public static string OnlyBinary(this string? raw) => string.IsNullOrWhiteSpace(raw) ? string.Empty : string.Concat(raw.Where(IsBinary));
/// <returns>
/// A copy of <paramref name="raw"/> with characters removed so that the whole thing passes <see cref="IsHex(string?)">IsHex</see>.<br/>
/// That is, all chars of the copy will be hex digits (<c>[0-9A-F]</c>).
/// </returns>
public static string OnlyHex(this string? raw) => string.IsNullOrWhiteSpace(raw) ? string.Empty : string.Concat(raw.Where(IsHex)).ToUpperInvariant();
/// <returns>
/// A copy of <paramref name="raw"/> with characters removed so that the whole thing passes <see cref="IsUnsigned(string?)">IsUnsigned</see>.<br/>
/// That is, all chars of the copy will be digits.
/// </returns>
public static string OnlyUnsigned(this string? raw) => string.IsNullOrWhiteSpace(raw) ? string.Empty : string.Concat(raw.Where(IsUnsigned));
#pragma warning disable CS8602
/// <returns>
/// <see langword="true"/> iff <paramref name="str"/> is not <see langword="null"/>,<br/>
/// the first char of <paramref name="str"/> is <c>'-'</c>, <c>'.'</c>, or a digit,<br/>
/// all subsequent chars of <paramref name="str"/> are <c>'.'</c> or a digit, and<br/>
/// <paramref name="str"/> contains at most <c>1</c> decimal separator <c>'.'</c>
/// </returns>
/// <remarks>
/// <paramref name="str"/> should exclude the suffix <c>f</c>.<br/>
/// This method returning <see langword="true"/> for some <paramref name="str"/> does not imply that <see cref="float.TryParse(string,out float)">float.TryParse</see> will also return <see langword="true"/>.<br/>
/// Also this has nothing to do with fixed- vs. floating-point numbers, a better name would be <c>IsSignedDecimal</c>.
/// </remarks>
public static bool IsFloat(this string? str) => !string.IsNullOrWhiteSpace(str) && str.Count(c => c == '.') <= 1 && str[0].IsFloat() && str.Substring(1).All(IsFixedPoint);
/// <returns>
/// <see langword="true"/> iff <paramref name="str"/> is not <see langword="null"/>,
/// the first char of <paramref name="str"/> is <c>'-'</c> or a digit, and
/// all subsequent chars of <paramref name="str"/> are digits
/// </returns>
public static bool IsSigned(this string? str) => !string.IsNullOrWhiteSpace(str) && str[0].IsSigned() && str.Substring(1).All(IsUnsigned);
/// <returns>
/// A copy of <paramref name="raw"/> with characters removed so that the whole thing passes <see cref="IsFixedPoint(string?)">IsFixedPoint</see>.<br/>
/// That is, the all chars of the copy will be <c>'.'</c> or a digit and the copy will contain at most <c>1</c> decimal separator <c>'.'</c>.
/// </returns>
/// <remarks>
/// The returned value may not be parseable by <see cref="float.TryParse(string,out float)">float.TryParse</see>.<br/>
/// Also this has nothing to do with fixed- vs. floating-point numbers, a better name would be <c>IsUnsignedDecimal</c>.
/// </remarks>
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();
}
/// <returns>
/// A copy of <paramref name="raw"/> with characters removed so that the whole thing passes <see cref="IsFloat(string?)">IsFloat</see>.<br/>
/// That is, the first char of the copy will be <c>'-'</c>, <c>'.'</c>, or a digit,<br/>
/// all subsequent chars of the copy will be <c>'.'</c> or a digit, and<br/>
/// the copy will contain at most <c>1</c> decimal separator <c>'.'</c>.
/// </returns>
/// <remarks>
/// If <paramref name="raw"/> contains a serialized negative decimal, it must be at the start (<paramref name="raw"/><c>[0] == '-'</c>) or the sign will be dropped.<br/>
/// The returned value may not be parseable by <see cref="float.TryParse(string,out float)">float.TryParse</see>.<br/>
/// Also this has nothing to do with fixed- vs. floating-point numbers, a better name would be <c>IsSignedDecimal</c>.
/// </remarks>
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();
}
/// <returns>
/// A copy of <paramref name="raw"/> with characters removed so that the whole thing passes <see cref="IsSigned(string?)">IsSigned</see>.<br/>
/// That is, the first char of the copy will be <c>'-'</c> or a digit, and all subsequent chars of the copy will be digits.
/// </returns>
/// <remarks>If <paramref name="raw"/> contains a serialized negative integer, it must be at the start (<paramref name="raw"/><c>[0] == '-'</c>) or the sign will be dropped.</remarks>
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));
}
#pragma warning restore CS8602
/// <returns><see langword="true"/> iff <paramref name="str"/> is not <see langword="null"/> and all chars of <paramref name="str"/> are digits</returns>
public static bool IsUnsigned(this string? str) => !string.IsNullOrWhiteSpace(str) && str.All(IsUnsigned);
}
}