C# 数值字符串转数值(泛型)

起因

不同地区的数值字符串,有的用点(‘.’)做小数点,有的用逗号(‘,’)做小数点。需要一个方法,将不同风格的数值字符串转换成点优先的字符串,并最终转换成数值类型。
最终实现效果对照如下:

输入 输出
3,14 3.14
12,34 12.34
1,234 1234
1,234.56 / 1.234,56 1234.56
1,5e3 / 1.5e3 1.5e3 >>> 1500
1,5e-3 / 1.5e-3 1.5e-3 >>> 0.0015

使用正则表达式处理

在线测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

/// <summary>
/// 将数字字符串规范化为“纯点风格”:小数点为 '.',且不含任何千位分隔符(正则版)。
///
/// 修复说明:原实现依赖 decimal.TryParse + AllowThousands 来判断 ',' 是否为千位符,
/// 但 .NET 对分组位数校验是宽松的——"3,14" 会被 Invariant 解析成功为 314,
/// 导致逗号被当千位符删除,返回 "314" 而非 "3.14"。
/// 本版与 TryNormalizeToDot.cs 逻辑一致,仅用正则实现严格三位分组判定。
/// </summary>
public static class NumberNormalizer2
{
/// <summary>
/// 严格的三位千位分组:可选正负号 + 1~3 位首组 + 一个或多个“逗号 + 恰好 3 位数字”分组,
/// 且串内不允许出现任何其他字符(指数 e/E 等一律不匹配)。
/// 例:1,234 / 1,234,567 / -12,345 匹配;3,14 / 1,23,456 / 1,5e3 不匹配。
/// </summary>
private static readonly Regex StrictThousandsGroups =
new Regex(@"\A[+-]?\d{1,3}(,\d{3})+\z", RegexOptions.Compiled);

public static bool TryNormalizeToDot(string value, out string normalized)
{
normalized = null;
if (string.IsNullOrWhiteSpace(value))
return false;

string s = value.Trim();
int lastDot = s.LastIndexOf('.');
int lastComma = s.LastIndexOf(',');

if (lastDot >= 0 && lastComma >= 0)
{
// 1) 两种分隔符并存:最后出现的那个必是小数符(1,234.56 / 1.234,56)
string candidate = lastDot > lastComma
? s.Replace(",", "") // ',' 是千位符
: s.Replace(".", "").Replace(',', '.'); // '.' 是千位符
return TryFinalize(candidate, out normalized);
}

if (lastComma >= 0)
{
// 2) 只有逗号:若整体是严格的三位分组则视为千位符(1,234 → 1234),
// 否则视为小数符(3,14 → 3.14、12,34 → 12.34)
string candidate = StrictThousandsGroups.IsMatch(s)
? s.Replace(",", "")
: s.Replace(',', '.');
return TryFinalize(candidate, out normalized);
}

// 3) 只有点或无分隔符:本身就是点风格,直接校验
// ("1.234" 按 1.234 处理;"1.2.3" 会被校验拒绝)
return TryFinalize(s, out normalized);
}

/// <summary>最终用 Invariant 解析校验候选串是否为合法数字,通过则输出。</summary>
private static bool TryFinalize(string candidate, out string normalized)
{
normalized = null;
if (decimal.TryParse(candidate, NumberStyles.Float, CultureInfo.InvariantCulture, out _))
{
normalized = candidate;
return true;
}
return false;
}

public static bool TryGetValFromModel<T>(string numStr, out T val) where T : struct, IParsable<T>
{
val = default(T);
if (!TryNormalizeToDot(numStr, out string formateStr))
return false;
if (T.TryParse(formateStr, CultureInfo.InvariantCulture, out T _tmpLimit))
{
val = _tmpLimit;
return true;
}
return false;
}
}

使用for循环处理

