Skip to content

Commit 96c903c

Browse files
committed
Implement number literal parsers and tests for various formats and cultures
1 parent c7a16db commit 96c903c

3 files changed

Lines changed: 280 additions & 7 deletions

File tree

src/Parlot/Fluent/NumberLiteralBase.cs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using Parlot.SourceGeneration;
44
using System;
55
using System.Globalization;
6+
using System.Linq;
67
using System.Linq.Expressions;
78
using System.Numerics;
89
using System.Reflection;
@@ -186,20 +187,23 @@ public SourceResult GenerateSource(SourceGenerationContext context)
186187
var allowGroupSeparator = _allowGroupSeparator ? "true" : "false";
187188
var allowExponent = _allowExponent ? "true" : "false";
188189

189-
// Emit NumberStyles as a literal cast
190-
var numberStylesExpr = $"(global::System.Globalization.NumberStyles){(int)_numberStyles}";
190+
// Emit NumberStyles as a static readonly field
191+
var numberStylesFieldName = context.RegisterStaticField(
192+
$"private static readonly global::System.Globalization.NumberStyles",
193+
$"(global::System.Globalization.NumberStyles){(int)_numberStyles}");
191194

192-
// Emit CultureInfo - use InvariantCulture if it's the default, otherwise create a clone
195+
// Emit CultureInfo - use InvariantCulture if it's the default, otherwise create a static field
193196
string cultureExpr;
194197
if (_culture == CultureInfo.InvariantCulture)
195198
{
196199
cultureExpr = "global::System.Globalization.CultureInfo.InvariantCulture";
197200
}
198201
else
199202
{
200-
// For custom cultures, we need to emit code that creates the same culture
201-
// This is a simplified approach - for complex cases, we might need to register a factory
202-
cultureExpr = "global::System.Globalization.CultureInfo.InvariantCulture";
203+
// For custom cultures, inline the culture creation using a lambda IIFE
204+
cultureExpr = context.RegisterStaticField(
205+
"private static readonly global::System.Globalization.CultureInfo",
206+
$"new global::System.Func<global::System.Globalization.CultureInfo>(() => {{ var c = (global::System.Globalization.CultureInfo)global::System.Globalization.CultureInfo.InvariantCulture.Clone(); c.NumberFormat.NumberDecimalSeparator = \"{_decimalSeparator}\"; c.NumberFormat.NumberGroupSeparator = \"{_groupSeparator}\"; return c; }})()");
203207
}
204208

