Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions src/Argon/JsonReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,25 @@ JsonContainerType Peek() =>
throw JsonReaderException.Create(this, $"Could not convert string to integer: {s}.");
}

internal int? ReadInt32String(ReadOnlySpan<char> s)
{
if (s.IsEmpty)
{
SetNullToken();
return null;
}

if (int.TryParse(s, NumberStyles.Integer, InvariantCulture, out var i))
{
SetToken(i);
return i;
}

var text = s.ToString();
SetToken(text);
throw JsonReaderException.Create(this, $"Could not convert string to integer: {text}.");
}

/// <summary>
/// Reads the next JSON token from the source as a <see cref="String" />.
/// </summary>
Expand Down Expand Up @@ -558,6 +577,25 @@ bool ReadArrayElementIntoByteArrayReportDone(List<byte> buffer)
throw JsonReaderException.Create(this, $"Error reading double. Unexpected token: {token}.");
}

internal double? ReadDoubleString(ReadOnlySpan<char> s)
{
if (s.IsEmpty)
{
SetNullToken();
return null;
}

if (double.TryParse(s, NumberStyles.Float | NumberStyles.AllowThousands, InvariantCulture, out var d))
{
SetToken(d);
return d;
}

var text = s.ToString();
SetToken(text);
throw JsonReaderException.Create(this, $"Could not convert string to double: {text}.");
}

internal double? ReadDoubleString(string? s)
{
if (s.IsNullOrEmpty())
Expand Down Expand Up @@ -631,6 +669,32 @@ bool ReadArrayElementIntoByteArrayReportDone(List<byte> buffer)
throw JsonReaderException.Create(this, $"Could not convert string to boolean: {s}.");
}

internal bool? ReadBooleanString(ReadOnlySpan<char> s)
{
if (s.IsEmpty)
{
SetNullToken();
return null;
}

var trimmed = s.Trim();
if (trimmed.Equals("true".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
SetToken(true);
return true;
}

if (trimmed.Equals("false".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
SetToken(false);
return false;
}

var text = s.ToString();
SetToken(text);
throw JsonReaderException.Create(this, $"Could not convert string to boolean: {text}.");
}

/// <summary>
/// Reads the next JSON token from the source as a <see cref="Nullable{T}" /> of <see cref="Decimal" />.
/// </summary>
Expand Down Expand Up @@ -704,6 +768,33 @@ bool ReadArrayElementIntoByteArrayReportDone(List<byte> buffer)
throw JsonReaderException.Create(this, $"Could not convert string to decimal: {s}.");
}

internal decimal? ReadDecimalString(ReadOnlySpan<char> s)
{
if (s.IsEmpty)
{
SetNullToken();
return null;
}

if (decimal.TryParse(s, NumberStyles.Number, InvariantCulture, out var d))
{
SetToken(d);
return d;
}

// fallback handles strings like "96.014e-05" not supported by decimal.TryParse
var chars = s.ToArray();
if (ConvertUtils.DecimalTryParse(chars, 0, chars.Length, out d) == ParseResult.Success)
{
SetToken(d);
return d;
}

var text = s.ToString();
SetToken(text);
throw JsonReaderException.Create(this, $"Could not convert string to decimal: {text}.");
}

/// <summary>
/// Reads the next JSON token from the source as a <see cref="Nullable{T}" /> of <see cref="DateTime" />.
/// </summary>
Expand Down
8 changes: 4 additions & 4 deletions src/Argon/JsonTextReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ JsonReaderException CreateUnexpectedCharacterException(char c) =>
case '"':
case '\'':
ParseString(currentChar, ReadType.Read);
return ReadBooleanString(stringReference.ToString());
return ReadBooleanString(stringReference.AsSpan());
case 'n':
HandleNull();
return null;
Expand Down Expand Up @@ -864,11 +864,11 @@ void ProcessValueComma()
switch (readType)
{
case ReadType.ReadAsInt32:
return ReadInt32String(stringReference.ToString());
return ReadInt32String(stringReference.AsSpan());
case ReadType.ReadAsDecimal:
return ReadDecimalString(stringReference.ToString());
return ReadDecimalString(stringReference.AsSpan());
case ReadType.ReadAsDouble:
return ReadDoubleString(stringReference.ToString());
return ReadDoubleString(stringReference.AsSpan());
default:
throw new ArgumentOutOfRangeException(nameof(readType));
}
Expand Down
3 changes: 3 additions & 0 deletions src/Argon/Utilities/StringReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ readonly struct StringReference(char[] chars, int startIndex, int length)

public int Length { get; } = length;

public ReadOnlySpan<char> AsSpan() =>
Chars.AsSpan(StartIndex, Length);

public override string ToString() =>
new(Chars, StartIndex, Length);
}
74 changes: 74 additions & 0 deletions src/ArgonTests/Benchmarks/ReadQuotedNumbers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) 2007 James Newton-King. All rights reserved.
// Use of this source code is governed by The MIT License,
// as found in the license.md file.

