@@ -243,10 +243,11 @@ internal bool AnyNull()
243243
244244 private static readonly int MaxCanonicalLength = Math . Max ( Format . MaxInt64TextLen , Format . MaxDoubleTextLen ) ;
245245
246- // Recognizes the canonical decimal text of an integer or finite double, returning a numeric-backed
247- // RedisValue only when re-formatting the parsed value reproduces the exact input bytes. This keeps the
248- // optimization invisible to callers: non-canonical spellings ("01234", "+5", "1.50", "1e3"), oversize
249- // values, and the special inf/nan tokens all return false and are kept as a byte[] payload by the caller.
246+ // Recognizes the canonical decimal text of an integer (signed Int64 or, for non-negative values up to
247+ // ulong.MaxValue, UInt64) or a finite double, returning a numeric-backed RedisValue only when re-formatting
248+ // the parsed value reproduces the exact input bytes. This keeps the optimization invisible to callers:
249+ // non-canonical spellings ("01234", "+5", "1.50", "1e3"), values beyond the ulong/double range, and the
250+ // special inf/nan tokens all return false and are kept as a byte[] payload by the caller.
250251 private static bool TryReadCanonicalNumber ( ReadOnlySpan < byte > span , out RedisValue value )
251252 {
252253 static bool Failure ( out RedisValue value )
@@ -258,7 +259,10 @@ static bool Failure(out RedisValue value)
258259 // leading '+', "-0", trailing junk, etc.) - so no need to re-emit and compare bytes
259260 if ( span . IsEmpty | span . Length > MaxCanonicalLength ) return Failure ( out value ) ;
260261
261- // restrict to *just* basic number tokens
262+ // restrict to *just* basic number tokens, tracking the two facts that let us pick a single parse:
263+ // whether a '-' appeared (so we know whether the integer is signed), and whether a '.'/'e'/'E'
264+ // appeared (which rules out an integer entirely - only a double can be canonical)
265+ bool seenNegative = false , seenDotOrExp = false ;
262266 foreach ( var b in span )
263267 {
264268 switch ( b )
@@ -273,23 +277,44 @@ static bool Failure(out RedisValue value)
273277 case ( byte ) '7' :
274278 case ( byte ) '8' :
275279 case ( byte ) '9' :
276- case ( byte ) '.' :
280+ break ;
277281 case ( byte ) '-' :
282+ seenNegative = true ;
283+ break ;
284+ case ( byte ) '.' :
278285 case ( byte ) 'E' :
279286 case ( byte ) 'e' :
287+ seenDotOrExp = true ;
280288 break ;
281289 default :
282290 return Failure ( out value ) ;
283291 }
284292 }
285293
286- if ( Format . TryParseInt64 ( span , out var i64 ) && Format . MeasureInt64 ( i64 ) == span . Length )
294+ if ( ! seenDotOrExp )
287295 {
288- value = i64 ;
289- return true ;
290- }
296+ // pure integer text. For a non-negative value parse as *unsigned* so the full ulong range is
297+ // covered; the RedisValue(ulong) ctor demotes to Int64 storage when the value fits, so smaller
298+ // values still land as Int64 exactly as before. A negative value can only be Int64.
299+ if ( seenNegative )
300+ {
301+ if ( Format . TryParseInt64 ( span , out var i64 ) && Format . MeasureInt64 ( i64 ) == span . Length )
302+ {
303+ value = i64 ;
304+ return true ;
305+ }
306+ }
307+ else if ( Format . TryParseUInt64 ( span , out var u64 ) && Format . MeasureUInt64 ( u64 ) == span . Length )
308+ {
309+ value = u64 ;
310+ return true ;
311+ }
291312
292- if ( Format . TryParseDouble ( span , out var dbl ) )
313+ // note that all-digit text which isn't a canonical integer (oversize, leading zero, etc.) can't
314+ // be a canonical double either - any such value re-formats with an exponent - so we simply fall
315+ // through to the failure at the bottom rather than attempting a (futile) double parse
316+ }
317+ else if ( Format . TryParseDouble ( span , out var dbl ) )
293318 {
294319 if ( dbl == 0.0 ) dbl = Math . Abs ( dbl ) ; // prevent problems with -0 formatting
295320 Span < byte > formatted = stackalloc byte [ Format . MaxDoubleTextLen ] ;
0 commit comments