Skip to content

Commit 601220a

Browse files
committed
tidying (support ulong as canonical)
1 parent 0815125 commit 601220a

5 files changed

Lines changed: 63 additions & 17 deletions

File tree

src/StackExchange.Redis/RedisValue.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1498,7 +1498,6 @@ public bool TryParse(out long val)
14981498
return Format.TryParseInt64(RawString(), out val);
14991499
case StorageType.MemoryManager or StorageType.ByteArray or StorageType.ShortBlob:
15001500
return Format.TryParseInt64(UnsafeRawSpan(out _), out val);
1501-
15021501
case StorageType.Sequence:
15031502
// longer than the largest possible Int64 text => cannot be an Int64; otherwise
15041503
// linearize onto the stack and reuse the span-based parse (matching the ByteArray path)

src/StackExchange.Redis/RespReaderExtensions.cs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -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];

tests/StackExchange.Redis.Tests/Issues/Issue1103Tests.cs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@ public class Issue1103Tests(ITestOutputHelper output) : TestBase(output)
99
{
1010
[Theory]
1111
[InlineData(142205255210238005UL, (int)StorageType.Int64, (int)StorageType.Int64)]
12-
[InlineData(ulong.MaxValue, (int)StorageType.UInt64)]
12+
[InlineData(ulong.MaxValue, (int)StorageType.UInt64, (int)StorageType.UInt64)] // 20-byte canonical uint => UInt64 on read
1313
[InlineData(ulong.MinValue, (int)StorageType.Int64, (int)StorageType.ShortBlob)]
14-
[InlineData(0x8000000000000000UL, (int)StorageType.UInt64)]
15-
[InlineData(0x8000000000000001UL, (int)StorageType.UInt64)]
14+
[InlineData(0x8000000000000000UL, (int)StorageType.UInt64, (int)StorageType.UInt64)] // long.MaxValue+1: 19-byte canonical uint => UInt64 on read
15+
[InlineData(0x8000000000000001UL, (int)StorageType.UInt64, (int)StorageType.UInt64)]
1616
[InlineData(0x7FFFFFFFFFFFFFFFUL, (int)StorageType.Int64, (int)StorageType.Int64)] // long.MaxValue: 19-byte canonical int => Int64 on read
17-
public async Task LargeUInt64StoredCorrectly(ulong value, int storageType, int fromRedisType = (int)StorageType.ByteArray)
17+
public async Task LargeUInt64StoredCorrectly(ulong value, int storageType, int fromRedisType)
1818
{
1919
await using var conn = Create();
2020

@@ -35,7 +35,7 @@ public async Task LargeUInt64StoredCorrectly(ulong value, int storageType, int f
3535

3636
var simplified = fromRedis.Simplify();
3737
Log($"{simplified.Type}: {simplified}");
38-
Assert.Equal((StorageType)storageType, typed.Type);
38+
Assert.Equal((StorageType)storageType, simplified.Type);
3939
Assert.Equal(value, (ulong)simplified);
4040
Assert.Equal(value.ToString(CultureInfo.InvariantCulture), fromRedis.ToString());
4141
}

tests/StackExchange.Redis.Tests/RedisValueShortBlobTests.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,25 @@ public void ShortBlob_StartsWith_MatchesByteArray()
112112
Assert.False(shortBlob.StartsWith("abcdef"u8.ToArray()));
113113
}
114114

115+
[Fact]
116+
public void ShortBlob_CompareTo_Sequence_NonEqualOrdering()
117+
{
118+
// the equal-content tests only assert CompareTo == 0; this pins the *non-zero* cross-kind branches
119+
// of BlobCompareTo (contiguous ShortBlob vs multi-segment Sequence), in both directions.
120+
var abc = Short("abc"u8.ToArray());
121+
var abd = Sequence("abd"u8.ToArray());
122+
123+
// differing content: "abc" < "abd"
124+
Assert.True(abc.CompareTo(abd) < 0); // ShortBlob (x) vs Sequence (y) - the ySeq branch
125+
Assert.True(abd.CompareTo(abc) > 0); // Sequence (x) vs ShortBlob (y) - the negated xSeq branch
126+
127+
// length mismatch: "ab" is a prefix of "abc", so the shorter value sorts first
128+
var ab = Short("ab"u8.ToArray());
129+
var abcSeq = Sequence("abc"u8.ToArray());
130+
Assert.True(ab.CompareTo(abcSeq) < 0);
131+
Assert.True(abcSeq.CompareTo(ab) > 0);
132+
}
133+
115134
[Fact]
116135
public void FromRaw_RoutesByLength()
117136
{

tests/StackExchange.Redis.Tests/RedisValueStorageKindUnitTests.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ public class RedisValueStorageKindUnitTests(ITestOutputHelper output, InProcServ
4242
// > 8 bytes AND a canonical number => compact Int64/Double (also avoids the byte[] alloc)
4343
[InlineData("123456789", "Int64")] // 9 bytes
4444
[InlineData("9223372036854775807", "Int64")] // long.MaxValue (19 bytes)
45+
// canonical, non-negative, and > long.MaxValue => UInt64 (covers the full ulong range on read)
46+
[InlineData("9223372036854775808", "UInt64")] // long.MaxValue + 1 (19 bytes)
47+
[InlineData("18446744073709551615", "UInt64")] // ulong.MaxValue (20 bytes)
4548
[InlineData("1048576.5", "Double")] // 9 bytes, exactly representable => canonical under G17
4649
[InlineData("-1048576.25", "Double")] // 11 bytes
4750
// > 8 bytes and not a canonical number => materialized as a byte[]

0 commit comments

Comments
 (0)