Skip to content

Commit

Permalink
Merge pull request #1373 from concord-consortium/188007822-number-for…
Browse files Browse the repository at this point in the history
…mula

feat: implement `number` formula [PT-188007822]
  • Loading branch information
pjanik authored Jul 25, 2024
2 parents cec3e2f + 5fedfb0 commit 41696ef
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 1 deletion.
18 changes: 18 additions & 0 deletions v3/src/models/formula/functions/other-functions.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { UNDEF_RESULT } from "./function-utils"
import { math } from "./math"

describe("if", () => {
Expand Down Expand Up @@ -59,3 +60,20 @@ describe("randomBinomial", () => {
expect(integers.every((n) => Math.round(n) === n && n >= 0 && n <= 5)).toBeTruthy()
})
})

describe("number", () => {
it("converts a date to epoch time in seconds", () => {
const fn = math.compile("number(date(100500))")
expect(fn.evaluate()).toEqual(100500)
})

it("converts a date string to epoch time in seconds", () => {
const fn = math.compile("number('01/01/2020')")
expect(fn.evaluate()).toEqual(new Date('01/01/2020').getTime() / 1000) // Convert to seconds
})

it("returns UNDEF_RESULT for non-date values", () => {
const fn = math.compile("number('foo')")
expect(fn.evaluate()).toEqual(UNDEF_RESULT)
})
})
17 changes: 17 additions & 0 deletions v3/src/models/formula/functions/other-functions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { pickRandom } from "mathjs"
import { Random } from "random"
import { FValue } from "../formula-types"
import { isDate } from "../../../utilities/date-utils"
import { isDateString, parseDate } from "../../../utilities/date-parser"
import { UNDEF_RESULT } from "./function-utils"
import { extractNumeric } from "../../../utilities/math-utils"

const randomGen = new Random()

Expand Down Expand Up @@ -51,4 +55,17 @@ export const otherFunctions = {
}
},

number: {
numOfRequiredArguments: 1,
evaluate: (arg: FValue) => {
if (isDate(arg)) {
return arg.getTime() / 1000 // Convert to seconds
}
if (isDateString(arg)) {
const time = parseDate(arg)?.getTime()
return time != null ? time / 1000 : UNDEF_RESULT // Convert to seconds
}
return extractNumeric(arg) ?? UNDEF_RESULT
}
}
}
25 changes: 24 additions & 1 deletion v3/src/utilities/math-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {FormatLocaleDefinition, format, formatLocale} from "d3-format"
import {between, isFiniteNumber, isValueNonEmpty, isNumber} from "./math-utils"
import {between, isFiniteNumber, isValueNonEmpty, isNumber, extractNumeric} from "./math-utils"

// default formatting except uses ASCII minus sign
const asciiLocale = formatLocale({ minus: "-" } as FormatLocaleDefinition)
Expand Down Expand Up @@ -92,4 +92,27 @@ describe("math-utils", () => {
expect(isNumber(undefined)).toBe(false)
})
})

describe("extractNumeric", () => {
it("should return null for empty values", () => {
expect(extractNumeric("")).toBe(null)
expect(extractNumeric(null)).toBe(null)
expect(extractNumeric(undefined)).toBe(null)
})

it("should return the number for non-empty values", () => {
expect(extractNumeric("0")).toBe(0)
expect(extractNumeric("1.23")).toBe(1.23)
expect(extractNumeric(0)).toBe(0)
expect(extractNumeric(1.23)).toBe(1.23)
expect(extractNumeric(false)).toBe(0)
expect(extractNumeric(true)).toBe(1)
expect(extractNumeric("1e3")).toBe(1000)
expect(extractNumeric("1e-3")).toBe(0.001)
expect(extractNumeric("Infinity")).toBe(Infinity)
expect(extractNumeric("-Infinity")).toBe(-Infinity)
expect(extractNumeric("aa123bbb")).toBe(123)
expect(extractNumeric("123aa456")).toBe(123456)
})
})
})
21 changes: 21 additions & 0 deletions v3/src/utilities/math-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,27 @@ export const isValueNonEmpty = (value: any) => value !== "" && value != null
// It allows for strings that can be converted to numbers and treats Infinity and -Infinity as valid numbers.
export const isNumber = (v: any) => isValueNonEmpty(v) && !isNaN(Number(v))

export const extractNumeric = (v: any) => {
if (!isValueNonEmpty(v)) {
return null
}

const num = Number(v)
if (!isNaN(num)) {
return num
}

// Based on the V2 implementation for the backward compatibility.
if (typeof v === 'string') {
const noNumberPatt = /[^.\d-]+/gm
const firstNumericPatt = /(^-?\.?[\d]+(?:\.?[\d]*)?)/gm
const firstPass = v.replace(noNumberPatt, '')
const matches = firstPass.match(firstNumericPatt)
v = matches ? matches[0] : null
}
return isValueNonEmpty(v) ? Number(v) : null
}

export function goodTickValue(iMin: number, iMax: number) {
const range = (iMin >= iMax) ? Math.abs(iMin) : iMax - iMin,
gap = range / 5
Expand Down

0 comments on commit 41696ef

Please sign in to comment.