using BenchmarkDotNet.Attributes;

[MemoryDiagnoser]
public class ReadQuotedNumbers
{
string json = null!;

[GlobalSetup]
public void Setup()
{
var builder = new StringBuilder("[");
for (var i = 0; i < 500; i++)
{
if (i > 0)
{
builder.Append(',');
}

builder.Append('"').Append(i * 12345).Append('"');
}

builder.Append(']');
json = builder.ToString();
}

[Benchmark]
public long ReadAsInt32()
{
using var stringReader = new StringReader(json);
using var reader = new JsonTextReader(stringReader);
long sum = 0;
reader.Read();
while (reader.ReadAsInt32() is { } value)
{
sum += value;
}

return sum;
}

[Benchmark]
public decimal ReadAsDecimal()
{
using var stringReader = new StringReader(json);
using var reader = new JsonTextReader(stringReader);
decimal sum = 0;
reader.Read();
while (reader.ReadAsDecimal() is { } value)
{
sum += value;
}

return sum;
}

[Benchmark]
public double ReadAsDouble()
{
using var stringReader = new StringReader(json);
using var reader = new JsonTextReader(stringReader);
double sum = 0;
reader.Read();
while (reader.ReadAsDouble() is { } value)
{
sum += value;
}

return sum;
}
}
15 changes: 15 additions & 0 deletions src/ArgonTests/JsonTextReaderTests/ReadTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,21 @@ public void ReadAsDouble_Null()
Assert.Null(reader.ReadAsDouble());
}

[Fact]
public void ReadAsInt32_QuotedNumber_Success()
{
var reader = new JsonTextReader(new StringReader("'42'"));
Assert.Equal(42, reader.ReadAsInt32());
}

[Fact]
public void ReadAsDecimal_QuotedNumber_Failure()
{
var reader = new JsonTextReader(new StringReader("'blah'"));
var exception = Assert.Throws<JsonReaderException>(() => reader.ReadAsDecimal());
Assert.Equal("Could not convert string to decimal: blah. Path '', line 1, position 6.", exception.Message);
}

[Fact]
public void ReadAsDouble_Success()
{
Expand Down
2 changes: 1 addition & 1 deletion src/Benchmark.Tests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public static void Main(string[] args)
var attribute = (AssemblyFileVersionAttribute)typeof(JsonConvert).Assembly.GetCustomAttribute(typeof(AssemblyFileVersionAttribute))!;
Console.WriteLine($"Json.NET Version: {attribute.Version}");

var switcher = new BenchmarkSwitcher([typeof(WriteEscapedJavaScriptString), typeof(SerializeJTokenList)]);
var switcher = new BenchmarkSwitcher([typeof(WriteEscapedJavaScriptString), typeof(SerializeJTokenList), typeof(ReadQuotedNumbers)]);
if (args.Length == 0)
{
switcher.Run(["*"]);
Expand Down
Loading