优化 Utility 和 DefaultConvert 功能及单元测试

- 新增 `ToDateTimeOffset` 方法的 XML 注释。
- 更新 `Trim` 方法注释,增加对微秒 (us) 的支持。
- 修复 `ToGMK` 方法负数处理逻辑。
- 为 `DefaultConvert` 类新增 16 字节数组解析为 `Decimal` 的支持。
- 增强 `ToBoolean` 方法对布尔值同义词的支持。
- 优化数字字符串的全角字符处理逻辑。
- 改进 `Trim` 方法,统一使用 ticks 粒度裁剪时间。
- 改进 `ToString` 方法,确保使用 `InvariantCulture`。
- 新增多项单元测试,覆盖新功能和边界情况。
- 修复注释术语错误,完善对微秒的支持说明。
- 改进数字解析逻辑,支持清理常见分隔符及全角字符。
This commit is contained in:
石头
2025-09-21 23:38:28 +08:00
parent 11961567ab
commit 0c90dce359
2 changed files with 159 additions and 24 deletions

View File

@@ -91,7 +91,7 @@ public static class Utility
/// <returns></returns>
public static DateTimeOffset ToDateTimeOffset(this Object? value, DateTimeOffset defaultValue) => Convert.ToDateTimeOffset(value, defaultValue);
/// <summary>去掉时间日期指定位置后面部分可指定毫秒ms、秒s、分m、小时h、纳秒ns</summary>
/// <summary>去掉时间日期指定位置后面部分可指定毫秒ms、秒s、分m、小时h、微秒us、纳秒ns</summary>
/// <param name="value">时间日期</param>
/// <param name="format">格式字符串默认s格式化到秒ms格式化到毫秒</param>
/// <returns></returns>
@@ -156,7 +156,7 @@ public static class Utility
/// <param name="value">数值</param>
/// <param name="format">格式化字符串</param>
/// <returns></returns>
public static String ToGMK(this Int64 value, String? format = null) => value < 0 ? value + "" : Convert.ToGMK((UInt64)value, format);
public static String ToGMK(this Int64 value, String? format = null) => value < 0 ? "-" + Convert.ToGMK((UInt64)(-value), format) : Convert.ToGMK((UInt64)value, format);
#endregion
#region
@@ -452,8 +452,24 @@ public class DefaultConvert
case 2: return BitConverter.ToInt16(buf, 0);
case 3: return buf[0] | (buf[1] << 8) | (buf[2] << 16);
case 4: return BitConverter.ToInt32(buf, 0);
case 16:
{
// 按 Decimal 内部 4*Int32 bits 构造lo, mid, hi, flags。仅在 flags 合法时采纳,否则回退。
var lo = BitConverter.ToInt32(buf, 0);
var mid = BitConverter.ToInt32(buf, 4);
var hi = BitConverter.ToInt32(buf, 8);
var flags = BitConverter.ToInt32(buf, 12);
var scale = (flags >> 16) & 0xFF;
var reserved = flags & 0x7F00FFFF; // 除去符号位与比例位其余应为0
if (scale <= 28 && reserved == 0)
{
try { return new Decimal([lo, mid, hi, flags]); } catch { /* fallthrough */ }
}
// 非法 flags回退为 Double 解析
goto default;
}
default:
// 凑够8字节
// 凑够8字节,使用 Double 近似解析
if (buf.Length < 8)
{
var bts = Pool.Shared.Rent(8);
@@ -506,7 +522,13 @@ public class DefaultConvert
if (String.Equals(str, Boolean.TrueString, StringComparison.OrdinalIgnoreCase)) return true;
if (String.Equals(str, Boolean.FalseString, StringComparison.OrdinalIgnoreCase)) return false;
return Int32.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n) ? n != 0 : defaultValue;
// 常见配置值同义词
return str.ToLowerInvariant() switch
{
"y" or "yes" or "on" or "enable" or "enabled" => true,
"n" or "no" or "off" or "disable" or "disabled" => false,
_ => Int32.TryParse(str, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n) ? n != 0 : defaultValue,
};
}
try
@@ -663,6 +685,10 @@ public class DefaultConvert
{
// 去掉逗号分隔符
var ch = input[i];
if (ch == 0x3000)
ch = (Char)0x20; // 全角空格
else if (ch is > (Char)0xFF00 and < (Char)0xFF5F)
ch = (Char)(input[i] - 0xFEE0);
if (ch == ',' || ch == '_' || ch == ' ') continue;
// 支持前缀正号。Redis响应中就会返回带正号的整数
@@ -675,12 +701,6 @@ public class DefaultConvert
// 支持负数
if (ch == '-' && idx > 0) return 0;
// 全角空格
if (ch == 0x3000)
ch = (Char)0x20;
else if (ch is > (Char)0xFF00 and < (Char)0xFF5F)
ch = (Char)(input[i] - 0xFEE0);
// 数字和小数点 以外字符,认为非数字
if (ch is '.' or '-' or not < '0' and not > '9')
output[idx++] = ch;
@@ -704,24 +724,26 @@ public class DefaultConvert
return idx;
}
/// <summary>去掉时间日期指定位置后面部分可指定毫秒ms、秒s、分m、小时h、纳秒ns</summary>
/// <summary>去掉时间日期指定位置后面部分可指定毫秒ms、秒s、分m、小时h、微秒us、纳秒ns</summary>
/// <param name="value">时间日期</param>
/// <param name="format">格式字符串默认s格式化到秒ms格式化到毫秒</param>
/// <returns></returns>
public virtual DateTime Trim(DateTime value, String format)
{
return format switch
// 统一使用 ticks 粒度裁剪,更高效且避免构造函数校验/进位误差
var step = format switch
{
#if NET7_0_OR_GREATER
"us" => new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond, value.Microsecond, value.Kind),
"ns" => new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond, value.Microsecond / 100 * 100, value.Kind),
#endif
"ms" => new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Millisecond, value.Kind),
"s" => new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, value.Second, value.Kind),
"m" => new DateTime(value.Year, value.Month, value.Day, value.Hour, value.Minute, 0, value.Kind),
"h" => new DateTime(value.Year, value.Month, value.Day, value.Hour, 0, 0, value.Kind),
_ => value,
"ms" => TimeSpan.TicksPerMillisecond,
"s" => TimeSpan.TicksPerSecond,
"m" => TimeSpan.TicksPerMinute,
"h" => TimeSpan.TicksPerHour,
"us" => 10, // 1 微秒 = 10 ticks
"ns" => 1, // 1 tick = 100ns已是最小粒度
_ => 0,
};
if (step <= 0) return value;
var ticks = value.Ticks / step * step;
return new DateTime(ticks, value.Kind);
}
/// <summary>时间日期转为yyyy-MM-dd HH:mm:ss完整字符串</summary>
@@ -891,9 +913,9 @@ public class DefaultConvert
{
if (emptyValue != null && value <= DateTime.MinValue) return emptyValue;
//return value.ToString(format ?? "yyyy-MM-dd HH:mm:ss");
return format.IsNullOrEmpty() || format == "yyyy-MM-dd HH:mm:ss" ? ToFullString(value, false, emptyValue) : value.ToString(format);
return format.IsNullOrEmpty() || format == "yyyy-MM-dd HH:mm:ss"
? ToFullString(value, false, emptyValue)
: value.ToString(format, CultureInfo.InvariantCulture);
}
/// <summary>获取内部真实异常</summary>

