diff --git a/javascript/packages/core/lib/typeResolver.ts b/javascript/packages/core/lib/typeResolver.ts index 30d27d54c0..4ab4a9ed31 100644 --- a/javascript/packages/core/lib/typeResolver.ts +++ b/javascript/packages/core/lib/typeResolver.ts @@ -358,10 +358,12 @@ export default class TypeResolver { } return this.varint32Serializer; } - if (v > MaxInt32 || v < MinInt32) { - return this.float64Serializer; + // A non-integer number is a float64 value; narrow to float32 only when + // that representation is exact, otherwise precision is silently lost. + if (Math.fround(v) === v) { + return this.float32Serializer; } - return this.float32Serializer; + return this.float64Serializer; } if (typeof v === "bigint") { diff --git a/javascript/test/any.test.ts b/javascript/test/any.test.ts index 7a977301e2..f040ea899b 100644 --- a/javascript/test/any.test.ts +++ b/javascript/test/any.test.ts @@ -18,6 +18,7 @@ */ import Fory, { Type } from "../packages/core/index"; +import { TypeId } from "../packages/core/lib/type"; import { describe, expect, test } from "@jest/globals"; describe("bool", () => { @@ -59,7 +60,7 @@ describe("bool", () => { test("should write float work", () => { const fory = new Fory({ compatible: false }); const bin = fory.serialize(123.123); - expect(fory.deserialize(bin).toFixed(3)).toBe("123.123"); + expect(fory.deserialize(bin)).toBe(123.123); }); test("should write bigint work", () => { @@ -115,4 +116,34 @@ describe("bool", () => { const result = deserialize(bin); expect(result).toEqual("hello"); }); + + test.each([ + [1.5, TypeId.FLOAT32], + [-1.5, TypeId.FLOAT32], + [2 ** -149, TypeId.FLOAT32], + [-(2 ** -149), TypeId.FLOAT32], + [0.1, TypeId.FLOAT64], + [1 / 3, TypeId.FLOAT64], + [-0.7, TypeId.FLOAT64], + [1.5 + Number.EPSILON, TypeId.FLOAT64], + [Number.MIN_VALUE, TypeId.FLOAT64], + [-Number.MIN_VALUE, TypeId.FLOAT64], + [3000000000.5, TypeId.FLOAT64], + ])("should dispatch %p as type %p", (value, typeId) => { + const fory = new Fory({ compatible: false }); + // Round trips alone also pass if every value is written as float64. + expect(fory.typeResolver.getSerializerByData(value)).toBe( + fory.typeResolver.getSerializerById(typeId), + ); + expect(fory.deserialize(fory.serialize(value))).toBe(value); + }); + + test("should preserve mixed float precision", () => { + // Non-integer numbers narrow to float32 only when exactly representable; + // otherwise the dynamic dispatch must pick float64. + const fory = new Fory({ compatible: false }); + const { serialize, deserialize } = fory.register(Type.list(Type.any())); + const values = [0.1, 1 / 3, 1234.5678, -0.7, 1.5, 3000000000.5]; + expect(deserialize(serialize(values))).toEqual(values); + }); });