mirror of
https://gitee.com/NewLifeX/X.git
synced 2025-12-06 09:58:57 +08:00
优化 Utility 和 DefaultConvert 功能及单元测试
- 新增 `ToDateTimeOffset` 方法的 XML 注释。 - 更新 `Trim` 方法注释,增加对微秒 (us) 的支持。 - 修复 `ToGMK` 方法负数处理逻辑。 - 为 `DefaultConvert` 类新增 16 字节数组解析为 `Decimal` 的支持。 - 增强 `ToBoolean` 方法对布尔值同义词的支持。 - 优化数字字符串的全角字符处理逻辑。 - 改进 `Trim` 方法,统一使用 ticks 粒度裁剪时间。 - 改进 `ToString` 方法,确保使用 `InvariantCulture`。 - 新增多项单元测试,覆盖新功能和边界情况。 - 修复注释术语错误,完善对微秒的支持说明。 - 改进数字解析逻辑,支持清理常见分隔符及全角字符。
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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"; // 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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user