205209
result.Body.Add($"if ({scannerName}.ReadDecimal({allowLeadingSign}, {allowDecimalSeparator}, {allowGroupSeparator}, {allowExponent}, out {numberSpanName}, '{_decimalSeparator}', '{_groupSeparator}'))");
@@ -211,7 +215,7 @@ public SourceResult GenerateSource(SourceGenerationContext context)
211215
else
212216
{
213217
// Use ReadOnlySpan<char> overload directly - .NET 7+ types all support TryParse(ReadOnlySpan<char>, ...)
214-
result.Body.Add($" if (global::Parlot.Numbers.TryParse({numberSpanName}, {numberStylesExpr}, {cultureExpr}, out {parsedValueName}))");
218+
result.Body.Add($" if (global::Parlot.Numbers.TryParse({numberSpanName}, {numberStylesFieldName}, {cultureExpr}, out {parsedValueName}))");
215219
result.Body.Add(" {");
216220
result.Body.Add($" {result.ValueVariable} = {parsedValueName};");
217221
result.Body.Add(" return true;");

test/Parlot.SourceGenerator.Tests/Grammars.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,4 +404,45 @@ public static Parser<char> CountingOneOfParser()
404404

405405
[GenerateParser]
406406
public static Parser<TextSpan> NoneOfWhitespaceParser() => Literals.NoneOf(" \t\r\n");
407+
408+
#region Number Literal Parsers
409+
410+
[GenerateParser]
411+
public static Parser<long> IntegerNumberLiteralParser() => Terms.Integer();
412+
413+
[GenerateParser]
414+
public static Parser<decimal> DecimalNumberLiteralParser() => Terms.Decimal();
415+
416+
[GenerateParser]
417+
public static Parser<double> DoubleNumberLiteralWithExponentParser() =>
418+
Terms.Number<double>(NumberOptions.Number | NumberOptions.AllowExponent);
419+
420+
[GenerateParser]
421+
public static Parser<decimal> DecimalNumberLiteralWithCommaSeparatorParser() =>
422+
Terms.Number<decimal>(NumberOptions.AllowLeadingSign | NumberOptions.AllowDecimalSeparator, decimalSeparator: ',');
423+
424+
[GenerateParser]
425+
public static Parser<long> IntegerNumberLiteralWithUnderscoreSeparatorParser() =>
426+
Terms.Number<long>(NumberOptions.Integer | NumberOptions.AllowGroupSeparators, groupSeparator: '_');
427+
428+
[GenerateParser]
429+
public static Parser<long> IntegerNumberLiteralNoLeadingSignParser() =>
430+
Terms.Number<long>(NumberOptions.None);
431+
432+
[GenerateParser]
433+
public static Parser<decimal> DecimalNumberLiteralNoDecimalSeparatorParser() =>
434+
Terms.Number<decimal>(NumberOptions.AllowLeadingSign);
435+
436+
[GenerateParser]
437+
public static Parser<float> FloatNumberLiteralParser() =>
438+
Terms.Number<float>(NumberOptions.Number | NumberOptions.AllowExponent);
439+
440+
[GenerateParser]
441+
public static Parser<long> LongNumberLiteralParser() => Terms.Integer(NumberOptions.Integer);
442+
443+
[GenerateParser]
444+
public static Parser<decimal> DecimalNumberLiteralCustomCultureParser() =>
445+
Terms.Number<decimal>(NumberOptions.Number | NumberOptions.AllowGroupSeparators, decimalSeparator: ',', groupSeparator: '_');
446+
447+
#endregion
407448
}
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
#nullable enable
2+
3+
using System.Globalization;
4+
using Parlot;
5+
using Parlot.Fluent;
6+
using Xunit;
7+
using static Parlot.Fluent.Parsers;
8+
9+
namespace Parlot.SourceGenerator.Tests;
10+
11+
/// <summary>
12+
/// Tests for source-generated number literal parsers with various cultures and number styles.
13+
/// </summary>
14+
public class NumberLiteralTests
15+
{
16+
#region Integer Number Literals
17+
18+
[Theory]
19+
[InlineData("123", true, 123L)]
20+
[InlineData(" 456", true, 456L)] // Terms skip whitespace
21+
[InlineData("-789", true, -789L)]
22+
[InlineData("+42", true, 42L)]
23+
[InlineData("0", true, 0L)]
24+
[InlineData("abc", false, 0L)]
25+
[InlineData("", false, 0L)]
26+
public void IntegerNumberLiteral_VariousInputs(string input, bool shouldSucceed, long expected)
27+
{
28+
var parser = Grammars.IntegerNumberLiteralParser();
29+
var result = parser.TryParse(input, out var value);
30+
31+
Assert.Equal(shouldSucceed, result);
32+
if (shouldSucceed)
33+
{
34+
Assert.Equal(expected, value);
35+
}
36+
}
37+
38+
#endregion
39+
40+
#region Decimal Number Literals
41+
42+
[Theory]
43+
[InlineData("123.45", true, 123.45)]
44+
[InlineData(" 678.90", true, 678.90)]
45+
[InlineData("-12.34", true, -12.34)]
46+
[InlineData("+56.78", true, 56.78)]
47+
[InlineData("0.0", true, 0.0)]
48+
[InlineData("123", true, 123.0)]
49+
[InlineData("abc", false, 0.0)]
50+
[InlineData("", false, 0.0)]
51+
public void DecimalNumberLiteral_VariousInputs(string input, bool shouldSucceed, decimal expected)
52+
{
53+
var parser = Grammars.DecimalNumberLiteralParser();
54+
var result = parser.TryParse(input, out var value);
55+
56+
Assert.Equal(shouldSucceed, result);
57+
if (shouldSucceed)
58+
{
59+
Assert.Equal(expected, value);
60+
}
61+
}
62+
63+
#endregion
64+
65+
#region Double Number Literals
66+
67+
[Theory]
68+
[InlineData("123.45", true, 123.45)]
69+
[InlineData("1.23e2", true, 123.0)]
70+
[InlineData("1.23E+2", true, 123.0)]
71+
[InlineData("1.23e-2", true, 0.0123)]
72+
[InlineData("-1.5e3", true, -1500.0)]
73+
[InlineData(" 456.78", true, 456.78)]
74+
public void DoubleNumberLiteral_WithExponent(string input, bool shouldSucceed, double expected)
75+
{
76+
var parser = Grammars.DoubleNumberLiteralWithExponentParser();
77+
var result = parser.TryParse(input, out var value);
78+
79+
Assert.Equal(shouldSucceed, result);
80+
if (shouldSucceed)
81+
{
82+
Assert.Equal(expected, value, 5); // 5 decimal places precision
83+
}
84+
}
85+
86+
#endregion
87+
88+
#region Custom Decimal Separator
89+
90+
[Theory]
91+
[InlineData("123,45", true, 123.45)]
92+
[InlineData(" 678,90", true, 678.90)]
93+
[InlineData("-12,34", true, -12.34)]
94+
[InlineData("123", true, 123.0)]
95+
[InlineData("abc", false, 0.0)]
96+
public void DecimalNumberLiteral_CustomDecimalSeparator(string input, bool shouldSucceed, decimal expected)
97+
{
98+
var parser = Grammars.DecimalNumberLiteralWithCommaSeparatorParser();
99+
var result = parser.TryParse(input, out var value);
100+
101+
Assert.Equal(shouldSucceed, result);
102+
if (shouldSucceed)
103+
{
104+
Assert.Equal(expected, value);
105+
}
106+
}
107+
108+
#endregion
109+
110+
#region Custom Group Separator
111+
112+
[Theory]
113+
[InlineData("1_234", true, 1234L)]
114+
[InlineData("1_234_567", true, 1234567L)]
115+
[InlineData(" 999_999", true, 999999L)]
116+
[InlineData("123", true, 123L)]
117+
[InlineData("abc", false, 0L)]
118+
public void IntegerNumberLiteral_CustomGroupSeparator(string input, bool shouldSucceed, long expected)
119+
{
120+
var parser = Grammars.IntegerNumberLiteralWithUnderscoreSeparatorParser();
121+
var result = parser.TryParse(input, out var value);
122+
123+
Assert.Equal(shouldSucceed, result);
124+
if (shouldSucceed)
125+
{
126+
Assert.Equal(expected, value);
127+
}
128+
}
129+
130+
#endregion
131+
132+
#region NumberOptions Tests
133+
134+
[Theory]
135+
[InlineData("123", true, 123L)]
136+
[InlineData(" 456", true, 456L)]
137+
[InlineData("-789", false, 0L)] // Leading sign not allowed
138+
[InlineData("+42", false, 0L)] // Leading sign not allowed
139+
public void IntegerNumberLiteral_NoLeadingSign(string input, bool shouldSucceed, long expected)
140+
{
141+
var parser = Grammars.IntegerNumberLiteralNoLeadingSignParser();
142+
var result = parser.TryParse(input, out var value);
143+
144+
Assert.Equal(shouldSucceed, result);
145+
if (shouldSucceed)
146+
{
147+
Assert.Equal(expected, value);
148+
}
149+
}
150+
151+
[Theory]
152+
[InlineData("123", true, 123.0)]
153+
[InlineData("456", true, 456.0)]
154+
[InlineData("-789", true, -789.0)]
155+
[InlineData("abc", false, 0.0)]
156+
public void DecimalNumberLiteral_NoDecimalSeparator(string input, bool shouldSucceed, decimal expected)
157+
{
158+
var parser = Grammars.DecimalNumberLiteralNoDecimalSeparatorParser();
159+
var result = parser.TryParse(input, out var value);
160+
161+
Assert.Equal(shouldSucceed, result);
162+
if (shouldSucceed)
163+
{
164+
Assert.Equal(expected, value);
165+
}
166+
}
167+
168+
#endregion
169+
170+
#region Float and Long Number Literals
171+
172+
[Theory]
173+
[InlineData("123.45", true, 123.45f)]
174+
[InlineData("1.23e2", true, 123.0f)]
175+
[InlineData("-45.67", true, -45.67f)]
176+
public void FloatNumberLiteral_VariousInputs(string input, bool shouldSucceed, float expected)
177+
{
178+
var parser = Grammars.FloatNumberLiteralParser();
179+
var result = parser.TryParse(input, out var value);
180+
181+
Assert.Equal(shouldSucceed, result);
182+
if (shouldSucceed)
183+
{
184+
Assert.Equal(expected, value, 5);
185+
}
186+
}
187+
188+
[Theory]
189+
[InlineData("9223372036854775807", true, 9223372036854775807)]
190+
[InlineData("-9223372036854775808", true, -9223372036854775808)]
191+
[InlineData(" 12345", true, 12345L)]
192+
[InlineData("0", true, 0L)]
193+
public void LongNumberLiteral_VariousInputs(string input, bool shouldSucceed, long expected)
194+
{
195+
var parser = Grammars.LongNumberLiteralParser();
196+
var result = parser.TryParse(input, out var value);
197+
198+
Assert.Equal(shouldSucceed, result);
199+
if (shouldSucceed)
200+
{
201+
Assert.Equal(expected, value);
202+
}
203+
}
204+
205+
#endregion
206+
207+
#region Combined Custom Culture Tests
208+
209+
[Theory]
210+
[InlineData("1_234,56", true, 1234.56)]
211+
[InlineData(" 9_999,99", true, 9999.99)]
212+
[InlineData("-1_234,56", true, -1234.56)]
213+
[InlineData("123", true, 123.0)]
214+
[InlineData("abc", false, 0.0)]
215+
public void DecimalNumberLiteral_CustomCulture(string input, bool shouldSucceed, decimal expected)
216+
{
217+
var parser = Grammars.DecimalNumberLiteralCustomCultureParser();
218+
var result = parser.TryParse(input, out var value);
219+
220+
Assert.Equal(shouldSucceed, result);
221+
if (shouldSucceed)
222+
{
223+
Assert.Equal(expected, value);
224+
}
225+
}
226+
227+
#endregion
228+
}

0 commit comments

Comments
 (0)