Skip to content

Commit

Permalink
Merge pull request #1237
Browse files Browse the repository at this point in the history
* [#187526964] Implement V2 string functions.

* * Disable max-len lint warnings in `date-utils.ts` for now

* * Added tests for situations that should throw errors
  • Loading branch information
bfinzer authored May 3, 2024
1 parent 40d7600 commit 4c5f135
Show file tree
Hide file tree
Showing 7 changed files with 961 additions and 23 deletions.
25 changes: 2 additions & 23 deletions v3/src/components/graph/utilities/graph-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {ScaleNumericBaseType} from "../../axis/axis-types"
import {defaultSelectedColor, defaultSelectedStroke, defaultSelectedStrokeWidth, defaultStrokeWidth}
from "../../../utilities/color-utils"
import {IDataConfigurationModel} from "../../data-display/models/data-configuration-model"
import { isFiniteNumber } from "../../../utilities/math-utils"
import { goodTickValue, isFiniteNumber } from "../../../utilities/math-utils"
import { IDomainOptions } from "../graphing-types"
import { IGraphDataConfigurationModel } from "../models/graph-data-configuration-model"
import { GraphLayout } from "../models/graph-layout"
Expand All @@ -25,27 +25,6 @@ import { t } from "../../../utilities/translation/translate"
*/
export function computeNiceNumericBounds(min: number, max: number): { min: number, max: number } {

function computeTickGap(iMin: number, iMax: number) {
const range = (iMin >= iMax) ? Math.abs(iMin) : iMax - iMin,
gap = range / 5
if (gap === 0) {
return 1
}
// We move to base 10, so we can get rid of the power of ten.
const logTrial = Math.log(gap) / Math.LN10,
floor = Math.floor(logTrial),
power = Math.pow(10.0, floor)

// Whatever is left is in the range 1 to 10. Choose desired number
let base = Math.pow(10.0, logTrial - floor)

if (base < 2) base = 1
else if (base < 5) base = 2
else base = 5

return Math.max(power * base, Number.MIN_VALUE)
}

const kAddend = 5, // amount to extend scale
kFactor = 2.5,
bounds = {min, max}
Expand All @@ -63,7 +42,7 @@ export function computeNiceNumericBounds(min: number, max: number): { min: numbe
} else if (min < 0 && max < 0 && max >= min / kFactor) { // Snap to zero
bounds.max = 0
}
const tickGap = computeTickGap(bounds.min, bounds.max)
const tickGap = goodTickValue(bounds.min, bounds.max)
if (tickGap !== 0) {
bounds.min = (Math.floor(bounds.min / tickGap) - 0.5) * tickGap
bounds.max = (Math.floor(bounds.max / tickGap) + 1.5) * tickGap
Expand Down
3 changes: 3 additions & 0 deletions v3/src/models/formula/functions/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '../formula-types'
import { equal, evaluateNode } from './function-utils'
import { arithmeticFunctions } from './arithmetic-functions'
import { stringFunctions } from './string-functions'
import { lookupFunctions } from './lookup-functions'
import { otherFunctions } from './other-functions'
import { aggregateFunctions } from './aggregate-functions'
Expand Down Expand Up @@ -77,6 +78,8 @@ export const fnRegistry = {

...arithmeticFunctions,

...stringFunctions,

...lookupFunctions,

...otherFunctions,
Expand Down
163 changes: 163 additions & 0 deletions v3/src/models/formula/functions/string-functions.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import { stringFunctions } from "./string-functions"

describe("stringFunctions", () => {
describe("beginsWith", () => {
it("returns true if text begins with prefix", () => {
expect(stringFunctions.beginsWith.evaluate("Hello World", "Hello")).toBe(true)
})

it("returns false if text does not begin with prefix", () => {
expect(stringFunctions.beginsWith.evaluate("Hello World", "World")).toBe(false)
})
})

describe("charAt", () => {
it("returns the character at the specified 1-based index in the text", () => {
expect(stringFunctions.charAt.evaluate("Hello World", 1)).toBe("H")
})
it("returns an error if the index is not an integer", () => {
expect(() => stringFunctions.charAt.evaluate("Hello World", 1.5)).toThrow()
})
})

describe("endsWith", () => {
it("returns true if text ends with suffix", () => {
expect(stringFunctions.endsWith.evaluate("Hello World", "World")).toBe(true)
})

it("returns false if text does not end with suffix", () => {
expect(stringFunctions.endsWith.evaluate("Hello World", "Hello")).toBe(false)
})
})

describe("findString", () => {
it("returns the position of stringToFind in stringToFind starting from start", () => {
expect(stringFunctions.findString.evaluate("Hello World", "World", 7)).toBe(7)
})

it("returns 0 if stringToFind is not found", () => {
expect(stringFunctions.findString.evaluate("Hello World", "Foo")).toBe(0)
})
})

describe("includes", () => {
it("returns true if the second argument is a substring of the first", () => {
expect(stringFunctions.includes.evaluate("Hello World", "World")).toBe(true)
})

it("returns false if the second argument is not a substring of the first", () => {
expect(stringFunctions.includes.evaluate("Hello World", "Foo")).toBe(false)
})
})

describe("combine", () => {
it("returns the concatenation of text all the values of the attribute", () => {
expect(stringFunctions.combine.evaluate("Hello World")).toBe("NYI")
})
})

describe("concat", () => {
it("returns a string that is the concatenation of the string arguments", () => {
expect(stringFunctions.concat.evaluate("Hello", "World")).toBe("HelloWorld")
})
})

describe("join", () => {
it("returns a string that is the concatenation of the string arguments separated by a delimiter", () => {
expect(stringFunctions.join.evaluate("/", 2024, "Hello", "World")).toBe("2024/Hello/World")
})
})

describe("patternMatches", () => {
it("returns the number of times the text matches the pattern", () => {
expect(stringFunctions.patternMatches.evaluate("Hello World", "Hello")).toBe(1)
})

it("returns 0 if the text does not match the pattern", () => {
expect(stringFunctions.patternMatches.evaluate("Hello World", "Wxrld")).toBe(0)
})
})

describe("repeatString", () => {
it("returns the text repeated n times", () => {
expect(stringFunctions.repeatString.evaluate("Hello", 3)).toBe("HelloHelloHello")
})
it("returns an error if the numRepetitions is not an integer", () => {
expect(() => stringFunctions.repeatString.evaluate("Hello World", 1.5)).toThrow()
})
})

describe("replaceChars", () => {
it("returns the text with all occurrences of search replaced with replace", () => {
expect(stringFunctions.replaceChars.evaluate("Hello World", 7, 5, "Universe")).toBe("Hello Universe")
})
it("returns the text with returns the text with the substitute inserted if numChars is 0", () => {
expect(stringFunctions.replaceChars.evaluate("Hello World", 7, 0, "Universe ")).toBe("Hello Universe World")
})
})

describe("replaceString", () => {
it("returns the text with all occurrences of search replaced with replace", () => {
expect(stringFunctions.replaceString.evaluate("Hello World", "World", "Universe")).toBe("Hello Universe")
})
})

describe("sortItems", () => {
it("returns the items in the text sorted", () => {
expect(stringFunctions.sortItems.evaluate("there, hello, cat")).toBe("cat,hello,there")
})
it("with the empty string as delimited it returns the characters sorted", () => {
expect(stringFunctions.sortItems.evaluate("cba", "")).toBe("abc")
})
})

describe("split", () => {
it("returns the element at the specified index after splitting the string by the specified separator", () => {
expect(stringFunctions.split.evaluate("Hello World", " ", 2)).toBe("World")
})

it("returns undefined if the index is out of range", () => {
expect(stringFunctions.split.evaluate("Hello World", " ", 3)).toBeUndefined()
})
it("returns an error if the index is not an integer", () => {
expect(() => stringFunctions.split.evaluate("Hello World", " ", 1.5)).toThrow()
})
})

describe("stringLength", () => {
it("returns the number of characters in the text", () => {
expect(stringFunctions.stringLength.evaluate("Hello World")).toBe(11)
})
})

describe("subString", () => {
it("returns the substring of the text starting from start with length characters", () => {
expect(stringFunctions.subString.evaluate("Hello World", 7, 5)).toBe("World")
})
it("returns an error if the start is not an integer", () => {
expect(() => stringFunctions.subString.evaluate("Hello World", 7.1, 5)).toThrow()
})
it("returns an error if the length is not an integer", () => {
expect(() => stringFunctions.subString.evaluate("Hello World", 7, 5.1)).toThrow()
})
})

describe("toLower", () => {
it("returns the text in lower case", () => {
expect(stringFunctions.toLower.evaluate("Hello World")).toBe("hello world")
})
})

describe("toUpper", () => {
it("returns the text in upper case", () => {
expect(stringFunctions.toUpper.evaluate("Hello World")).toBe("HELLO WORLD")
})
})

describe("trim", () => {
it("returns the text with leading, trailing and >2 spaces whitespace removed", () => {
expect(stringFunctions.trim.evaluate(" Hello World ")).toBe("Hello World")
})
})

})
Loading

0 comments on commit 4c5f135

Please sign in to comment.