From ab31c409d6a6b1bd0f36ff68d74d9ca77d9034b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mike=20Kr=C3=BCger?= Date: Mon, 5 Oct 2026 09:12:17 +0200 Subject: [PATCH] Use double consistently for JSON decimal conversion Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 4 ++++ .../Parser/CultureInvariantConversionTests.cs | 16 ++++++++++++++++ .../Parser/ShellObjectConversionTests.cs | 15 ++++++++++----- .../Parser/StatementExecutionTests.cs | 8 ++++++++ .../ShellObject/ShellJson.cs | 4 ++-- README.md | 2 ++ docs/programming.md | 2 ++ 7 files changed, 44 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 760da1d1..8551683f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Fixes + +- Use consistent `double` conversion for JSON values in decimal arithmetic and comparisons, including numeric strings read through JSON paths. + ## 1.1.271-preview — 2026-10-02 ### New features diff --git a/CosmosDBShell.Tests/Parser/CultureInvariantConversionTests.cs b/CosmosDBShell.Tests/Parser/CultureInvariantConversionTests.cs index 1e7c05d8..f456ea23 100644 --- a/CosmosDBShell.Tests/Parser/CultureInvariantConversionTests.cs +++ b/CosmosDBShell.Tests/Parser/CultureInvariantConversionTests.cs @@ -51,6 +51,22 @@ public async Task ExpressionEvaluation_TextDecimalArithmetic_UsesInvariantCultur Assert.Equal(3.0d, value.Value); } + [Fact] + public void ShellJson_DecimalConversion_UsesInvariantCulture() + { + WithGermanCulture(() => + { + using var document = System.Text.Json.JsonDocument.Parse("\"1.5\""); + var value = new ShellJson(document.RootElement); + Assert.Equal(1.5d, Assert.IsType(value.ConvertShellObject(DataType.Decimal))); + + using var localized = System.Text.Json.JsonDocument.Parse("\"1,5\""); + Assert.Throws(() => + new ShellJson(localized.RootElement).ConvertShellObject(DataType.Decimal)); + return true; + }); + } + private static T WithGermanCulture(Func action) { var originalCulture = CultureInfo.CurrentCulture; diff --git a/CosmosDBShell.Tests/Parser/ShellObjectConversionTests.cs b/CosmosDBShell.Tests/Parser/ShellObjectConversionTests.cs index 84d16489..dcf497ec 100644 --- a/CosmosDBShell.Tests/Parser/ShellObjectConversionTests.cs +++ b/CosmosDBShell.Tests/Parser/ShellObjectConversionTests.cs @@ -116,7 +116,7 @@ public void ShellJson_NumberToAllTypes() var json = Json("42"); Assert.Equal("42", json.ConvertShellObject(DataType.Text)); Assert.Equal(42, json.ConvertShellObject(DataType.Number)); - Assert.Equal(42m, json.ConvertShellObject(DataType.Decimal)); + Assert.Equal(42d, Assert.IsType(json.ConvertShellObject(DataType.Decimal))); Assert.Equal(true, json.ConvertShellObject(DataType.Boolean)); } @@ -167,10 +167,15 @@ public void ShellJson_StringToNumber(string raw, int expected) Assert.Equal(expected, Json(raw).ConvertShellObject(DataType.Number)); } - [Fact] - public void ShellJson_StringToDecimal() - { - Assert.Equal(15m, Json("\"15\"").ConvertShellObject(DataType.Decimal)); + [Theory] + [InlineData("\"15\"", 15d)] + [InlineData("\"1.5\"", 1.5d)] + [InlineData("\"1e100\"", 1e100d)] + [InlineData("1e100", 1e100d)] + [InlineData("3.0", 3d)] + public void ShellJson_ToDecimal_ReturnsDouble(string raw, double expected) + { + Assert.Equal(expected, Assert.IsType(Json(raw).ConvertShellObject(DataType.Decimal))); } [Fact] diff --git a/CosmosDBShell.Tests/Parser/StatementExecutionTests.cs b/CosmosDBShell.Tests/Parser/StatementExecutionTests.cs index 22ade0c4..88939435 100644 --- a/CosmosDBShell.Tests/Parser/StatementExecutionTests.cs +++ b/CosmosDBShell.Tests/Parser/StatementExecutionTests.cs @@ -30,6 +30,14 @@ public static System.Collections.Generic.IEnumerable ValueOriginCases( ("3.5", "+", "2", "Decimal", "5.5"), ("3", "/", "2", "Number", "1"), ("3.0", "/", "2", "Decimal", "1.5"), + ("\"2\"", "*", "1.5", "Decimal", "3"), + ("1.5", "*", "\"2\"", "Decimal", "3"), + ("\"2\"", "-", "1.5", "Decimal", "0.5"), + ("\"3.5\"", "/", "2.0", "Decimal", "1.75"), + ("5.0", "%", "\"2\"", "Decimal", "1"), + ("\"2\"", "**", "3.0", "Decimal", "8"), + ("\"2\"", "<", "2.5", "Boolean", "true"), + ("2.5", ">=", "\"2\"", "Boolean", "true"), ("[1]", "+", "[2]", "Json", "[1,2]"), ("\"a\"", "==", "\"a\"", "Boolean", "true"), ("true", "&&", "false", "Boolean", "false"), diff --git a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/ShellObject/ShellJson.cs b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/ShellObject/ShellJson.cs index ca9dd00c..0efb5b1a 100644 --- a/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/ShellObject/ShellJson.cs +++ b/CosmosDBShell/Azure.Data.Cosmos.Shell.Parser/ShellObject/ShellJson.cs @@ -91,13 +91,13 @@ public override object ConvertShellObject(DataType type) case DataType.Decimal: if (this.Value.ValueKind == JsonValueKind.Number) { - return this.Value.GetDecimal(); + return this.Value.GetDouble(); } if (this.Value.ValueKind == JsonValueKind.String) { var strValue = this.Value.GetString() ?? string.Empty; - if (decimal.TryParse(strValue, NumberStyles.Float, CultureInfo.InvariantCulture, out decimal decimalValue)) + if (double.TryParse(strValue, NumberStyles.Float, CultureInfo.InvariantCulture, out double decimalValue)) { return decimalValue; } diff --git a/README.md b/README.md index d25bce44..3b2e7f90 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,8 @@ Parser nesting, expression tree depth, and active function/script calls have fix Script diagnostics preserve source files, runtime failure categories, and function/script call sites. The language server shares the runtime's control-flow and duplicate-parameter checks, recognizes document-local functions, and checks nested commands and options. Incorrect function argument counts produce usage exit code `2`. See [validation and errors](docs/programming.md#validation-and-errors). +Numeric strings read from JSON use the same `double` conversion as shell strings when combined with decimal operands in arithmetic or ordered comparisons. String addition remains concatenation. + Loops and functions preserve JSON `null` values. Numeric conditions use the same zero/nonzero rule for shell values and JSON properties, including fractional numbers. See [value conversion rules](docs/programming.md#numbers). JSON strings use the same `+` concatenation rules as shell strings. A bare `return` is valid immediately before a closing block brace. Editor variable symbols distinguish case-sensitive names such as `$value` and `$Value`. diff --git a/docs/programming.md b/docs/programming.md index f7d15ed5..323e6989 100644 --- a/docs/programming.md +++ b/docs/programming.md @@ -44,6 +44,8 @@ JSON `null` remains JSON `null` when bound by a `for` loop or passed through a f The `+` operator concatenates when either operand is a shell string or a JSON string, including values read through JSON paths or passed as function arguments. Numeric-looking strings remain text: two JSON properties containing `"2"` concatenate to `"22"`, not `4`. +Other arithmetic operators and ordered comparisons convert numeric strings when combined with a decimal operand. This conversion uses culture-invariant `double` for both shell and JSON strings: `"2" * 1.5` and a JSON property containing `"2"` multiplied by `1.5` both produce `3`. + | Type | Syntax | Notes | | ------ | ------ | ----- | | Single-quoted | `'text'` | Literal, no escapes. Double `'` for quote: `'it''s'` |