From 481218fd8271f14a4e42209c41e4501051e31a24 Mon Sep 17 00:00:00 2001 From: andrea rota Date: Wed, 20 Mar 2024 13:23:56 +0000 Subject: [PATCH] do string to float casting with some safety nets --- .../scenario-features-gap-data.geo.entity.ts | 5 ++-- .../src/numeric-string-to-float.utils.spec.ts | 21 ++++++++++++++++ .../src/numeric-string-to-float.utils.ts | 24 +++++++++++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 api/libs/utils/src/numeric-string-to-float.utils.spec.ts create mode 100644 api/libs/utils/src/numeric-string-to-float.utils.ts diff --git a/api/libs/features/src/scenario-features-gap-data.geo.entity.ts b/api/libs/features/src/scenario-features-gap-data.geo.entity.ts index 397286f2a0..c9d4450a4b 100644 --- a/api/libs/features/src/scenario-features-gap-data.geo.entity.ts +++ b/api/libs/features/src/scenario-features-gap-data.geo.entity.ts @@ -1,3 +1,4 @@ +import { numericStringToFloat } from '@marxan/utils/numeric-string-to-float.utils'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { Column, ViewEntity } from 'typeorm'; @@ -24,7 +25,7 @@ export class ScenarioFeaturesGapData { // explicitly set type, otherwise TypeORM (v10, at least) will cast to integer // TypeORM will still represent the value as string though (https://github.com/typeorm/typeorm/issues/873#issuecomment-328912050) @Column({ name: 'met', type: 'double precision' }) - @Transform(parseFloat) + @Transform(numericStringToFloat) met!: number; @ApiProperty() @@ -35,7 +36,7 @@ export class ScenarioFeaturesGapData { // explicitly set type, otherwise TypeORM (v10, at least) will cast to integer // TypeORM will still represent the value as string though (https://github.com/typeorm/typeorm/issues/873#issuecomment-328912050) @Column({ name: 'coverage_target', type: 'double precision' }) - @Transform(parseFloat) + @Transform(numericStringToFloat) coverageTarget!: number; @ApiProperty() diff --git a/api/libs/utils/src/numeric-string-to-float.utils.spec.ts b/api/libs/utils/src/numeric-string-to-float.utils.spec.ts new file mode 100644 index 0000000000..1bddc129b3 --- /dev/null +++ b/api/libs/utils/src/numeric-string-to-float.utils.spec.ts @@ -0,0 +1,21 @@ +import { numericStringToFloat } from './numeric-string-to-float.utils'; + +describe('parseOptionalFloat', () => { + it('should return undefined if the value is undefined', () => { + expect(numericStringToFloat(undefined)).toBeUndefined(); + }); + + it('should throw an exception if the value is not numeric', () => { + expect(() => numericStringToFloat('foo')).toThrow('Invalid number: foo'); + }); + + it('should return a numeric representation of the value if the value is numeric', () => { + expect(numericStringToFloat('123.456')).toBe(123.456); + }); + + it('should silently round a float to the maximum precision supported by javascript', () => { + expect(numericStringToFloat('123.456789012345678901234567890')).toBe( + 123.45678901234568, + ); + }); +}); diff --git a/api/libs/utils/src/numeric-string-to-float.utils.ts b/api/libs/utils/src/numeric-string-to-float.utils.ts new file mode 100644 index 0000000000..af3ac3fa38 --- /dev/null +++ b/api/libs/utils/src/numeric-string-to-float.utils.ts @@ -0,0 +1,24 @@ +import { isNil } from 'lodash'; + +/** + * Kind of like parseFloat(), but passing through undefined values, and handling + * values that don't cast to a number. + * + * @debt It silently rounds to the maximum precision supported by Javascript any + * input values that are numeric but beyond what can be represented in a + * Javascript number (not BigInt). Infinity and -Infinity are also passed + * through as corresponding Javascript Infinity numeric values. + */ +export function numericStringToFloat( + value: string | undefined, +): number | undefined { + // +(null) === 0, so we only cast if input is neither undefined nor null. + if (!isNil(value)) { + const floatValue = +value; + if (!isNaN(floatValue)) { + return floatValue; + } + throw new Error(`Invalid number: ${value}`); + } + return; +}