在线测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
/// <summary>
/// 将数字字符串规范化为“纯点风格”:小数点为 '.',且不含任何千位分隔符。
///
/// 修复说明:原实现依赖 decimal.TryParse + AllowThousands 来判断 ',' 是否为千位符,
/// 但 .NET 对分组位数校验是宽松的——"3,14" 会被 Invariant 解析成功为 314,
/// 导致逗号被当千位符删除,返回 "314" 而非 "3.14"。
/// 新实现改为基于分隔符位置的确定性规则,最后再用解析做一次合法性校验。
/// </summary>
public static class NumberNormalizer
{
public static bool TryNormalizeToDot(string value, out string normalized)
{
normalized = null;
if (string.IsNullOrWhiteSpace(value))
return false;

string s = value.Trim();
int lastDot = s.LastIndexOf('.');
int lastComma = s.LastIndexOf(',');

if (lastDot >= 0 && lastComma >= 0)
{
// 1) 两种分隔符并存:最后出现的那个必是小数符(1,234.56 / 1.234,56)
string candidate = lastDot > lastComma
? s.Replace(",", "") // ',' 是千位符
: s.Replace(".", "").Replace(',', '.'); // '.' 是千位符
return TryFinalize(candidate, out normalized);
}

if (lastComma >= 0)
{
// 2) 只有逗号:若整体是严格的三位分组则视为千位符(1,234 → 1234),
// 否则视为小数符(3,14 → 3.14、12,34 → 12.34)
string candidate = IsStrictThousandsGroups(s)
? s.Replace(",", "")
: s.Replace(',', '.');
return TryFinalize(candidate, out normalized);
}

// 3) 只有点或无分隔符:本身就是点风格,直接校验
// ("1.234" 按 1.234 处理;"1.2.3" 会被校验拒绝)
return TryFinalize(s, out normalized);
}

/// <summary>
/// 判断是否为严格的三位千位分组(等价于 ^[+-]?\d{{1,3}}(,\d{{3}})+$):
/// 可选正负号 + 1~3 位数字 + 一个或多个“逗号 + 恰好 3 位数字”的分组,
/// 且不允许出现任何其他字符(指数 e/E 等一律不匹配)。
/// </summary>
private static bool IsStrictThousandsGroups(string s)
{
int i = 0;
if (i < s.Length && (s[i] == '+' || s[i] == '-'))
i++;

// 首组:1~3 位数字
int first = 0;
while (i < s.Length && char.IsDigit(s[i]))
{
i++;
first++;
}
if (first < 1 || first > 3 || i >= s.Length)
return false;

// 后续组:',' + 恰好 3 位数字,直到字符串结束
while (true)
{
if (s[i] != ',')
return false;
i++;
int group = 0;
while (i < s.Length && char.IsDigit(s[i]))
{
i++;
group++;
}
if (group != 3)
return false;
if (i == s.Length)
return true;
}
}

/// <summary>最终用 Invariant 解析校验候选串是否为合法数字,通过则输出。</summary>
private static bool TryFinalize(string candidate, out string normalized)
{
normalized = null;
if (decimal.TryParse(candidate, NumberStyles.Float, CultureInfo.InvariantCulture, out _))
{
normalized = candidate;
return true;
}
return false;
}

public static bool TryGetValFromModel<T>(string numStr, out T val) where T : struct, IParsable<T>
{
val = default(T);
if (!TryNormalizeToDot(numStr, out string formateStr))
return false;
if (T.TryParse(formateStr, CultureInfo.InvariantCulture, out T _tmpLimit))
{
val = _tmpLimit;
return true;
}
return false;
}
}

测试代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

static void DoCheck<T>(string checkStr) where T : struct, IParsable<T>
{
T val, val2;
if (NumberNormalizer.TryGetValFromModel(checkStr, out val) && NumberNormalizer2.TryGetValFromModel(checkStr, out val2) && val.Equals(val2))
Console.WriteLine($"check ready: {val}");
else
Console.WriteLine($"error: {val} !!!");
}

static void Main(string[] args)
{
Console.WriteLine("Hello, World!");
DoCheck<int>("3");
DoCheck<double>("21");
DoCheck<float>("321");
DoCheck<float>("3,14");
DoCheck<double>("3.14");
DoCheck<float>("12.34");
DoCheck<float>("12,34");
DoCheck<float>("1.234");
DoCheck<float>("1,234");
DoCheck<float>("1,234.56");
DoCheck<float>("1.234,56");
DoCheck<float>("1,5e3");
DoCheck<float>("1.5e3");
DoCheck<float>("1,5e-3");
DoCheck<float>("1.5e-3");
}