diff --git a/v3/src/data-interactive/data-interactive-type-utils.ts b/v3/src/data-interactive/data-interactive-type-utils.ts index 97bbefe85..0cf2431e8 100644 --- a/v3/src/data-interactive/data-interactive-type-utils.ts +++ b/v3/src/data-interactive/data-interactive-type-utils.ts @@ -8,7 +8,7 @@ import { IGlobalValue } from "../models/global/global-value" import { getSharedCaseMetadataFromDataset } from "../models/shared/shared-data-utils" import { kAttrIdPrefix, maybeToV2Id, toV2Id, toV3AttrId } from "../utilities/codap-utils" import { - ICodapV2Attribute, ICodapV2CollectionV3, ICodapV2DataContextV3, v3TypeFromV2TypeString + ICodapV2Attribute, ICodapV2Case, ICodapV2CollectionV3, ICodapV2DataContextV3, v3TypeFromV2TypeString } from "../v2/codap-v2-types" import { DIAttribute, DIGetCaseResult, DIResources, DISingleValues } from "./data-interactive-types" import { getCaseValues } from "./data-interactive-utils" @@ -139,17 +139,36 @@ export function convertAttributeToV2FromResources(resources: DIResources) { } } -export function convertCollectionToV2(collection: ICollectionModel, dataContext?: IDataSet): ICodapV2CollectionV3 { +interface CCV2Options { + dataSet?: IDataSet + exportCases?: boolean +} +export function convertCollectionToV2(collection: ICollectionModel, options?: CCV2Options): ICodapV2CollectionV3 { const { name, title, id, labels: _labels } = collection + const { dataSet, exportCases } = options || {} const v2Id = toV2Id(id) const labels = _labels ? getSnapshot(_labels) : undefined const attrs = collection.attributes.map(attribute => { - if (attribute) return convertAttributeToV2(attribute, dataContext) + if (attribute) return convertAttributeToV2(attribute, dataSet) }).filter(attr => !!attr) + + let cases: Maybe<{ cases: ICodapV2Case[] }> + if (exportCases) { + cases = { + cases: collection.cases.map(aCase => { + const v2CaseId = toV2Id(aCase.__id__) + const values: ICodapV2Case["values"] = {} + collection.allDataAttributes.forEach(attr => { + values[attr.name] = dataSet?.getValue(aCase.__id__, attr.id) ?? "" + }) + return { guid: v2CaseId, id: v2CaseId, values } + }) + } + } return { // areParentChildLinksConfigured, attrs, - // cases, + ...cases, // caseName, // childAttrName, // collapseChildren, @@ -163,16 +182,17 @@ export function convertCollectionToV2(collection: ICollectionModel, dataContext? } } -export function convertDataSetToV2(dataSet: IDataSet, docId: number | string): ICodapV2DataContextV3 { +export function convertDataSetToV2(dataSet: IDataSet, exportCases = false): ICodapV2DataContextV3 { const { name, title, id, description } = dataSet const v2Id = toV2Id(id) + dataSet.validateCases() const collections: ICodapV2CollectionV3[] = - dataSet.collections.map(collection => convertCollectionToV2(collection, dataSet)) + dataSet.collections.map(collection => convertCollectionToV2(collection, { dataSet, exportCases })) return { type: "DG.DataContext", - document: docId, + document: 1, guid: v2Id, id: v2Id, // flexibleGroupChangeFlag, diff --git a/v3/src/data-interactive/handlers/all-cases-handler.test.ts b/v3/src/data-interactive/handlers/all-cases-handler.test.ts index df0ed1a02..cc539077f 100644 --- a/v3/src/data-interactive/handlers/all-cases-handler.test.ts +++ b/v3/src/data-interactive/handlers/all-cases-handler.test.ts @@ -20,7 +20,7 @@ describe("DataInteractive AllCasesHandler", () => { } const getCase = (result: GetAllCasesResult, index: number) => result.values.cases![index].case! - const checkCase = (c: any, attribute: string, value: string, children: number, parent?: number) => { + const checkCase = (c: any, attribute: string, value: number | string, children: number, parent?: number) => { expect(c.values[attribute]).toBe(value) expect(c.children.length).toBe(children) expect(c.parent).toBe(parent) @@ -49,11 +49,11 @@ describe("DataInteractive AllCasesHandler", () => { const result3 = handler.get?.({ dataContext: dataset, collection: dataset?.childCollection }) as GetAllCasesResult expect(result3.success).toBe(true) expect(result3.values.cases?.length).toBe(6) - checkCase(getCase(result3, 0), "a3", "1", 0, c2c1.id) - checkCase(getCase(result3, 1), "a3", "5", 0, c2c1.id) - checkCase(getCase(result3, 2), "a3", "3", 0, c2c2.id) - checkCase(getCase(result3, 3), "a3", "2", 0, c2c3.id) - checkCase(getCase(result3, 4), "a3", "6", 0, c2c3.id) - checkCase(getCase(result3, 5), "a3", "4", 0, c2c4.id) + checkCase(getCase(result3, 0), "a3", 1, 0, c2c1.id) + checkCase(getCase(result3, 1), "a3", 5, 0, c2c1.id) + checkCase(getCase(result3, 2), "a3", 3, 0, c2c2.id) + checkCase(getCase(result3, 3), "a3", 2, 0, c2c3.id) + checkCase(getCase(result3, 4), "a3", 6, 0, c2c3.id) + checkCase(getCase(result3, 5), "a3", 4, 0, c2c4.id) }) }) diff --git a/v3/src/data-interactive/handlers/case-handler.test.ts b/v3/src/data-interactive/handlers/case-handler.test.ts index f3107d746..06923df7b 100644 --- a/v3/src/data-interactive/handlers/case-handler.test.ts +++ b/v3/src/data-interactive/handlers/case-handler.test.ts @@ -63,7 +63,7 @@ describe("DataInteractive CaseHandler", () => { const caseId0 = dataset.items[0].__id__ const result = handler.update?.({ dataContext: dataset }, { id: toV2Id(caseId0), values: { a3: 10 } } as DIValues) expect(result?.success).toBe(true) - expect(dataset.getAttributeByName("a3")?.value(0)).toBe("10") + expect(dataset.getAttributeByName("a3")?.value(0)).toBe(10) expect((result as DISuccessResult).caseIDs?.includes(toV2Id(caseId0))).toBe(true) // Update multiple cases diff --git a/v3/src/data-interactive/handlers/collection-handler.ts b/v3/src/data-interactive/handlers/collection-handler.ts index 13cbcebc3..22edc3371 100644 --- a/v3/src/data-interactive/handlers/collection-handler.ts +++ b/v3/src/data-interactive/handlers/collection-handler.ts @@ -109,7 +109,7 @@ export const diCollectionHandler: DIHandler = { if (!dataContext) return dataContextNotFoundResult if (!collection) return collectionNotFoundResult - const v2Collection = convertCollectionToV2(collection, dataContext) + const v2Collection = convertCollectionToV2(collection, { dataSet: dataContext }) return { success: true, values: v2Collection diff --git a/v3/src/data-interactive/handlers/data-context-handler.ts b/v3/src/data-interactive/handlers/data-context-handler.ts index d1fbb890b..7ec079b02 100644 --- a/v3/src/data-interactive/handlers/data-context-handler.ts +++ b/v3/src/data-interactive/handlers/data-context-handler.ts @@ -20,7 +20,7 @@ import { findTileFromNameOrId } from "../resource-parser-utils" import { createCollection } from "./di-handler-utils" import { attributeNotFoundResult, dataContextNotFoundResult, errorResult, fieldRequiredResult } from "./di-results" -const requestRequiedResult = fieldRequiredResult("Notify", "dataContext", "request") +const requestRequiredResult = fieldRequiredResult("Notify", "dataContext", "request") export const diDataContextHandler: DIHandler = { create(_resources: DIResources, _values?: DIValues) { @@ -73,16 +73,16 @@ export const diDataContextHandler: DIHandler = { const { dataContext } = resources if (!dataContext) return dataContextNotFoundResult - return { success: true, values: convertDataSetToV2(dataContext, appState.document.key) } + return { success: true, values: convertDataSetToV2(dataContext) } }, notify(resources: DIResources, values?: DIValues) { const { dataContext } = resources if (!dataContext) return dataContextNotFoundResult - if (!values) return requestRequiedResult + if (!values) return requestRequiredResult const { caseIDs, operation, request } = values as DINotifyDataContext - if (!request) return requestRequiedResult + if (!request) return requestRequiredResult const successResult = { success: true as const, values: {} } if (request === "setAside") { @@ -100,7 +100,7 @@ export const diDataContextHandler: DIHandler = { } else { addSetAsideCases(dataContext, v3CaseIds, false) } - + return successResult } diff --git a/v3/src/models/data/attribute.test.ts b/v3/src/models/data/attribute.test.ts index 94d1d2621..33adb81cc 100644 --- a/v3/src/models/data/attribute.test.ts +++ b/v3/src/models/data/attribute.test.ts @@ -114,52 +114,52 @@ describe("Attribute", () => { attribute.addValue("1") expect(attribute.length).toBe(1) - expect(attribute.value(0)).toBe("1") - expect(attribute.numeric(0)).toBe(1) + expect(attribute.value(0)).toBe(1) + expect(attribute.numValue(0)).toBe(1) attribute.addValues([2, 3]) expect(attribute.length).toBe(3) - expect(attribute.value(1)).toBe("2") - expect(attribute.value(2)).toBe("3") - expect(attribute.numeric(1)).toBe(2) - expect(attribute.numeric(2)).toBe(3) + expect(attribute.value(1)).toBe(2) + expect(attribute.value(2)).toBe(3) + expect(attribute.numValue(1)).toBe(2) + expect(attribute.numValue(2)).toBe(3) expect(attribute.getNumericCount()).toBe(3) attribute.addValue(0, 0) expect(attribute.length).toBe(4) - expect(attribute.value(0)).toBe("0") - expect(attribute.value(3)).toBe("3") + expect(attribute.value(0)).toBe(0) + expect(attribute.value(3)).toBe(3) expect(attribute.isNumeric(0)).toBe(true) expect(attribute.isNumeric(3)).toBe(true) - expect(attribute.numeric(0)).toBe(0) - expect(attribute.numeric(3)).toBe(3) + expect(attribute.numValue(0)).toBe(0) + expect(attribute.numValue(3)).toBe(3) attribute.addValues(["-2", "-1"], 0) expect(attribute.length).toBe(6) - expect(attribute.value(0)).toBe("-2") - expect(attribute.value(1)).toBe("-1") - expect(attribute.value(5)).toBe("3") - expect(attribute.numeric(0)).toBe(-2) - expect(attribute.numeric(1)).toBe(-1) - expect(attribute.numeric(5)).toBe(3) + expect(attribute.value(0)).toBe(-2) + expect(attribute.value(1)).toBe(-1) + expect(attribute.value(5)).toBe(3) + expect(attribute.numValue(0)).toBe(-2) + expect(attribute.numValue(1)).toBe(-1) + expect(attribute.numValue(5)).toBe(3) attribute.setValue(2, 3) - expect(attribute.value(2)).toBe("3") - expect(attribute.numeric(2)).toBe(3) + expect(attribute.value(2)).toBe(3) + expect(attribute.numValue(2)).toBe(3) attribute.setValue(10, 10) attribute.setValues([0, 1], ["1", "2"]) - expect(attribute.value(0)).toBe("1") - expect(attribute.value(1)).toBe("2") - expect(attribute.numeric(0)).toBe(1) - expect(attribute.numeric(1)).toBe(2) + expect(attribute.value(0)).toBe(1) + expect(attribute.value(1)).toBe(2) + expect(attribute.numValue(0)).toBe(1) + expect(attribute.numValue(1)).toBe(2) attribute.setValues([10, 11], [10, 11]) attribute.setValues([0, 1], [0]) - expect(attribute.value(0)).toBe("0") - expect(attribute.value(1)).toBe("2") - expect(attribute.numeric(0)).toBe(0) - expect(attribute.numeric(1)).toBe(2) + expect(attribute.value(0)).toBe(0) + expect(attribute.value(1)).toBe(2) + expect(attribute.numValue(0)).toBe(0) + expect(attribute.numValue(1)).toBe(2) expect(attribute.getNumericCount()).toBe(6) expect(attribute.type).toBe("numeric") @@ -169,22 +169,22 @@ describe("Attribute", () => { attribute.removeValues(2) expect(attribute.length).toBe(5) - expect(attribute.value(2)).toBe("1") - expect(attribute.numeric(2)).toBe(1) + expect(attribute.value(2)).toBe(1) + expect(attribute.numValue(2)).toBe(1) attribute.removeValues(0, 2) expect(attribute.length).toBe(3) - expect(attribute.value(0)).toBe("1") - expect(attribute.numeric(0)).toBe(1) + expect(attribute.value(0)).toBe(1) + expect(attribute.numValue(0)).toBe(1) attribute.removeValues(0, 0) expect(attribute.length).toBe(3) - expect(attribute.value(0)).toBe("1") - expect(attribute.numeric(0)).toBe(1) + expect(attribute.value(0)).toBe(1) + expect(attribute.numValue(0)).toBe(1) attribute.addValues(["a", "b"]) expect(attribute.value(3)).toBe("a") expect(attribute.isNumeric(3)).toBe(false) - expect(attribute.numeric(3)).toBeNaN() + expect(attribute.numValue(3)).toBeNaN() expect(attribute.type).toBe("categorical") attribute.setUserType("numeric") @@ -193,7 +193,7 @@ describe("Attribute", () => { attribute.addValue() expect(attribute.value(5)).toBe("") expect(attribute.isNumeric(5)).toBe(false) - expect(attribute.numeric(5)).toBeNaN() + expect(attribute.numValue(5)).toBeNaN() attribute.clearValues() expect(attribute.strValues.length).toBe(6) diff --git a/v3/src/models/data/attribute.ts b/v3/src/models/data/attribute.ts index bdfb52d03..e90b4c02b 100644 --- a/v3/src/models/data/attribute.ts +++ b/v3/src/models/data/attribute.ts @@ -243,17 +243,21 @@ export const Attribute = V2Model.named("Attribute").props({ return self.editable && !self.hasFormula }, value(index: number) { - return self.strValues[index] + const numValue = self.numValues[index] + return !isNaN(numValue) ? numValue : self.strValues[index] }, isNumeric(index: number) { return !isNaN(self.numValues[index]) }, - numeric(index: number) { + numValue(index: number) { return self.numValues[index] }, + strValue(index: number) { + return self.strValues[index] + }, boolean(index: number) { return ["true", "yes"].includes(self.strValues[index].toLowerCase()) || - (!isNaN(this.numeric(index)) ? this.numeric(index) !== 0 : false) + (!isNaN(this.numValue(index)) ? this.numValue(index) !== 0 : false) }, derive(name?: string) { return { id: self.id, name: name || self.name, values: [] } diff --git a/v3/src/models/data/data-set-collections.test.ts b/v3/src/models/data/data-set-collections.test.ts index 10ec5ef23..00f375759 100644 --- a/v3/src/models/data/data-set-collections.test.ts +++ b/v3/src/models/data/data-set-collections.test.ts @@ -202,7 +202,7 @@ describe("DataSet collections", () => { const parentCase = { ...parentCases[0], aId: "4" } data.setCaseValues([parentCase]) for (const caseId of data.collections[0].caseGroups[0].childItemIds) { - expect(data.getValue(caseId, "aId")).toBe("4") + expect(data.getValue(caseId, "aId")).toBe(4) } }) diff --git a/v3/src/models/data/data-set.test.ts b/v3/src/models/data/data-set.test.ts index 992b115e8..2fb41b090 100644 --- a/v3/src/models/data/data-set.test.ts +++ b/v3/src/models/data/data-set.test.ts @@ -257,11 +257,11 @@ test("DataSet basic functionality", () => { expect(dataset.items.length).toBe(1) expect(caseD4ID).toBeDefined() expect(dataset.attributes[0].value(0)).toBe("d") - expect(dataset.attributes[1].value(0)).toBe("4") - expect(dataset.attributes[1].numeric(0)).toBe(4) + expect(dataset.attributes[1].value(0)).toBe(4) + expect(dataset.attributes[1].numValue(0)).toBe(4) expect(dataset.getNumeric(caseD4ID, numAttrID)).toBe(4) expect(dataset.getNumericAtItemIndex(0, numAttrID)).toBe(4) - expect(dataset.getValueAtItemIndex(0, numAttrID)).toBe("4") + expect(dataset.getValueAtItemIndex(0, numAttrID)).toBe(4) expect(dataset.getStrValueAtItemIndex(0, numAttrID)).toBe("4") // add new case before first case @@ -275,11 +275,11 @@ test("DataSet basic functionality", () => { expect(dataset.nextItemID(caseC3ID)).toBe(caseD4ID) expect(dataset.items[1].__id__).toBe(caseD4ID) expect(dataset.attributes[0].value(0)).toBe("c") - expect(dataset.attributes[1].value(0)).toBe("3") - expect(dataset.attributes[1].numeric(0)).toBe(3) + expect(dataset.attributes[1].value(0)).toBe(3) + expect(dataset.attributes[1].numValue(0)).toBe(3) expect(dataset.getNumeric(caseC3ID, numAttrID)).toBe(3) expect(dataset.getNumericAtItemIndex(0, numAttrID)).toBe(3) - expect(dataset.getValueAtItemIndex(0, numAttrID)).toBe("3") + expect(dataset.getValueAtItemIndex(0, numAttrID)).toBe(3) // add multiple new cases before specified case dataset.addCases([{ str: "a", num: 1 }, { str: "b", num: 2 }], { before: caseC3ID, canonicalize: true }) @@ -287,9 +287,9 @@ test("DataSet basic functionality", () => { caseB2ID = dataset.items[1].__id__ expect(dataset.items.length).toBe(4) expect(dataset.attributes[0].value(0)).toBe("a") - expect(dataset.attributes[1].value(0)).toBe("1") + expect(dataset.attributes[1].value(0)).toBe(1) expect(dataset.attributes[0].value(1)).toBe("b") - expect(dataset.attributes[1].value(1)).toBe("2") + expect(dataset.attributes[1].value(1)).toBe(2) expect(dataset.getValue(caseA1ID, "foo")).toBeUndefined() expect(dataset.getValue("foo", "bar")).toBeUndefined() expect(dataset.getItem(caseA1ID, { canonical: false })).toEqual({ __id__: caseA1ID, str: "a", num: 1 }) @@ -321,9 +321,9 @@ test("DataSet basic functionality", () => { caseK2ID = dataset.items[4].__id__ expect(dataset.items.length).toBe(7) expect(dataset.attributes[0].value(3)).toBe("j") - expect(dataset.attributes[1].value(3)).toBe("1") + expect(dataset.attributes[1].value(3)).toBe(1) expect(dataset.attributes[0].value(4)).toBe("k") - expect(dataset.attributes[1].value(4)).toBe("2") + expect(dataset.attributes[1].value(4)).toBe(2) expect(dataset.getValue(caseJ1ID, "foo")).toBeUndefined() expect(dataset.getValue("foo", "bar")).toBeUndefined() expect(dataset.getItem(caseJ1ID, { canonical: false })).toEqual({ __id__: caseJ1ID, str: "j", num: 1 }) @@ -344,10 +344,10 @@ test("DataSet basic functionality", () => { dataset.setCaseValues(toCanonical(dataset, [{ __id__: caseB2ID, str: "B", num: 20 }, { __id__: caseC3ID, str: "C", num: 30 }])) expect(dataset.getValue(caseB2ID, strAttrID)).toBe("B") - expect(dataset.getValue(caseB2ID, numAttrID)).toBe("20") + expect(dataset.getValue(caseB2ID, numAttrID)).toBe(20) expect(dataset.getItem(caseB2ID, { canonical: false })).toEqual({ __id__: caseB2ID, str: "B", num: 20 }) expect(dataset.getValue(caseC3ID, strAttrID)).toBe("C") - expect(dataset.getValue(caseC3ID, numAttrID)).toBe("30") + expect(dataset.getValue(caseC3ID, numAttrID)).toBe(30) const mockConsoleWarn = jest.fn() const consoleSpy = jest.spyOn(console, "warn").mockImplementation((...args: any[]) => mockConsoleWarn(...args)) dataset.setCaseValues(toCanonical(dataset, [{ __id__: caseA1ID, foo: "bar" }])) @@ -457,7 +457,7 @@ test("Canonical case functionality", () => { expect(dataset.items.length).toBe(1) expect(caseD4ID).toBeDefined() expect(dataset.attributes[0].value(0)).toBe("d") - expect(dataset.attributes[1].value(0)).toBe("4") + expect(dataset.attributes[1].value(0)).toBe(4) // add new case before first case dataset.addCases([{ [strAttrID]: "c", [numAttrID]: 3 }], { before: caseD4ID }) @@ -469,7 +469,7 @@ test("Canonical case functionality", () => { expect(dataset.nextItemID(caseC3ID)).toBe(caseD4ID) expect(dataset.items[1].__id__).toBe(caseD4ID) expect(dataset.attributes[0].value(0)).toBe("c") - expect(dataset.attributes[1].value(0)).toBe("3") + expect(dataset.attributes[1].value(0)).toBe(3) // add multiple new cases dataset.addCases([{ [strAttrID]: "a", [numAttrID]: 1 }, @@ -478,9 +478,9 @@ test("Canonical case functionality", () => { caseB2ID = dataset.items[1].__id__ expect(dataset.items.length).toBe(4) expect(dataset.attributes[0].value(0)).toBe("a") - expect(dataset.attributes[1].numeric(0)).toBe(1) + expect(dataset.attributes[1].numValue(0)).toBe(1) expect(dataset.attributes[0].value(1)).toBe("b") - expect(dataset.attributes[1].numeric(1)).toBe(2) + expect(dataset.attributes[1].numValue(1)).toBe(2) expect(dataset.getItem(caseA1ID, { canonical: false })).toEqual({ __id__: caseA1ID, str: "a", num: 1 }) expect(dataset.getItem(caseB2ID, { canonical: false })).toEqual({ __id__: caseB2ID, str: "b", num: 2 }) expect(dataset.getItem(caseA1ID, { canonical: true })) diff --git a/v3/src/models/data/data-set.ts b/v3/src/models/data/data-set.ts index 24a9dcafa..cbc953e52 100644 --- a/v3/src/models/data/data-set.ts +++ b/v3/src/models/data/data-set.ts @@ -736,7 +736,7 @@ export const DataSet = V2Model.named("DataSet").props({ const item: ICase = { __id__: itemID } self.attributes.forEach((attr) => { const key = canonical ? attr.id : attr.name - item[key] = numeric && attr.isNumeric(index) ? attr.numeric(index) : attr.value(index) + item[key] = numeric ? attr.value(index) : attr.strValue(index) }) return item } @@ -818,7 +818,7 @@ export const DataSet = V2Model.named("DataSet").props({ } } const attr = self.getAttribute(attributeID) - return attr?.value(itemIndex) ?? "" + return attr?.strValue(itemIndex) ?? "" }, getNumeric(caseID: string, attributeID: string): number | undefined { const index = self.getItemIndexForCaseOrItem(caseID) @@ -833,7 +833,7 @@ export const DataSet = V2Model.named("DataSet").props({ } } const attr = self.getAttribute(attributeID) - return attr?.numeric(index) + return attr?.numValue(index) }, getItem, getItems, diff --git a/v3/src/models/formula/attribute-formula-adapter.test.ts b/v3/src/models/formula/attribute-formula-adapter.test.ts index bf491c247..9193f7705 100644 --- a/v3/src/models/formula/attribute-formula-adapter.test.ts +++ b/v3/src/models/formula/attribute-formula-adapter.test.ts @@ -41,7 +41,7 @@ describe("AttributeFormulaAdapter", () => { it("should store results in DataSet case values", () => { const { adapter, dataSet, attribute, context, extraMetadata } = getTestEnv() adapter.recalculateFormula(context, extraMetadata, "ALL_CASES") - expect(dataSet.getValueAtItemIndex(0, attribute.id)).toEqual("3") + expect(dataSet.getValueAtItemIndex(0, attribute.id)).toEqual(3) }) }) @@ -113,7 +113,7 @@ describe("AttributeFormulaAdapter", () => { expect(dataSet.getValueAtItemIndex(0, attribute.id)).toEqual("") dataSet.moveAttributeToNewCollection(dataSet.attrIDFromName("bar")!) - expect(dataSet.getValueAtItemIndex(0, attribute.id)).toEqual("3") // formula has been recalculated + expect(dataSet.getValueAtItemIndex(0, attribute.id)).toEqual(3) // formula has been recalculated }) }) }) diff --git a/v3/src/models/formula/functions/lookup-functions.test.ts b/v3/src/models/formula/functions/lookup-functions.test.ts index a9580768a..513e70889 100644 --- a/v3/src/models/formula/functions/lookup-functions.test.ts +++ b/v3/src/models/formula/functions/lookup-functions.test.ts @@ -46,13 +46,13 @@ describe("lookupBoundary", () => { describe("lookupByIndex", () => { it("returns the value at the given constant index (1-based)", () => { expect(evaluate("lookupByIndex('Mammals', 'Mammal', 2)")).toEqual("Asian Elephant") - expect(evaluate("lookupByIndex('Mammals', 'Mass', 2)")).toEqual("5000") + expect(evaluate("lookupByIndex('Mammals', 'Mass', 2)")).toEqual(5000) expect(evaluate("lookupByIndex('Cats', 'PadColor', 5)")).toEqual("pink") }) it("returns the value at the given evaluated index (1-based)", () => { expect(evaluate("lookupByIndex('Mammals', 'Mammal', caseIndex)", 1)).toEqual("Asian Elephant") - expect(evaluate("lookupByIndex('Mammals', 'Mass', caseIndex)", 1)).toEqual("5000") + expect(evaluate("lookupByIndex('Mammals', 'Mass', caseIndex)", 1)).toEqual(5000) expect(evaluate("lookupByIndex('Cats', 'PadColor', caseIndex)", 4)).toEqual("pink") }) diff --git a/v3/src/models/formula/functions/semi-aggregate-functions.test.ts b/v3/src/models/formula/functions/semi-aggregate-functions.test.ts index 9c1d6167e..b2767d999 100644 --- a/v3/src/models/formula/functions/semi-aggregate-functions.test.ts +++ b/v3/src/models/formula/functions/semi-aggregate-functions.test.ts @@ -8,10 +8,10 @@ describe("semiAggregateFunctions", () => { describe("prev", () => { it("should returns the prev value", () => { expect(evaluate("prev(LifeSpan)", 0)).toBe("") - expect(evaluate("prev(LifeSpan)", 1)).toBe("70") - expect(evaluate("prev(LifeSpan)", 2)).toBe("70") - expect(evaluate("prev(LifeSpan)", 3)).toBe("19") - expect(evaluate("prev(LifeSpan)", 4)).toBe("25") + expect(evaluate("prev(LifeSpan)", 1)).toBe(70) + expect(evaluate("prev(LifeSpan)", 2)).toBe(70) + expect(evaluate("prev(LifeSpan)", 3)).toBe(19) + expect(evaluate("prev(LifeSpan)", 4)).toBe(25) }) it("supports the default value", () => { @@ -23,15 +23,15 @@ describe("semiAggregateFunctions", () => { // When prev is evaluated with filter, it needs to be evaluated for each case using the same scope. // That's why evaluateForAllCases is used here. expect(evaluateForAllCases("prev(LifeSpan, 0, Diet = 'both')")).toEqual([ - 0, 0, 0, 0, 0, 0, "40", "40", "40", "40", "40", "40", "9", "9", "3", "80", "80", "80", "80", "5", "5", "12", - "20", "10", "10", "10", "7", + 0, 0, 0, 0, 0, 0, 40, 40, 40, 40, 40, 40, 9, 9, 3, 80, 80, 80, 80, 5, 5, 12, + 20, 10, 10, 10, 7 ]) }) it("supports recursive calls", () => { expect(evaluateForAllCases("prev(prev(LifeSpan))")).toEqual([ - "", "", "70", "70", "19", "25", "14", "40", "16", "40", "25", "16", "30", "9", "25", "3", "80", "20", "50", - "15", "5", "10", "12", "20", "10", "10", "5" + "", "", 70, 70, 19, 25, 14, 40, 16, 40, 25, 16, 30, 9, 25, 3, 80, 20, 50, + 15, 5, 10, 12, 20, 10, 10, 5 ]) }) @@ -71,7 +71,7 @@ describe("semiAggregateFunctions", () => { return formulaValue }) - const expectedResult = new Array(27).fill("70") + const expectedResult = new Array(27).fill(70) expectedResult[0] = 0 // default value expect(result).toEqual(expectedResult) @@ -83,10 +83,10 @@ describe("semiAggregateFunctions", () => { describe("next", () => { it("should returns the next value", () => { - expect(evaluate("next(LifeSpan)", 0)).toBe("70") - expect(evaluate("next(LifeSpan)", 1)).toBe("19") - expect(evaluate("next(LifeSpan)", 2)).toBe("25") - expect(evaluate("next(LifeSpan)", 25)).toBe("25") + expect(evaluate("next(LifeSpan)", 0)).toBe(70) + expect(evaluate("next(LifeSpan)", 1)).toBe(19) + expect(evaluate("next(LifeSpan)", 2)).toBe(25) + expect(evaluate("next(LifeSpan)", 25)).toBe(25) expect(evaluate("next(LifeSpan)", 26)).toBe("") // last case }) @@ -96,18 +96,18 @@ describe("semiAggregateFunctions", () => { }) it("supports the filter argument", () => { - expect(evaluate("next(LifeSpan, 0, Diet = 'both')", 0)).toBe("40") - expect(evaluate("next(LifeSpan, 0, Diet = 'both')", 1)).toBe("40") - expect(evaluate("next(LifeSpan, 0, Diet = 'both')", 5)).toBe("9") - expect(evaluate("next(LifeSpan, 0, Diet = 'both')", 6)).toBe("9") - expect(evaluate("next(LifeSpan, 0, Diet = 'both')", 11)).toBe("3") - expect(evaluate("next(LifeSpan, 0, Diet = 'both')", 12)).toBe("3") + expect(evaluate("next(LifeSpan, 0, Diet = 'both')", 0)).toBe(40) + expect(evaluate("next(LifeSpan, 0, Diet = 'both')", 1)).toBe(40) + expect(evaluate("next(LifeSpan, 0, Diet = 'both')", 5)).toBe(9) + expect(evaluate("next(LifeSpan, 0, Diet = 'both')", 6)).toBe(9) + expect(evaluate("next(LifeSpan, 0, Diet = 'both')", 11)).toBe(3) + expect(evaluate("next(LifeSpan, 0, Diet = 'both')", 12)).toBe(3) }) it("supports recursive calls", () => { expect(evaluateForAllCases("next(next(LifeSpan))")).toEqual([ - "19", "25", "14", "40", "16", "40", "25", "16", "30", "9", "25", "3", "80", "20", "50", "15", "5", "10", "12", - "20", "10", "10", "5", "7", "25", "", "" + 19, 25, 14, 40, 16, 40, 25, 16, 30, 9, 25, 3, 80, 20, 50, 15, 5, 10, 12, + 20, 10, 10, 5, 7, 25, "", "" ]) }) }) @@ -140,7 +140,7 @@ describe("semiAggregateFunctions", () => { return formulaValue }) - const expectedResult = new Array(27).fill("25") + const expectedResult = new Array(27).fill(25) expectedResult[26] = 0 // default value expect(result).toEqual(expectedResult) diff --git a/v3/src/models/formula/functions/string-functions.ts b/v3/src/models/formula/functions/string-functions.ts index f8b8b78d5..6f4dca591 100644 --- a/v3/src/models/formula/functions/string-functions.ts +++ b/v3/src/models/formula/functions/string-functions.ts @@ -251,7 +251,7 @@ export const stringFunctions = { }) const [textRefArg, dataSetTitleArg, wordAttributeNameArg, ratingAttributeNameArg] = args as [MathNode, LookupStringConstantArg, LookupStringConstantArg, LookupStringConstantArg] - const text = evaluateNode(textRefArg, scope) as string + const text = String(evaluateNode(textRefArg, scope)) const dataSetTitle = dataSetTitleArg?.value || "" const dataSet = scope.getDataSetByTitle(dataSetTitle) if (!dataSet) { @@ -269,7 +269,7 @@ export const stringFunctions = { const ratingAttribute = ratingAttributeName ? dataSet.getAttributeByName(ratingAttributeName) : undefined const wordRatingMap: Record = {} - wordAttribute.strValues.forEach((word, index) => wordRatingMap[word] = ratingAttribute?.numeric(index) ?? 1) + wordAttribute.strValues.forEach((word, index) => wordRatingMap[word] = ratingAttribute?.numValue(index) ?? 1) let result = 0 wordAttribute.strValues.forEach(word => { diff --git a/v3/src/models/formula/test-utils/formula-test-utils.test.ts b/v3/src/models/formula/test-utils/formula-test-utils.test.ts index 99b2ab2b2..abc92040b 100644 --- a/v3/src/models/formula/test-utils/formula-test-utils.test.ts +++ b/v3/src/models/formula/test-utils/formula-test-utils.test.ts @@ -20,9 +20,9 @@ describe("evaluate", () => { }) it("can evaluate case-dependant formulas when case pointer is provided", () => { - expect(evaluate("LifeSpan", 0)).toEqual("70") - expect(evaluate("LifeSpan", 10)).toEqual("30") - expect(evaluate("LifeSpan", 26)).toEqual("25") + expect(evaluate("LifeSpan", 0)).toEqual(70) + expect(evaluate("LifeSpan", 10)).toEqual(30) + expect(evaluate("LifeSpan", 26)).toEqual(25) }) it("evaluates formulas with global values", () => { @@ -31,7 +31,7 @@ describe("evaluate", () => { }) it("sets Mammals dataset as a local dataset", () => { - expect(evaluate("LifeSpan", 0)).toEqual("70") + expect(evaluate("LifeSpan", 0)).toEqual(70) // TailLength is an attribute from Cats dataset. expect(() => evaluate("TailLength", 0)).toThrow("Undefined symbol TailLength") }) @@ -49,8 +49,8 @@ describe("evaluateForAllCases", () => { it("can evaluate case-dependant formulas", () => { expect(evaluateForAllCases("LifeSpan")).toEqual([ - "70", "70", "19", "25", "14", "40", "16", "40", "25", "16", "30", "9", "25", "3", "80", "20", "50", "15", "5", - "10", "12", "20", "10", "10", "5", "7", "25" + 70, 70, 19, 25, 14, 40, 16, 40, 25, 16, 30, 9, 25, 3, 80, 20, 50, 15, 5, + 10, 12, 20, 10, 10, 5, 7, 25 ]) }) }) diff --git a/v3/src/v2/codap-v2-types.ts b/v3/src/v2/codap-v2-types.ts index f0f22a88d..7aacc7c3d 100644 --- a/v3/src/v2/codap-v2-types.ts +++ b/v3/src/v2/codap-v2-types.ts @@ -177,8 +177,7 @@ export interface ICodapV2GameContext extends Omit { // when exporting a v3 data set to v2 data context type DCNotYetExported = "flexibleGroupingChangeFlag" export interface ICodapV2DataContextV3 - extends SetOptional, DCNotYetExported> { - document: number | string + extends SetOptional, DCNotYetExported> { collections: ICodapV2CollectionV3[] } diff --git a/v3/src/v2/export-v2-document.test.ts b/v3/src/v2/export-v2-document.test.ts index ee542f626..501f8b5f2 100644 --- a/v3/src/v2/export-v2-document.test.ts +++ b/v3/src/v2/export-v2-document.test.ts @@ -1,6 +1,14 @@ import { kCalculatorTileType, kV2CalculatorDGType } from "../components/calculator/calculator-defs" import { createCodapDocument } from "../models/codap/create-codap-document" +import { DataBroker } from "../models/data/data-broker" +import { DataSet } from "../models/data/data-set" +import { GlobalValue } from "../models/global/global-value" +import { getSharedDataSets } from "../models/shared/shared-data-utils" +import { getGlobalValueManager, getSharedModelManager } from "../models/tiles/tile-environment" +import { toV2Id } from "../utilities/codap-utils" +import { ICodapV2DataContext } from "./codap-v2-types" import { exportV2Document } from "./export-v2-document" + import "../components/calculator/calculator-registration" jest.mock("../../build_number.json", () => ({ @@ -49,4 +57,63 @@ describe("exportV2Document", () => { expect(components.length).toBe(1) expect(components[0].type).toBe(kV2CalculatorDGType) }) + + it("exports a document with a single global value", () => { + const doc = createCodapDocument() + const globalValueManager = getGlobalValueManager(getSharedModelManager(doc)) + globalValueManager?.addValue(GlobalValue.create({ name: "v1", value: 1 })) + const { globalValues, ...others } = exportV2Document(doc) + expect(others).toEqual({ + type: "DG.Document", + id: 1, + guid: 1, + name: doc.title, + appName: "DG", + appVersion: "3.0.0-test", + appBuildNum: `${1234}`, + metadata: {}, + contexts: [], + components: [] + }) + expect(globalValues.length).toBe(1) + expect(globalValues[0].name).toBe("v1") + expect(globalValues[0].value).toBe(1) + }) + + it("exports a document with a single data set", () => { + const doc = createCodapDocument() + const sharedModelManager = getSharedModelManager(doc) + const dataBroker = new DataBroker({ sharedModelManager }) + const dataSet = DataSet.create({ collections: [{ name: "Cases" }]}) + const attr = dataSet.addAttribute({ name: "A" }) + dataSet.addCases([{ [attr.id]: 1 }, { [attr.id]: 2 }, { [attr.id]: 3 }]) + dataBroker.addDataSet(dataSet) + expect(getSharedDataSets(doc).length).toBe(1) + expect(dataSet.itemIds.length).toBe(3) + + const { contexts, ...others } = exportV2Document(doc) + const context = contexts[0] as ICodapV2DataContext + expect(others).toEqual({ + type: "DG.Document", + id: 1, + guid: 1, + name: doc.title, + appName: "DG", + appVersion: "3.0.0-test", + appBuildNum: `${1234}`, + metadata: {}, + components: [], + globalValues: [] + }) + expect(context.guid).toBe(toV2Id(dataSet.id)) + const collection = context.collections[0] + expect(collection.guid).toBe(toV2Id(dataSet.collections[0].id)) + const cases = collection.cases + expect(cases[0].guid).toBe(toV2Id(dataSet.collections[0].cases[0].__id__)) + expect(cases[0].values).toEqual({ A: 1 }) + expect(cases[1].guid).toBe(toV2Id(dataSet.collections[0].cases[1].__id__)) + expect(cases[1].values).toEqual({ A: 2 }) + expect(cases[2].guid).toBe(toV2Id(dataSet.collections[0].cases[2].__id__)) + expect(cases[2].values).toEqual({ A: 3 }) + }) }) diff --git a/v3/src/v2/export-v2-document.ts b/v3/src/v2/export-v2-document.ts index b049af060..93a26e103 100644 --- a/v3/src/v2/export-v2-document.ts +++ b/v3/src/v2/export-v2-document.ts @@ -1,10 +1,13 @@ import build from "../../build_number.json" import pkg from "../../package.json" +import { convertDataSetToV2 } from "../data-interactive/data-interactive-type-utils" import { IDocumentModel } from "../models/document/document" import { isFreeTileRow } from "../models/document/free-tile-row" -import { getSharedModelManager } from "../models/tiles/tile-environment" +import { getSharedDataSets } from "../models/shared/shared-data-utils" +import { getGlobalValueManager, getSharedModelManager } from "../models/tiles/tile-environment" +import { toV2Id } from "../utilities/codap-utils" import { exportV2Component } from "./codap-v2-tile-exporters" -import { CodapV2Component, ICodapV2DocumentJson } from "./codap-v2-types" +import { CodapV2Component, ICodapV2DataContext, ICodapV2DocumentJson } from "./codap-v2-types" interface IV2DocumentExportOptions { filename?: string @@ -22,6 +25,19 @@ export function exportV2Document(document: IDocumentModel, options?: IV2Document if (component) components.push(component) }) + // export the data contexts + const contexts: ICodapV2DataContext[] = [] + getSharedDataSets(document).forEach(sharedData => { + const data = convertDataSetToV2(sharedData.dataSet, true) as ICodapV2DataContext + contexts.push(data) + }) + + // export the global values + const globalValueManager = getGlobalValueManager(getSharedModelManager(document)) + const globalValues = Array.from(globalValueManager?.globals.values() ?? []).map(global => { + return { guid: toV2Id(global.id), name: global.name, value: global.value } + }) + return { type: "DG.Document", id: 1, @@ -32,7 +48,7 @@ export function exportV2Document(document: IDocumentModel, options?: IV2Document appBuildNum: `${build.buildNumber}`, metadata: {}, components, - contexts: [], - globalValues: [] + contexts, + globalValues } }