View File

@@ -2,6 +2,7 @@
using NewLife;
using NewLife.Log;
using Xunit;
using System.Globalization;
namespace XUnitTest.Common;
@@ -425,4 +426,116 @@ public class UtilityTests
Assert.True(Math.Abs(ticks - d3) < 1000_0000_0000);
//Assert.Equal(d2, d3);
}
[Fact]
public void GMK_Negative_Format()
{
var n = -1024L;
Assert.Equal("-1.00K", n.ToGMK("n2"));
n = -1L;
Assert.Equal("-1", n.ToGMK());
}
[Fact]
public void Trim_Ticks_Precision()
{
var now = DateTime.Now;
var ms = now.Trim("ms");
Assert.Equal(0, ms.Ticks % TimeSpan.TicksPerMillisecond);
var s = now.Trim("s");
Assert.Equal(0, s.Ticks % TimeSpan.TicksPerSecond);
var m = now.Trim("m");
Assert.Equal(0, m.Ticks % TimeSpan.TicksPerMinute);
var h = now.Trim("h");
Assert.Equal(0, h.Ticks % TimeSpan.TicksPerHour);
var us = now.Trim("us");
Assert.Equal(0, us.Ticks % 10);
var ns = now.Trim("ns");
// DateTime tick = 100ns按 1 tick 裁剪等于不变
Assert.Equal(now, ns);
}
[Fact]
public void Decimal_From_Bytes_16()
{
var expected = 12345.6789m;
var bits = decimal.GetBits(expected); // lo, mid, hi, flags
var buf = new byte[16];
System.Buffer.BlockCopy(BitConverter.GetBytes(bits[0]), 0, buf, 0, 4);
System.Buffer.BlockCopy(BitConverter.GetBytes(bits[1]), 0, buf, 4, 4);
System.Buffer.BlockCopy(BitConverter.GetBytes(bits[2]), 0, buf, 8, 4);
System.Buffer.BlockCopy(BitConverter.GetBytes(bits[3]), 0, buf, 12, 4);
var val = buf.ToDecimal();
Assert.Equal(expected, val);
}
[Fact]
public void Boolean_Synonyms()
{
Assert.True("yes".ToBoolean());
Assert.True("Y".ToBoolean());
Assert.True("On".ToBoolean());
Assert.True("ENABLED".ToBoolean());
Assert.False("no".ToBoolean());
Assert.False("N".ToBoolean());
Assert.False("off".ToBoolean());
Assert.False("disabled".ToBoolean());
}
[Fact]
public void InvariantCulture_Number_Parsing()
{
var old = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo("fr-FR");
// 小数点必须是点千分位逗号InvariantCulture 不受当前区域影响
Assert.Equal(1234.56, "1,234.56".ToDouble());
Assert.Equal(1234.56m, "1,234.56".ToDecimal());
// 设计目标:去掉空格/逗号等分隔符,法语写法会被清理为整数
Assert.Equal(123456d, "1 234,56".ToDouble());
Assert.Equal(123456m, "1 234,56".ToDecimal());
}
finally
{
CultureInfo.CurrentCulture = old;
}
}
[Fact]
public void TrimNumber_Cleans_Common_Separators()
{
// 逗号 / 空格 / 下划线 会被清理
Assert.Equal(123456d, "1,234,56".ToDouble());
Assert.Equal(123456d, "1 234 56".ToDouble());
Assert.Equal(123456d, "1_234_56".ToDouble());
// 科学计数法保留
Assert.Equal(1.23e6, "1.23e6".ToDouble());
// 全角数字与全角空格
var fullWidth = " "; // 1,234⎵56全角
Assert.Equal(123456d, fullWidth.ToDouble());
// 正/负号
Assert.Equal(+123456d, "+123,456".ToDouble());
Assert.Equal(-123456d, "-123,456".ToDouble());
// 小数点 + 千分符
Assert.Equal(1234.56d, "1,234.56".ToDouble());
Assert.Equal(1234.56m, "1,234.56".ToDecimal());
// 使用逗号作为小数点(如法语),按当前转换策略将被视为清理后整数
Assert.Equal(123456d, "1 234,56".ToDouble());
Assert.Equal(123456m, "1 234,56".ToDecimal());
}
}