@@ -11,6 +11,7 @@ namespace NeoReports.Core.Configuration;
1111/// <c>{"var": "Name"}</c> reads a column by name; the supported operators are <c>==</c>, <c>===</c>,
1212/// <c>!=</c>, <c>!==</c>, <c>></c>, <c>>=</c>, <c><</c>, <c><=</c>, <c>and</c>, <c>or</c>,
1313/// <c>!</c>, <c>!!</c> and <c>in</c>. Unknown operators raise a <see cref="ConfigurationException"/>.
14+ /// Numeric comparisons use <see cref="decimal"/> for exact equality on ids and money.
1415/// </summary>
1516public static class JsonLogicFilter
1617{
@@ -26,15 +27,15 @@ public static Func<ReportRecord, bool> Compile(string expression)
2627 JsonElement root ;
2728 try
2829 {
29- using var document = JsonDocument . Parse ( expression ) ;
30+ using JsonDocument document = JsonDocument . Parse ( expression ) ;
3031 root = document . RootElement . Clone ( ) ;
3132 }
3233 catch ( JsonException ex )
3334 {
3435 throw new ConfigurationException ( $ "Invalid JsonLogic filter: { ex . Message } ", ex ) ;
3536 }
3637
37- var evaluate = Build ( root ) ;
38+ Func < ReportRecord , object ? > evaluate = Build ( root ) ;
3839 return record => IsTruthy ( evaluate ( record ) ) ;
3940 }
4041
@@ -45,14 +46,16 @@ public static Func<ReportRecord, bool> Compile(string expression)
4546 case JsonValueKind . Object :
4647 return BuildOperation ( node ) ;
4748 case JsonValueKind . Array :
48- var items = node . EnumerateArray ( ) . Select ( Build ) . ToArray ( ) ;
49+ Func < ReportRecord , object ? > [ ] items = node . EnumerateArray ( ) . Select ( Build ) . ToArray ( ) ;
4950 return record => items . Select ( f => f ( record ) ) . ToList ( ) ;
5051 case JsonValueKind . String :
51- var text = node . GetString ( ) ;
52+ string ? text = node . GetString ( ) ;
5253 return _ => text ;
5354 case JsonValueKind . Number :
54- var number = node . TryGetInt64 ( out var l ) ? l : node . GetDouble ( ) ;
55- return _ => number ;
55+ if ( node . TryGetInt64 ( out long longValue ) )
56+ return _ => longValue ;
57+ double doubleValue = node . GetDouble ( ) ;
58+ return _ => doubleValue ;
5659 case JsonValueKind . True :
5760 return _ => true ;
5861 case JsonValueKind . False :
@@ -65,20 +68,20 @@ public static Func<ReportRecord, bool> Compile(string expression)
6568 private static Func < ReportRecord , object ? > BuildOperation ( JsonElement node )
6669 {
6770 // A JsonLogic operation is a single-key object: { "op": args }.
68- var properties = node . EnumerateObject ( ) . ToArray ( ) ;
71+ JsonProperty [ ] properties = node . EnumerateObject ( ) . ToArray ( ) ;
6972 if ( properties . Length != 1 )
7073 throw new ConfigurationException ( "A JsonLogic operation must be an object with exactly one operator key." ) ;
7174
72- var op = properties [ 0 ] . Name ;
73- var value = properties [ 0 ] . Value ;
74- var args = value . ValueKind == JsonValueKind . Array
75+ string op = properties [ 0 ] . Name ;
76+ JsonElement value = properties [ 0 ] . Value ;
77+ Func < ReportRecord , object ? > [ ] args = value . ValueKind == JsonValueKind . Array
7578 ? value . EnumerateArray ( ) . Select ( Build ) . ToArray ( )
7679 : new [ ] { Build ( value ) } ;
7780
7881 return op switch
7982 {
8083 "var" => BuildVar ( value ) ,
81- "==" => Binary ( args , op , ( a , b ) => LooseEquals ( a , b ) ) ,
84+ "==" => Binary ( args , op , LooseEquals ) ,
8285 "!=" => Binary ( args , op , ( a , b ) => ! LooseEquals ( a , b ) ) ,
8386 "===" => Binary ( args , op , StrictEquals ) ,
8487 "!==" => Binary ( args , op , ( a , b ) => ! StrictEquals ( a , b ) ) ,
@@ -100,11 +103,11 @@ public static Func<ReportRecord, bool> Compile(string expression)
100103 // {"var": "Name"} or {"var": ["Name", default]}.
101104 string ? name ;
102105 JsonElement defaultElement = default ;
103- var hasDefault = false ;
106+ bool hasDefault = false ;
104107
105108 if ( value . ValueKind == JsonValueKind . Array )
106109 {
107- var parts = value . EnumerateArray ( ) . ToArray ( ) ;
110+ JsonElement [ ] parts = value . EnumerateArray ( ) . ToArray ( ) ;
108111 name = parts . Length > 0 ? parts [ 0 ] . GetString ( ) : null ;
109112 if ( parts . Length > 1 )
110113 {
@@ -120,20 +123,20 @@ public static Func<ReportRecord, bool> Compile(string expression)
120123 if ( string . IsNullOrEmpty ( name ) )
121124 throw new ConfigurationException ( "A JsonLogic 'var' requires a column name." ) ;
122125
123- var column = name ;
124- var fallback = hasDefault ? Build ( defaultElement ) : null ;
125- return record => record . TryGet ( column , out var v ) ? v : fallback ? . Invoke ( record ) ;
126+ string column = name ;
127+ Func < ReportRecord , object ? > ? fallback = hasDefault ? Build ( defaultElement ) : null ;
128+ return record => record . TryGet ( column , out object ? v ) ? v : fallback ? . Invoke ( record ) ;
126129 }
127130
128131 private static Func < ReportRecord , object ? > BuildIn ( Func < ReportRecord , object ? > [ ] args , string op )
129132 {
130133 Require ( args , 2 , op ) ;
131- var needle = args [ 0 ] ;
132- var haystack = args [ 1 ] ;
134+ Func < ReportRecord , object ? > needle = args [ 0 ] ;
135+ Func < ReportRecord , object ? > haystack = args [ 1 ] ;
133136 return record =>
134137 {
135- var n = needle ( record ) ;
136- var h = haystack ( record ) ;
138+ object ? n = needle ( record ) ;
139+ object ? h = haystack ( record ) ;
137140 if ( h is string s )
138141 return n is not null && s . Contains ( Stringify ( n ) , StringComparison . Ordinal ) ;
139142 if ( h is IEnumerable enumerable and not string )
@@ -146,15 +149,15 @@ public static Func<ReportRecord, bool> Compile(string expression)
146149 Func < ReportRecord , object ? > [ ] args , string op , Func < object ? , object ? , bool > predicate )
147150 {
148151 Require ( args , 2 , op ) ;
149- var left = args [ 0 ] ;
150- var right = args [ 1 ] ;
152+ Func < ReportRecord , object ? > left = args [ 0 ] ;
153+ Func < ReportRecord , object ? > right = args [ 1 ] ;
151154 return record => predicate ( left ( record ) , right ( record ) ) ;
152155 }
153156
154157 private static object EvaluateAnd ( Func < ReportRecord , object ? > [ ] args , ReportRecord record )
155158 {
156159 object ? last = true ;
157- foreach ( var arg in args )
160+ foreach ( Func < ReportRecord , object ? > arg in args )
158161 {
159162 last = arg ( record ) ;
160163 if ( ! IsTruthy ( last ) )
@@ -167,7 +170,7 @@ private static object EvaluateAnd(Func<ReportRecord, object?>[] args, ReportReco
167170 private static object EvaluateOr ( Func < ReportRecord , object ? > [ ] args , ReportRecord record )
168171 {
169172 object ? last = false ;
170- foreach ( var arg in args )
173+ foreach ( Func < ReportRecord , object ? > arg in args )
171174 {
172175 last = arg ( record ) ;
173176 if ( IsTruthy ( last ) )
@@ -190,24 +193,24 @@ private static bool LooseEquals(object? a, object? b)
190193 {
191194 if ( a is null || b is null )
192195 return a is null && b is null ;
193- if ( TryToDouble ( a , out var da ) && TryToDouble ( b , out var db ) )
194- return da . Equals ( db ) ;
196+ if ( TryToNumber ( a , out decimal na ) && TryToNumber ( b , out decimal nb ) )
197+ return na == nb ;
195198 return string . Equals ( Stringify ( a ) , Stringify ( b ) , StringComparison . Ordinal ) ;
196199 }
197200
198201 private static bool StrictEquals ( object ? a , object ? b )
199202 {
200203 if ( a is null || b is null )
201204 return a is null && b is null ;
202- if ( TryToDouble ( a , out var da ) && TryToDouble ( b , out var db ) )
203- return da . Equals ( db ) ;
205+ if ( TryToNumber ( a , out decimal na ) && TryToNumber ( b , out decimal nb ) )
206+ return na == nb ;
204207 return a . GetType ( ) == b . GetType ( ) && a . Equals ( b ) ;
205208 }
206209
207210 private static int Compare ( object ? a , object ? b )
208211 {
209- if ( TryToDouble ( a , out var da ) && TryToDouble ( b , out var db ) )
210- return da . CompareTo ( db ) ;
212+ if ( TryToNumber ( a , out decimal na ) && TryToNumber ( b , out decimal nb ) )
213+ return na . CompareTo ( nb ) ;
211214 if ( a is DateTime dta && b is DateTime dtb )
212215 return dta . CompareTo ( dtb ) ;
213216 return string . CompareOrdinal ( Stringify ( a ) , Stringify ( b ) ) ;
@@ -219,18 +222,19 @@ private static int Compare(object? a, object? b)
219222 bool b => b ,
220223 string s => s . Length > 0 ,
221224 IEnumerable enumerable => enumerable . Cast < object ? > ( ) . Any ( ) ,
222- _ => ! TryToDouble ( value , out var d ) || d != 0 ,
225+ _ => ! TryToNumber ( value , out decimal n ) || n != decimal . Zero ,
223226 } ;
224227
225- private static bool TryToDouble ( object ? value , out double result )
228+ // Numbers are compared as decimal so ids and money compare exactly (no floating-point equality).
229+ private static bool TryToNumber ( object ? value , out decimal result )
226230 {
227231 switch ( value )
228232 {
229233 case sbyte or byte or short or ushort or int or uint or long or ulong or float or double or decimal :
230- result = Convert . ToDouble ( value , CultureInfo . InvariantCulture ) ;
234+ result = Convert . ToDecimal ( value , CultureInfo . InvariantCulture ) ;
231235 return true ;
232236 default :
233- result = 0 ;
237+ result = decimal . Zero ;
234238 return false ;
235239 }
236240 }
0 commit comments