Skip to content

Commit

Permalink
187738935 v3 DI caseFormulaSearch (#1372)
Browse files Browse the repository at this point in the history
* Handle get caseFormulaSearch requests.
  • Loading branch information
tealefristoe authored Jul 25, 2024
1 parent a54699c commit 6750b30
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 10 deletions.
4 changes: 3 additions & 1 deletion v3/src/data-interactive/data-interactive-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,13 +145,14 @@ export interface DIResources {
attributeLocation?: IAttribute
caseByID?: ICase
caseByIndex?: ICase
caseFormulaSearch?: DICase[]
caseFormulaSearch?: ICase[]
caseSearch?: ICase[]
collection?: ICollectionModel
collectionList?: ICollectionModel[]
component?: DIComponent
dataContext?: IDataSet
dataContextList?: IDataSet[]
error?: string
global?: IGlobalValue
interactiveFrame?: ITileModel
isDefaultDataContext?: boolean
Expand Down Expand Up @@ -219,6 +220,7 @@ export interface DIResourceSelector {
case?: string
caseByID?: string
caseByIndex?: string
caseFormulaSearch?: string
caseSearch?: string
collection?: string
component?: string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { toV2Id } from "../../utilities/codap-utils"
import { DIFullCase } from "../data-interactive-types"
import { diCaseFormulaSearchHandler } from "./case-formula-search-handler"
import { setupTestDataset } from "./handler-test-utils"


describe("DataInteractive CaseFormulaSearchHandler", () => {
const handler = diCaseFormulaSearchHandler

it("get works", () => {
const { dataset: dataContext, c2: collection } = setupTestDataset()
const cases = dataContext.getGroupsForCollection(collection.id).map(c => c.groupedCase)
const caseFormulaSearch = [cases[0], cases[2], cases[3]]

expect(handler.get?.({ dataContext, collection }).success).toBe(false)
expect(handler.get?.({ dataContext, caseFormulaSearch }).success).toBe(false)
expect(handler.get?.({ collection, caseFormulaSearch }).success).toBe(false)

const result = handler.get!({ dataContext, collection, caseFormulaSearch })
expect(result.success).toBe(true)
const values = result.values as DIFullCase[]
caseFormulaSearch.forEach((item, index) => {
expect(values[index].id).toBe(toV2Id(item.__id__))
const itemIndex = dataContext.getItemIndex(dataContext.caseInfoMap.get(item.__id__)!.childItemIds[0])!
collection.attributes.forEach(attribute => {
expect(attribute && values[index].values?.[attribute.name]).toBe(attribute?.value(itemIndex))
})
})
})
})
18 changes: 16 additions & 2 deletions v3/src/data-interactive/handlers/case-formula-search-handler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import { registerDIHandler } from "../data-interactive-handler"
import { DIHandler, diNotImplementedYet } from "../data-interactive-types"
import { DIHandler, DIResources } from "../data-interactive-types"
import { getCaseRequestResultValues } from "../data-interactive-type-utils"
import { collectionNotFoundResult, couldNotParseQueryResult, dataContextNotFoundResult } from "./di-results"

export const diCaseFormulaSearchHandler: DIHandler = {
get: diNotImplementedYet
get(resources: DIResources) {
const { caseFormulaSearch, collection, dataContext, error } = resources
if (!collection) return collectionNotFoundResult
if (!dataContext) return dataContextNotFoundResult
if (error) return { success: false, error }
if (!caseFormulaSearch) return couldNotParseQueryResult

return {
success: true,
values: caseFormulaSearch?.map(aCase =>
getCaseRequestResultValues(aCase, dataContext).case)
}
}
}

registerDIHandler("caseFormulaSearch", diCaseFormulaSearchHandler)
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { toV2Id } from "../../utilities/codap-utils"
import { DIFullCase } from "../data-interactive-types"
import { setupTestDataset } from "./handler-test-utils"
import { diCaseSearchHandler } from "./case-search-handler"
import { setupTestDataset } from "./handler-test-utils"


describe("DataInteractive CaseSearchHandler", () => {
Expand Down
23 changes: 22 additions & 1 deletion v3/src/data-interactive/resource-parser-utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { setupTestDataset } from "./handlers/handler-test-utils"
import { parseSearchQuery } from "./resource-parser-utils"
import { evaluateCaseFormula, parseSearchQuery } from "./resource-parser-utils"

describe("DataInteractive ResourceParser Utilities", () => {
it("parses search queries", () => {
Expand Down Expand Up @@ -31,4 +31,25 @@ describe("DataInteractive ResourceParser Utilities", () => {
expect(emptyResult.left?.attr?.id).toBe(a1.id)
expect(emptyResult.right?.value).toBe("")
})

it("evaluates case formulas", () => {
const { dataset, c1, c2 } = setupTestDataset()
const c3 = dataset.collections[2]

expect(evaluateCaseFormula("bad formula", dataset, c1).valid).toBe(false)

const allResult = evaluateCaseFormula("true", dataset, c1)
expect(allResult.valid).toBe(true)
expect(allResult.caseIds?.length).toBe(2)
expect(evaluateCaseFormula("true", dataset, c2).caseIds?.length).toBe(4)
expect(evaluateCaseFormula("true", dataset, c3).caseIds?.length).toBe(6)

expect(evaluateCaseFormula("a3 < 4", dataset, c3).caseIds?.length).toBe(3)
expect(evaluateCaseFormula("a3 < 4", dataset, c2).caseIds?.length).toBe(3)
expect(evaluateCaseFormula("a3 < 4", dataset, c1).caseIds?.length).toBe(2)

expect(evaluateCaseFormula(`a3 < 4 and a1 != "b"`, dataset, c3).caseIds?.length).toBe(2)
expect(evaluateCaseFormula(`a3 < 4 and a1 != "b"`, dataset, c2).caseIds?.length).toBe(2)
expect(evaluateCaseFormula(`a3 < 4 and a1 != "b"`, dataset, c1).caseIds?.length).toBe(1)
})
})
60 changes: 60 additions & 0 deletions v3/src/data-interactive/resource-parser-utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import { appState } from "../models/app-state"
import { ICollectionModel } from "../models/data/collection"
import { IDataSet } from "../models/data/data-set"
import { FormulaMathJsScope } from "../models/formula/formula-mathjs-scope"
import { math } from "../models/formula/functions/math"
import { displayToCanonical } from "../models/formula/utils/canonicalization-utils"
import { getDisplayNameMap } from "../models/formula/utils/name-mapping-utils"
import { getSharedDataSets } from "../models/shared/shared-data-utils"
import { getTilePrefixes } from "../models/tiles/tile-content-info"
import { getGlobalValueManager, getSharedModelManager } from "../models/tiles/tile-environment"
import { toV3Id, toV3TileId } from "../utilities/codap-utils"
import { t } from "../utilities/translation/translate"
import { DIParsedQuery, DIQueryFunction } from "./data-interactive-types"

export function parseSearchQuery(query: string, dataContextOrCollection?: IDataSet | ICollectionModel): DIParsedQuery {
Expand Down Expand Up @@ -46,6 +53,59 @@ export function parseSearchQuery(query: string, dataContextOrCollection?: IDataS
return { valid, left, right, func }
}

export function evaluateCaseFormula(displayFormula: string, dataset: IDataSet, collection: ICollectionModel) {
// Build displayNameMap
const { document } = appState
const localDataSet = dataset
const dataSets: Map<string, IDataSet> = new Map()
getSharedDataSets(document).forEach(sharedDataSet => {
const { dataSet } = sharedDataSet
dataSets.set(dataSet.id, dataSet)
})
const globalValueManager = getGlobalValueManager(getSharedModelManager(document))
const displayNameMap = getDisplayNameMap({
localDataSet,
dataSets,
globalValueManager,
})

// Canonicalize formula
let formula = ""
try {
formula = displayToCanonical(displayFormula, displayNameMap)
} catch (e: any) {
return { valid: false, error: t("V3.DI.Error.couldNotParseQuery") }
}

// Evaluate formula for each case in collection
const caseIds: string[] = []
const childMostCollectionCaseIds = dataset.childCollection.caseIds
const errors = collection.caseIds.map(caseId => {
const scope = new FormulaMathJsScope({
localDataSet,
dataSets,
globalValueManager,
caseIds: [caseId],
childMostCollectionCaseIds
})

try {
if (math.evaluate(formula, scope)) caseIds.push(caseId)
} catch (e: any) {
return e.message
}
})

// Fail if any errors were encountered
const error = errors.find(e => !!e)
if (error) {
return { valid: false, error }
}

// Return case ids for cases that satisfied the formula
return { valid: true, caseIds }
}

export function findTileFromV2Id(v2Id: string) {
const { document } = appState
// We look for every possible v3 id the component might have (because each tile type has a different prefix).
Expand Down
30 changes: 29 additions & 1 deletion v3/src/data-interactive/resource-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe("DataInteractive ResourceParser", () => {
expect(resolve(`dataContext[data].collection[collection2].caseSearch[a1==a]`).caseSearch).toBeUndefined()

const allResult = resolve(`dataContext[data].collection[collection2].caseSearch[*]`)
expect(allResult.caseSearch?.length).toBe(dataset.getCasesForCollection(c2.id).length)
expect(allResult.caseSearch?.length).toBe(c2.cases.length)

const a1Result = resolve(`dataContext[data].collection[collection1].caseSearch[a1==a]`)
expect(a1Result.caseSearch?.length).toBe(1)
Expand All @@ -127,6 +127,34 @@ describe("DataInteractive ResourceParser", () => {
expect(a3Result.caseSearch?.length).toBe(5)
})

it("finds caseFormulaSearch", () => {
expect(resolve(`dataContext[data].collection[collection2].caseFormulaSearch`).caseFormulaSearch).toBeUndefined()
expect(resolve(`dataContext[data].collection[collection2].caseFormulaSearch[]`).caseFormulaSearch).toBeUndefined()
expect(resolve(`dataContext[data].collection[collection2].caseFormulaSearch[bad formula]`).error).toBeDefined()
expect(resolve(`dataContext[data].collection[collection2].caseFormulaSearch[>a2]`).error).toBeDefined()

const allResult = resolve(`dataContext[data].collection[collection2].caseFormulaSearch[true]`)
expect(allResult.caseFormulaSearch?.length).toBe(c2.cases.length)

const a11Result = resolve(`dataContext[data].collection[collection1].caseFormulaSearch[a1="a"]`)
expect(a11Result.caseFormulaSearch?.length).toBe(1)
const a21Result = resolve(`dataContext[data].collection[collection2].caseFormulaSearch[a1="a"]`)
expect(a21Result.caseFormulaSearch?.length).toBe(2)
const a31Result = resolve(`dataContext[data].collection[collection3].caseFormulaSearch[a1="a"]`)
expect(a31Result.caseFormulaSearch?.length).toBe(3)

const a12Result = resolve(`dataContext[data].collection[collection1].caseFormulaSearch[ a2 = "z" ]`)
// Can only check attributes in the collection or a parent collection
expect(a12Result.caseFormulaSearch?.length).toBe(0)
const a22Result = resolve(`dataContext[data].collection[collection2].caseFormulaSearch[ a2 = "z" ]`)
expect(a22Result.caseFormulaSearch?.length).toBe(2)
const a32Result = resolve(`dataContext[data].collection[collection3].caseFormulaSearch[ a2 = "z" ]`)
expect(a32Result.caseFormulaSearch?.length).toBe(2)

const a33Result = resolve(`dataContext[data].collection[collection3].caseFormulaSearch[a3>2]`)
expect(a33Result.caseFormulaSearch?.length).toBe(4)
})

it("finds item", () => {
expect(resolve(`dataContext[data].item`).item).toBeUndefined()
expect(resolve(`dataContext[data].item[-1]`).item).toBeUndefined()
Expand Down
18 changes: 14 additions & 4 deletions v3/src/data-interactive/resource-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { ITileModel } from "../models/tiles/tile-model"
import { toV3CaseId, toV3GlobalId, toV3ItemId } from "../utilities/codap-utils"
import { ActionName, DIResources, DIResourceSelector, DIParsedOperand } from "./data-interactive-types"
import { getAttribute, getCollection } from "./data-interactive-utils"
import { findTileFromV2Id, parseSearchQuery } from "./resource-parser-utils"
import { evaluateCaseFormula, findTileFromV2Id, parseSearchQuery } from "./resource-parser-utils"

/**
* A resource selector identifies a CODAP resource. It is either a group
Expand Down Expand Up @@ -185,9 +185,19 @@ export function resolveResources(
}
}

// if (resourceSelector.caseFormulaSearch) {
// result.caseFormulaSearch = collection && collection.searchCasesByFormula(resourceSelector.caseFormulaSearch);
// }
if (resourceSelector.caseFormulaSearch && collection && dataContext) {
result.caseFormulaSearch = []
const { valid, caseIds, error } =
evaluateCaseFormula(resourceSelector.caseFormulaSearch, dataContext, collection)
if (valid && caseIds) {
caseIds.forEach(caseId => {
const caseGroup = collection.getCaseGroup(caseId)
if (caseGroup) result.caseFormulaSearch?.push(caseGroup.groupedCase)
})
} else {
result.error = error
}
}

if (resourceSelector.item) {
const index = Number(resourceSelector.item)
Expand Down

0 comments on commit 6750b30

Please sign in to comment.