diff --git a/src/FSharp.Data.Json.Core/JsonInference.fs b/src/FSharp.Data.Json.Core/JsonInference.fs index 1699e66b9..429db173f 100644 --- a/src/FSharp.Data.Json.Core/JsonInference.fs +++ b/src/FSharp.Data.Json.Core/JsonInference.fs @@ -16,9 +16,7 @@ open FSharp.Data.Runtime.StructuralInference /// here we just need to infer types of primitive JSON values. let rec internal inferType unitsOfMeasureProvider inferenceMode cultureInfo parentName json = let inline inRangeDecimal lo hi (v: decimal) : bool = (v >= decimal lo) && (v <= decimal hi) - let inline inRangeFloat lo hi (v: float) : bool = (v >= float lo) && (v <= float hi) let inline isIntegerDecimal (v: decimal) : bool = Math.Round v = v - let inline isIntegerFloat (v: float) : bool = Math.Round v = v let shouldInferNonStringFromValue = match inferenceMode with @@ -49,18 +47,10 @@ let rec internal inferType unitsOfMeasureProvider inferenceMode cultureInfo pare -> InferedType.Primitive(typeof, None, false, false) | JsonValue.Number _ -> InferedType.Primitive(typeof, None, false, false) - | JsonValue.Float f when - shouldInferNonStringFromValue - && inRangeFloat Int32.MinValue Int32.MaxValue f - && isIntegerFloat f - -> - InferedType.Primitive(typeof, None, false, false) - | JsonValue.Float f when - shouldInferNonStringFromValue - && inRangeFloat Int64.MinValue Int64.MaxValue f - && isIntegerFloat f - -> - InferedType.Primitive(typeof, None, false, false) + // JsonValue.Float is produced when the JSON number uses exponential notation (e.g. 0.1e1, 2.34E5) + // because TextConversions.AsDecimal uses NumberStyles.Currency which excludes AllowExponent. + // Such values are always inferred as float regardless of whether the value happens to be a whole + // number, so that e.g. [0.1e1, 0.2e1] is inferred as float[] not int[]. See issue #1221. | JsonValue.Float _ -> InferedType.Primitive(typeof, None, false, false) // More interesting types | JsonValue.Array ar -> diff --git a/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs b/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs index 414c82ec3..ff28fb6f4 100644 --- a/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs +++ b/tests/FSharp.Data.DesignTime.Tests/InferenceTests.fs @@ -89,6 +89,18 @@ let ``Finds common subtype of numeric types (float)``() = let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source actual |> should equal expected +// Regression test for https://github.com/fsprojects/FSharp.Data/issues/1221 +// Exponential-notation JSON numbers (parsed as JsonValue.Float) should always infer as float, +// not be promoted to int/int64 when the value happens to be a whole number. +[] +let ``Exponential-notation numbers infer as float not int (issue 1221)``() = + // 0.1e1 = 1.0, 0.2e1 = 2.0 — these are stored as JsonValue.Float because + // TextConversions.AsDecimal (NumberStyles.Currency) does not allow exponent notation + let source = JsonValue.Parse """[ 0.1e1, 0.2e1, 1e3 ]""" + let expected = SimpleCollection(InferedType.Primitive(typeof, None, false, false)) + let actual = JsonInference.inferType unitsOfMeasureProvider inferenceMode culture "" source + actual |> should equal expected + [] let ``Infers heterogeneous type of InferedType.Primitives and records``() = let source = JsonValue.Parse """[ {"a":0}, 1,2 ]""" diff --git a/tests/FSharp.Data.Tests/JsonProvider.fs b/tests/FSharp.Data.Tests/JsonProvider.fs index 5bc372124..c18d4bf90 100644 --- a/tests/FSharp.Data.Tests/JsonProvider.fs +++ b/tests/FSharp.Data.Tests/JsonProvider.fs @@ -816,17 +816,18 @@ let ``ParseList return result list`` () = let prov = NumericFields.ParseList(""" [{"a":123}, {"a":987}] """) prov |> Array.map (fun v -> v.A) |> Array.sort |> should equal [|123M; 987M|] -// Regression test for https://github.com/fsprojects/FSharp.Data/issues/1230 +// Regression test for https://github.com/fsprojects/FSharp.Data/issues/1230 / #1221 // When a JSON array sample mixes decimal and exponential-notation numbers, the inferred -// type is decimal (because the exponential value is stored as JsonValue.Float and inferred -// as integer, which is then unified with decimal). At runtime, any exponential-notation -// number in the actual JSON must also be convertible to decimal. +// type is float. Exponential-notation numbers (e.g. 2.34567E5) are stored as JsonValue.Float +// (because TextConversions.AsDecimal uses NumberStyles.Currency which excludes AllowExponent) +// and are always inferred as float, not promoted to int/int64 as in earlier versions. +// This means [1, 2.34567E5, 3.14] is inferred as float[]. type ExponentialDecimalProvider = JsonProvider<"""{"mydata": [1, 2.34567E5, 3.14]}"""> [] -let ``Decimal inferred from mixed-notation array can parse exponential notation at runtime`` () = +let ``Mixed-notation array with exponential numbers is inferred as float`` () = let result = ExponentialDecimalProvider.Parse("""{"mydata": [2, 3.45678E5, 9.01]}""") - result.Mydata |> should equal [| 2M; 345678M; 9.01M |] + result.Mydata |> should equal [| 2.0; 345678.0; 9.01 |] type ServiceResponse = JsonProvider<"""[