From 06636a6346ccc426ab27c21066f48f8fcbd42064 Mon Sep 17 00:00:00 2001 From: Andrew Tang Date: Mon, 21 Jan 2019 01:26:15 -0600 Subject: [PATCH] feat(primitives): implement decimal type --- packages/primitives/package.json | 2 + packages/primitives/src/decimal.ts | 59 ++++++++++++++++++++++++ packages/primitives/src/index.ts | 12 ++++- packages/primitives/test/decimal.test.ts | 40 ++++++++++++++++ yarn.lock | 19 +++++++- 5 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 packages/primitives/src/decimal.ts create mode 100644 packages/primitives/test/decimal.test.ts diff --git a/packages/primitives/package.json b/packages/primitives/package.json index 9d3c48e..6e827b2 100644 --- a/packages/primitives/package.json +++ b/packages/primitives/package.json @@ -20,9 +20,11 @@ "test": "jest" }, "devDependencies": { + "@types/big.js": "^4.0.5", "typescript": "^3.2.2" }, "dependencies": { + "big.js": "^5.2.2", "fp-ts": "^1.12.3", "io-ts": "^1.5.2" }, diff --git a/packages/primitives/src/decimal.ts b/packages/primitives/src/decimal.ts new file mode 100644 index 0000000..9b7af55 --- /dev/null +++ b/packages/primitives/src/decimal.ts @@ -0,0 +1,59 @@ +/** + * Decimal FHIR Primitive Runtime Type + */ + +import { Big } from "big.js"; +import { Type, success, failure } from "io-ts"; + +/** + * Class that wraps big.js to maintain precision, including trailing zeros + */ +export class Decimal extends Big { + private dp: number; + constructor(n) { + super(n); + this.dp = this.decimalPlaces(n); + } + + public toFixed(dp?: number) { + return super.toFixed(this.dp); + } + + private decimalPlaces(n: string | number) { + // Coerce to string + const number = String(n); + const hasDecimal = number.includes("."); + const exponentialForm = number.includes("e"); + if (hasDecimal) { + if (exponentialForm) { + const [mantissa, exponent] = number.split(".")[1].split("e"); + return mantissa.length - parseInt(exponent); + } else { + return number.split(".")[1].length; + } + } else { + return 0; + } + } +} + +export class DecimalType extends Type { + readonly _tag: "DecimalType" = "DecimalType"; + constructor() { + super( + "decimal", + (m): m is Decimal => m instanceof Decimal, + (m, c) => { + try { + const decimal = new Decimal(m); + return success(decimal); + } catch (e) { + return failure(m, c); + } + }, + a => a.toFixed() + ); + } +} + +export const decimal = new DecimalType(); diff --git a/packages/primitives/src/index.ts b/packages/primitives/src/index.ts index ddf33a4..76a936e 100644 --- a/packages/primitives/src/index.ts +++ b/packages/primitives/src/index.ts @@ -3,7 +3,17 @@ */ import { BooleanType, boolean } from "./boolean"; +import { DecimalType, decimal } from "./decimal"; import { IntegerType, integer } from "./integer"; import { StringType, string } from "./string"; -export { BooleanType, boolean, IntegerType, integer, StringType, string }; +export { + boolean, + BooleanType, + decimal, + DecimalType, + integer, + IntegerType, + string, + StringType +}; diff --git a/packages/primitives/test/decimal.test.ts b/packages/primitives/test/decimal.test.ts new file mode 100644 index 0000000..6e307e9 --- /dev/null +++ b/packages/primitives/test/decimal.test.ts @@ -0,0 +1,40 @@ +/** + * Tests for Decimal Runtime Type + */ + +import { assertSuccess, assertFailure } from "./helpers"; +import { decimal } from "../src"; +import { Decimal } from "../src/decimal"; + +describe("DecimalType", () => { + it("should succeed validating a valid value", () => { + const T = decimal; + const input = "1.01"; + const value = new Decimal(input); + assertSuccess(T.decode(input)); + expect(T.decode(input).value).toEqual(value); + }); + + it("should successfully maintain precision", () => { + const T = decimal; + expect(T.decode("1.0100").map(T.encode).value).toEqual("1.0100"); + expect(T.decode(1.01).map(T.encode).value).toEqual("1.01"); + expect(T.decode("1.01e2").map(T.encode).value).toEqual("101"); + expect(T.decode("1.0100e2").map(T.encode).value).toEqual("101.00"); + }); + + it("should fail validating an invalid value", () => { + const T = decimal; + assertFailure(T.decode("abc"), [ + 'Invalid value "abc" supplied to : decimal' + ]); + }); + + it("should type guard", () => { + const T = decimal; + const value = new Decimal("1.010"); + expect(T.is(value)).toEqual(true); + expect(T.is("b")).toEqual(false); + expect(T.is(undefined)).toEqual(false); + }); +}); diff --git a/yarn.lock b/yarn.lock index 2e70b42..e8e7dba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -631,6 +631,11 @@ resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz#2b5a3ab3f918cca48a8c754c08168e3f03eba61b" integrity sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw== +"@types/big.js@^4.0.5": + version "4.0.5" + resolved "https://registry.yarnpkg.com/@types/big.js/-/big.js-4.0.5.tgz#62c61697646269e39191f24e55e8272f05f21fc0" + integrity sha512-D9KFrAt05FDSqLo7PU9TDHfDgkarlwdkuwFsg7Zm4xl62tTNaz+zN+Tkcdx2wGLBbSMf8BnoMhOVeUGUaJfLKg== + "@types/events@*": version "1.2.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" @@ -1111,6 +1116,11 @@ bcrypt-pbkdf@^1.0.0: dependencies: tweetnacl "^0.14.3" +big.js@^5.2.2: + version "5.2.2" + resolved "https://registry.yarnpkg.com/big.js/-/big.js-5.2.2.tgz#65f0af382f578bcdc742bd9c281e9cb2d7768328" + integrity sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ== + bin-links@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/bin-links/-/bin-links-1.1.2.tgz#fb74bd54bae6b7befc6c6221f25322ac830d9757" @@ -2184,6 +2194,13 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" +"fhir-ts-codegen@file:packages/fhir-ts-codegen": + version "0.0.6" + dependencies: + clime "^0.5.9" + glob "^7.1.3" + ts-simple-ast "^14.4.2" + figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" @@ -5997,7 +6014,7 @@ tslint-config-prettier@^1.15.0: resolved "https://registry.yarnpkg.com/tslint-config-prettier/-/tslint-config-prettier-1.17.0.tgz#946ed6117f98f3659a65848279156d87628c33dc" integrity sha512-NKWNkThwqE4Snn4Cm6SZB7lV5RMDDFsBwz6fWUkTxOKGjMx8ycOHnjIbhn7dZd5XmssW3CwqUjlANR6EhP9YQw== -tslint@^5.11.0, tslint@^5.12.1: +tslint@^5.11.0: version "5.12.1" resolved "https://registry.yarnpkg.com/tslint/-/tslint-5.12.1.tgz#8cec9d454cf8a1de9b0a26d7bdbad6de362e52c1" integrity sha512-sfodBHOucFg6egff8d1BvuofoOQ/nOeYNfbp7LDlKBcLNrL3lmS5zoiDGyOMdT7YsEXAwWpTdAHwOGOc8eRZAw==