diff --git a/packages/actor-bindings-aggregator-factory-average/lib/AverageAggregator.ts b/packages/actor-bindings-aggregator-factory-average/lib/AverageAggregator.ts index c0471ad7..a248ac71 100644 --- a/packages/actor-bindings-aggregator-factory-average/lib/AverageAggregator.ts +++ b/packages/actor-bindings-aggregator-factory-average/lib/AverageAggregator.ts @@ -7,6 +7,7 @@ import type * as RDF from '@rdfjs/types'; import { termToString } from 'rdf-string'; interface IAverageState { + index: Map; sum: Eval.NumericLiteral; count: number; } @@ -30,29 +31,44 @@ export class AverageAggregator extends AggregateEvaluator implements IBindingsAg return Eval.typedLiteral('0', Eval.TypeURL.XSD_INTEGER); } - public putTerm(term: RDF.Term): void { + protected putTerm(term: RDF.Term): void { + const hash = termToString(term); + const value = this.termToNumericOrError(term); if (this.state === undefined) { - const sum = this.termToNumericOrError(term); - this.state = { sum, count: 1 }; - } else { - const internalTerm = this.termToNumericOrError(term); - this.state.sum = this.additionFunction - .applyOnTerms([ this.state.sum, internalTerm ], this.evaluator); - this.state.count++; + this.state = { index: new Map([[ hash, 1 ]]), sum: value, count: 1 }; + return; } + this.state.index.set(hash, (this.state.index.get(hash) ?? 0) + 1); + this.state.sum = this.additionFunction + .applyOnTerms([ this.state.sum, value ], this.evaluator); + this.state.count++; } protected removeTerm(term: RDF.Term): void { + const hash = termToString(term); + const value = this.termToNumericOrError(term); if (this.state === undefined) { throw new Error(`Cannot remove term ${termToString(term)} from empty average aggregator`); } - const internalTerm = this.termToNumericOrError(term); + const count = this.state.index.get(hash); + if (count === undefined) { + throw new Error(`Cannot remove term ${termToString(term)} that was not added to average aggregator`); + } + if (count === 1) { + this.state.index.delete(hash); + if (this.state.count === 1) { + this.state = undefined; + return; + } + } else { + this.state.index.set(hash, count - 1); + } this.state.sum = this.subtractionFunction - .applyOnTerms([ this.state.sum, internalTerm ], this.evaluator); + .applyOnTerms([ this.state.sum, value ], this.evaluator); this.state.count--; } - public termResult(): RDF.Term | undefined { + protected termResult(): RDF.Term | undefined { if (this.state === undefined) { return this.emptyValue(); } diff --git a/packages/actor-bindings-aggregator-factory-average/test/ActorBindingsAggregatorFactoryAverage-test.ts b/packages/actor-bindings-aggregator-factory-average/test/ActorBindingsAggregatorFactoryAverage-test.ts index 50b36cbb..1db68ed2 100644 --- a/packages/actor-bindings-aggregator-factory-average/test/ActorBindingsAggregatorFactoryAverage-test.ts +++ b/packages/actor-bindings-aggregator-factory-average/test/ActorBindingsAggregatorFactoryAverage-test.ts @@ -1,15 +1,16 @@ import { ActorFunctionFactoryTermAddition } from '@comunica/actor-function-factory-term-addition'; import { ActorFunctionFactoryTermDivision } from '@comunica/actor-function-factory-term-division'; +import { ActorFunctionFactoryTermSubtraction } from '@comunica/actor-function-factory-term-subtraction'; import type { MediatorExpressionEvaluatorFactory } from '@comunica/bus-expression-evaluator-factory'; import type { MediatorFunctionFactory } from '@comunica/bus-function-factory'; -import { createFuncMediator } from '@comunica/bus-function-factory/test/util'; import { Bus } from '@comunica/core'; import type { IActionContext } from '@comunica/types'; import { + createFuncMediator, getMockEEActionContext, getMockMediatorExpressionEvaluatorFactory, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; +} from '@incremunica/dev-tools'; import { ActorBindingsAggregatorFactoryAverage } from '../lib'; import '@comunica/utils-jest'; @@ -26,6 +27,7 @@ describe('ActorBindingsAggregatorFactoryAverage', () => { mediatorExpressionEvaluatorFactory = getMockMediatorExpressionEvaluatorFactory(); mediatorFunctionFactory = createFuncMediator([ args => new ActorFunctionFactoryTermAddition(args), + args => new ActorFunctionFactoryTermSubtraction(args), args => new ActorFunctionFactoryTermDivision(args), ], {}); diff --git a/packages/actor-bindings-aggregator-factory-average/test/AverageAggregator-test.ts b/packages/actor-bindings-aggregator-factory-average/test/AverageAggregator-test.ts index c7edf8c8..413bcf42 100644 --- a/packages/actor-bindings-aggregator-factory-average/test/AverageAggregator-test.ts +++ b/packages/actor-bindings-aggregator-factory-average/test/AverageAggregator-test.ts @@ -1,12 +1,16 @@ import { ActorFunctionFactoryTermAddition } from '@comunica/actor-function-factory-term-addition'; import { ActorFunctionFactoryTermDivision } from '@comunica/actor-function-factory-term-division'; +import { ActorFunctionFactoryTermSubtraction } from '@comunica/actor-function-factory-term-subtraction'; import type { ActorExpressionEvaluatorFactory } from '@comunica/bus-expression-evaluator-factory'; import type { MediatorFunctionFactory } from '@comunica/bus-function-factory'; -import { createFuncMediator } from '@comunica/bus-function-factory/test/util'; import { KeysInitQuery } from '@comunica/context-entries'; import type { IActionContext } from '@comunica/types'; +import type { Bindings } from '@comunica/utils-bindings-factory'; import { SparqlOperator } from '@comunica/utils-expression-evaluator'; +import type { AggregateEvaluator, IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +import { KeysBindings } from '@incremunica/context-entries'; import { + createFuncMediator, BF, decimal, DF, @@ -16,12 +20,11 @@ import { getMockEEFactory, int, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; -import type { IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +} from '@incremunica/dev-tools'; import type * as RDF from '@rdfjs/types'; -import { AverageAggregator } from '../lib/AverageAggregator'; +import { AverageAggregator } from '../lib'; -async function runAggregator(aggregator: IBindingsAggregator, input: RDF.Bindings[]): Promise { +async function runAggregator(aggregator: IBindingsAggregator, input: Bindings[]): Promise { for (const bindings of input) { await aggregator.putBindings(bindings); } @@ -46,11 +49,17 @@ async function createAggregator({ expressionEvaluatorFactory, context, distinct, functionName: SparqlOperator.ADDITION, requireTermExpression: true, }), + await mediatorFunctionFactory.mediate({ + context, + functionName: SparqlOperator.SUBTRACTION, + requireTermExpression: true, + }), await mediatorFunctionFactory.mediate({ context, functionName: SparqlOperator.DIVISION, requireTermExpression: true, }), + true, ); } @@ -63,6 +72,7 @@ describe('AverageAggregator', () => { mediatorFunctionFactory = createFuncMediator([ args => new ActorFunctionFactoryTermAddition(args), args => new ActorFunctionFactoryTermDivision(args), + args => new ActorFunctionFactoryTermSubtraction(args), ], {}); expressionEvaluatorFactory = getMockEEFactory({ mediatorFunctionFactory, @@ -83,50 +93,133 @@ describe('AverageAggregator', () => { }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), float('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), - BF.bindings([[ DF.variable('x'), int('4') ]]), + BF.bindings([[ DF.variable('x'), float('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(float('2.5')); }); + it('a list of bindings 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), float('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(float('2')); + }); + it('with respect to empty input', async() => { await expect(runAggregator(aggregator, [])).resolves.toEqual(int('0')); }); - it('with respect to type promotion and subtype substitution', async() => { + it('should error on a deletion if aggregator empty', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "2"^^http://www.w3.org/2001/XMLSchema#integer from empty average aggregator'), + ); + }); + + it('should error on a deletion that has not been added', async() => { const input = [ - BF.bindings([[ DF.variable('x'), DF.literal('1', DF.namedNode('http://www.w3.org/2001/XMLSchema#byte')) ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), float('3') ]]), - BF.bindings([[ DF.variable('x'), DF.literal('4', DF.namedNode('http://www.w3.org/2001/XMLSchema#nonNegativeInteger')) ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "2"^^http://www.w3.org/2001/XMLSchema#integer that was not added to average aggregator'), + ); + }); + + it('delete everything', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves + .toEqual((aggregator).emptyValueTerm()); + }); + + it('with respect to type promotion and subtype substitution 1', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), DF.literal('1', DF.namedNode('http://www.w3.org/2001/XMLSchema#byte')) ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), float('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('4', DF.namedNode('http://www.w3.org/2001/XMLSchema#nonNegativeInteger')) ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(float('2.5')); }); - it('with respect to type preservation', async() => { + it('with respect to type promotion and subtype substitution 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), DF.literal('1', DF.namedNode('http://www.w3.org/2001/XMLSchema#byte')) ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), float('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('4', DF.namedNode('http://www.w3.org/2001/XMLSchema#nonNegativeInteger')) ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('1', DF.namedNode('http://www.w3.org/2001/XMLSchema#byte')) ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), float('3') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + await expect(runAggregator(aggregator, input)).resolves.toEqual(float('3')); + }); + + it('with respect to type preservation 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), - BF.bindings([[ DF.variable('x'), int('4') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(decimal('2.5')); }); - it('with respect to type promotion 2', async() => { + it('with respect to type preservation 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + await expect(runAggregator(aggregator, input)).resolves.toEqual(decimal('3.5')); + }); + + it('with respect to type promotion 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), double('1000') ]]), - BF.bindings([[ DF.variable('x'), int('2000') ]]), - BF.bindings([[ DF.variable('x'), float('3000') ]]), - BF.bindings([[ DF.variable('x'), double('4000') ]]), + BF.bindings([[ DF.variable('x'), double('1000') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2000') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), float('3000') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), double('4000') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(double('2.5E3')); }); + + it('with respect to type promotion 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), double('1000') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2000') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), float('3000') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), double('4000') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), float('3000') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), double('1000') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + await expect(runAggregator(aggregator, input)).resolves.toEqual(double('3.0E3')); + }); }); describe('distinctive avg', () => { @@ -141,12 +234,31 @@ describe('AverageAggregator', () => { }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(decimal('1.25')); + }); + + it('a list of bindings 2', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('1') ], [ DF.variable('y'), int('1') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(decimal('1.5')); diff --git a/packages/actor-bindings-aggregator-factory-count/lib/CountAggregator.ts b/packages/actor-bindings-aggregator-factory-count/lib/CountAggregator.ts index 5e996db6..6784ac2b 100644 --- a/packages/actor-bindings-aggregator-factory-count/lib/CountAggregator.ts +++ b/packages/actor-bindings-aggregator-factory-count/lib/CountAggregator.ts @@ -6,7 +6,7 @@ import type * as RDF from '@rdfjs/types'; import { termToString } from 'rdf-string'; export class CountAggregator extends AggregateEvaluator implements IBindingsAggregator { - private state: number | undefined = undefined; + private state: Map | undefined = undefined; public constructor(evaluator: IExpressionEvaluator, distinct: boolean, throwError?: boolean) { super(evaluator, distinct, throwError); } @@ -15,24 +15,45 @@ export class CountAggregator extends AggregateEvaluator implements IBindingsAggr return typedLiteral('0', TypeURL.XSD_INTEGER); } - protected putTerm(_: RDF.Term): void { + protected putTerm(term: RDF.Term): void { + const hash = termToString(term); if (this.state === undefined) { - this.state = 0; + this.state = new Map([[ hash, 1 ]]); + return; } - this.state++; + this.state.set(hash, (this.state.get(hash) ?? 0) + 1); } protected removeTerm(term: RDF.Term): void { + const hash = termToString(term); if (this.state === undefined) { - throw new Error(`Cannot remove term ${termToString(term)} from empty count aggregator`); + throw new Error(`Cannot remove term ${hash} from empty count aggregator`); } - this.state--; + const count = this.state.get(hash); + if (count === undefined) { + throw new Error(`Cannot remove term ${hash} that was not added to count aggregator`); + } + if (count === 1) { + this.state.delete(hash); + if (this.state.size === 0) { + this.state = undefined; + } + return; + } + this.state.set(hash, count - 1); } protected termResult(): RDF.Term | undefined { if (this.state === undefined) { return this.emptyValue(); } - return typedLiteral(String(this.state), TypeURL.XSD_INTEGER); + if (this.distinct) { + return typedLiteral(String(this.state.size), TypeURL.XSD_INTEGER); + } + let value = 0; + for (const count of this.state.values()) { + value += count; + } + return typedLiteral(String(value), TypeURL.XSD_INTEGER); } } diff --git a/packages/actor-bindings-aggregator-factory-count/test/ActorBindingsAggregatorFactoryCount-test.ts b/packages/actor-bindings-aggregator-factory-count/test/ActorBindingsAggregatorFactoryCount-test.ts index 34a4a8dd..51d7012c 100644 --- a/packages/actor-bindings-aggregator-factory-count/test/ActorBindingsAggregatorFactoryCount-test.ts +++ b/packages/actor-bindings-aggregator-factory-count/test/ActorBindingsAggregatorFactoryCount-test.ts @@ -7,7 +7,7 @@ import { getMockEEActionContext, getMockMediatorExpressionEvaluatorFactory, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; +} from '@incremunica/dev-tools'; import { Algebra } from 'sparqlalgebrajs'; import { Wildcard } from 'sparqljs'; import { ActorBindingsAggregatorFactoryCount } from '../lib'; diff --git a/packages/actor-bindings-aggregator-factory-count/test/CountAggregator-test.ts b/packages/actor-bindings-aggregator-factory-count/test/CountAggregator-test.ts index 92a86fff..fc6b4f74 100644 --- a/packages/actor-bindings-aggregator-factory-count/test/CountAggregator-test.ts +++ b/packages/actor-bindings-aggregator-factory-count/test/CountAggregator-test.ts @@ -1,5 +1,8 @@ import type { ActorExpressionEvaluatorFactory } from '@comunica/bus-expression-evaluator-factory'; import type { IActionContext } from '@comunica/types'; +import type { Bindings } from '@comunica/utils-bindings-factory'; +import type { AggregateEvaluator, IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +import { KeysBindings } from '@incremunica/context-entries'; import { BF, DF, @@ -7,12 +10,11 @@ import { getMockEEFactory, int, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; -import type { IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +} from '@incremunica/dev-tools'; import type * as RDF from '@rdfjs/types'; import { CountAggregator } from '../lib'; -async function runAggregator(aggregator: IBindingsAggregator, input: RDF.Bindings[]): Promise { +async function runAggregator(aggregator: IBindingsAggregator, input: Bindings[]): Promise { for (const bindings of input) { await aggregator.putBindings(bindings); } @@ -30,6 +32,7 @@ async function createAggregator({ expressionEvaluatorFactory, context, distinct context, }, undefined), distinct, + true, ); } @@ -50,17 +53,64 @@ describe('CountAggregator', () => { aggregator = await createAggregator({ expressionEvaluatorFactory, context, distinct: false }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), - BF.bindings([[ DF.variable('x'), int('4') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('4')); }); + it('a list of bindings 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('2')); + }); + + it('should error on a deletion if aggregator empty', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "2"^^http://www.w3.org/2001/XMLSchema#integer from empty count aggregator'), + ); + }); + + it('should error on a deletion that has not been added', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "2"^^http://www.w3.org/2001/XMLSchema#integer that was not added to count aggregator'), + ); + }); + + it('delete everything', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves + .toEqual((aggregator).emptyValueTerm()); + }); + it('with respect to empty input', async() => { await expect(runAggregator(aggregator, [])).resolves.toEqual(int('0')); }); @@ -73,17 +123,55 @@ describe('CountAggregator', () => { aggregator = await createAggregator({ expressionEvaluatorFactory, context, distinct: true }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('2')); + }); + + it('a list of bindings 2', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('1') ], [ DF.variable('y'), int('1') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('2')); }); + it('a list of bindings 3', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('1')); + }); + it('with respect to empty input', async() => { await expect(runAggregator(aggregator, [])).resolves.toEqual(int('0')); }); diff --git a/packages/actor-bindings-aggregator-factory-group-concat/lib/GroupConcatAggregator.ts b/packages/actor-bindings-aggregator-factory-group-concat/lib/GroupConcatAggregator.ts index 57f1ee4b..f01f41da 100644 --- a/packages/actor-bindings-aggregator-factory-group-concat/lib/GroupConcatAggregator.ts +++ b/packages/actor-bindings-aggregator-factory-group-concat/lib/GroupConcatAggregator.ts @@ -5,10 +5,13 @@ import { AggregateEvaluator } from '@incremunica/bus-bindings-aggregator-factory import type * as RDF from '@rdfjs/types'; import { termToString } from 'rdf-string'; +interface IGroupConcatStateValue { + term: RDF.Term; + count: number; +} + export class GroupConcatAggregator extends AggregateEvaluator implements IBindingsAggregator { - private state: Map | Set | undefined = undefined; - private lastLanguageValid = true; - private lastLanguage: string | undefined = undefined; + private state: Map | undefined = undefined; private readonly separator: string; public constructor( @@ -26,63 +29,73 @@ export class GroupConcatAggregator extends AggregateEvaluator implements IBindin return Eval.typedLiteral('', Eval.TypeURL.XSD_STRING); } - public putTerm(term: RDF.Term): void { + protected putTerm(term: RDF.Term): void { + const hash = termToString(term); if (this.state === undefined) { - if (this.distinct) { - this.state = new Set([ term.value ]); - } else { - this.state = new Map([[ term.value, 1 ]]); - } - if (term.termType === 'Literal') { - this.lastLanguage = term.language; - } + this.state = new Map([[ hash, { term, count: 1 }]]); + return; + } + const stateValue = this.state.get(hash); + if (stateValue === undefined) { + this.state.set(hash, { term, count: 1 }); } else { - if (this.distinct) { - (> this.state).add(term.value); - } else { - (> this.state) - .set(term.value, ((> this.state).get(term.value) ?? 0) + 1); - } - if (this.lastLanguageValid && term.termType === 'Literal' && this.lastLanguage !== term.language) { - this.lastLanguageValid = false; - this.lastLanguage = undefined; - } + stateValue.count++; } } protected removeTerm(term: RDF.Term): void { + const hash = termToString(term); if (this.state === undefined) { throw new Error(`Cannot remove term ${termToString(term)} from empty concat aggregator`); } - if (this.distinct) { - const count = (> this.state).get(term.value); - if (count === undefined) { - throw new Error(`Cannot remove term ${termToString(term)} that was not added to concat aggregator`); - } - if (count === 1) { - this.state.delete(term.value); - } else { - (> this.state).set(term.value, count - 1); - } - } else if (!this.state.delete(term.value)) { + const stateValue = this.state.get(hash); + if (stateValue === undefined) { throw new Error(`Cannot remove term ${termToString(term)} that was not added to concat aggregator`); } + if (stateValue.count === 1) { + this.state.delete(hash); + if (this.state.size === 0) { + this.state = undefined; + } + return; + } + stateValue.count--; } - public termResult(): RDF.Term | undefined { + protected termResult(): RDF.Term | undefined { if (this.state === undefined) { return this.emptyValue(); } let resultString: string; if (this.distinct) { - resultString = [ ...(> this.state) - .entries() ].map(([ value, count ]) => value.repeat(count)).join(this.separator); + resultString = [ ...this.state.values() ].map(stateValue => stateValue.term.value).join(this.separator); } else { - resultString = [ ...this.state.keys() ].join(this.separator); + resultString = ''; + for (const stateValue of this.state.values()) { + for (let i = 0; i < stateValue.count; i++) { + if (resultString.length > 0) { + resultString += this.separator; + } + resultString += stateValue.term.value; + } + } + } + let language = ''; + for (const stateValue of this.state.values()) { + if (stateValue.term.termType !== 'Literal') { + return Eval.typedLiteral(resultString, Eval.TypeURL.XSD_STRING); + } + if (!language) { + language = stateValue.term.language; + continue; + } + if (language !== stateValue.term.language) { + return Eval.typedLiteral(resultString, Eval.TypeURL.XSD_STRING); + } } - if (this.lastLanguageValid && this.lastLanguage) { - return Eval.langString(resultString, this.lastLanguage).toRDF(this.dataFactory); + if (!language) { + return Eval.typedLiteral(resultString, Eval.TypeURL.XSD_STRING); } - return Eval.typedLiteral(resultString, Eval.TypeURL.XSD_STRING); + return Eval.langString(resultString, language).toRDF(this.dataFactory); } } diff --git a/packages/actor-bindings-aggregator-factory-group-concat/test/ActorBindingsAggregatorFactoryGroupConcat-test.ts b/packages/actor-bindings-aggregator-factory-group-concat/test/ActorBindingsAggregatorFactoryGroupConcat-test.ts index bae0cc6b..6979dec3 100644 --- a/packages/actor-bindings-aggregator-factory-group-concat/test/ActorBindingsAggregatorFactoryGroupConcat-test.ts +++ b/packages/actor-bindings-aggregator-factory-group-concat/test/ActorBindingsAggregatorFactoryGroupConcat-test.ts @@ -7,7 +7,7 @@ import { getMockEEActionContext, getMockMediatorExpressionEvaluatorFactory, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; +} from '@incremunica/dev-tools'; import { ArrayIterator } from 'asynciterator'; import { ActorBindingsAggregatorFactoryGroupConcat } from '../lib'; import '@comunica/utils-jest'; diff --git a/packages/actor-bindings-aggregator-factory-group-concat/test/GroupConcatAggregator-test.ts b/packages/actor-bindings-aggregator-factory-group-concat/test/GroupConcatAggregator-test.ts index 67578795..0c542d76 100644 --- a/packages/actor-bindings-aggregator-factory-group-concat/test/GroupConcatAggregator-test.ts +++ b/packages/actor-bindings-aggregator-factory-group-concat/test/GroupConcatAggregator-test.ts @@ -1,6 +1,9 @@ import type { ActorExpressionEvaluatorFactory } from '@comunica/bus-expression-evaluator-factory'; import { KeysInitQuery } from '@comunica/context-entries'; import type { IActionContext } from '@comunica/types'; +import type { Bindings } from '@comunica/utils-bindings-factory'; +import type { AggregateEvaluator, IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +import { KeysBindings } from '@incremunica/context-entries'; import { BF, DF, @@ -8,12 +11,11 @@ import { getMockEEFactory, int, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; -import type { IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +} from '@incremunica/dev-tools'; import type * as RDF from '@rdfjs/types'; -import { GroupConcatAggregator } from '../lib/GroupConcatAggregator'; +import { GroupConcatAggregator } from '../lib'; -async function runAggregator(aggregator: IBindingsAggregator, input: RDF.Bindings[]): Promise { +async function runAggregator(aggregator: IBindingsAggregator, input: Bindings[]): Promise { for (const bindings of input) { await aggregator.putBindings(bindings); } @@ -34,6 +36,7 @@ async function createAggregator({ expressionEvaluatorFactory, context, distinct, distinct, context.getSafe(KeysInitQuery.dataFactory), separator, + true, ); } describe('CountAggregator', () => { @@ -53,42 +56,168 @@ describe('CountAggregator', () => { aggregator = await createAggregator({ expressionEvaluatorFactory, context, distinct: false }); }); - it('a list of bindings', async() => { + it('immediate deletion', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), - BF.bindings([[ DF.variable('x'), int('4') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "1"^^http://www.w3.org/2001/XMLSchema#integer from empty concat aggregator'), + ); + }); + + it('delete never seen value', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('10') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "10"^^http://www.w3.org/2001/XMLSchema#integer that was not added to concat aggregator'), + ); + }); + + it('delete everything', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves + .toEqual((aggregator).emptyValueTerm()); + }); + + it('delete everything with a list of different language strings', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), DF.literal('a', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('b', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('c', 'nl') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('d', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('a', 'en') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), DF.literal('b', 'en') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), DF.literal('c', 'nl') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), DF.literal('d', 'en') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves + .toEqual((aggregator).emptyValueTerm()); + }); + + it('a list of bindings 1', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(DF.literal('1 2 3 4')); }); + it('a list of bindings 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(DF.literal('2 4')); + }); + + it('a list of bindings with duplicates 1', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(DF.literal('1 2 2 3 4')); + }); + + it('a list of bindings with duplicates 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(DF.literal('1 2 3')); + }); + it('with respect to empty input', async() => { await expect(runAggregator(aggregator, [])).resolves.toEqual(DF.literal('')); }); - it('with a list of language strings', async() => { + it('with a list of language strings 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), DF.literal('a', 'en') ]]), - BF.bindings([[ DF.variable('x'), DF.literal('b', 'en') ]]), - BF.bindings([[ DF.variable('x'), DF.literal('c', 'en') ]]), - BF.bindings([[ DF.variable('x'), DF.literal('d', 'en') ]]), + BF.bindings([[ DF.variable('x'), DF.literal('a', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('b', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('c', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('d', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(DF.literal('a b c d', 'en')); }); + it('with a list of language strings 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), DF.literal('a', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('b', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('c', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('d', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('b', 'en') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), DF.literal('d', 'en') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(DF.literal('a c', 'en')); + }); + it('with a list of different language strings', async() => { const input = [ - BF.bindings([[ DF.variable('x'), DF.literal('a', 'en') ]]), - BF.bindings([[ DF.variable('x'), DF.literal('b', 'en') ]]), - BF.bindings([[ DF.variable('x'), DF.literal('c', 'nl') ]]), - BF.bindings([[ DF.variable('x'), DF.literal('d', 'en') ]]), + BF.bindings([[ DF.variable('x'), DF.literal('a', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('b', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('c', 'nl') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('d', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(DF.literal('a b c d')); }); + + it('with a list of different language strings 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), DF.literal('a', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('b', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('c', 'nl') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('d', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('c', 'nl') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(DF.literal('a b d', 'en')); + }); + + it('with a list of different language strings 3', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), DF.literal('a', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('b', 'en') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.namedNode('c') ]]).setContextEntry(KeysBindings.isAddition, true), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(DF.literal('a b c')); + }); }); describe('with custom separator', () => { @@ -100,10 +229,10 @@ describe('CountAggregator', () => { it('uses separator', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), - BF.bindings([[ DF.variable('x'), int('4') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(DF.literal('1;2;3;4')); @@ -117,17 +246,63 @@ describe('CountAggregator', () => { aggregator = await createAggregator({ expressionEvaluatorFactory, context, distinct: true }); }); - it('a list of bindings', async() => { + it('immediate deletion', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "1"^^http://www.w3.org/2001/XMLSchema#integer from empty concat aggregator'), + ); + }); + + it('never seen value', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('10') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "10"^^http://www.w3.org/2001/XMLSchema#integer that was not added to concat aggregator'), + ); + }); + + it('a list of bindings 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('1') ], [ DF.variable('y'), int('1') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(DF.literal('1 2')); }); + it('a list of bindings 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(DF.literal('1')); + }); + it('with respect to empty input', async() => { await expect(runAggregator(aggregator, [])).resolves.toEqual(DF.literal('')); }); diff --git a/packages/actor-bindings-aggregator-factory-max/lib/MaxAggregator.ts b/packages/actor-bindings-aggregator-factory-max/lib/MaxAggregator.ts index 01a603d7..034f57ec 100644 --- a/packages/actor-bindings-aggregator-factory-max/lib/MaxAggregator.ts +++ b/packages/actor-bindings-aggregator-factory-max/lib/MaxAggregator.ts @@ -7,8 +7,10 @@ import AVLTree from 'avl'; import type { Term } from 'n3'; import { termToString } from 'rdf-string'; +// TODO [2025-04-01]: Make a tree implementation that has constant time complexity for max and min +// & has a remove function for a node (not only a key) export class MaxAggregator extends AggregateEvaluator implements IBindingsAggregator { - private state: AVLTree | undefined = undefined; + private state: AVLTree | undefined = undefined; public constructor( evaluator: IExpressionEvaluator, distinct: boolean, @@ -18,24 +20,26 @@ export class MaxAggregator extends AggregateEvaluator implements IBindingsAggreg super(evaluator, distinct, throwError); } - public putTerm(term: RDF.Term): void { + protected putTerm(term: RDF.Term): void { if (term.termType !== 'Literal') { throw new Error(`Term with value ${term.value} has type ${term.termType} and is not a literal`); } if (this.state === undefined) { - this.state = new AVLTree((a, b) => this.orderByEvaluator.orderTypes(a, b), this.distinct); + this.state = new AVLTree((a, b) => this.orderByEvaluator.orderTypes(a, b)); } - this.state.insert(term, term); + this.state.insert(term); } protected removeTerm(term: Term): void { if (this.state === undefined) { throw new Error(`Cannot remove term ${termToString(term)} from empty max aggregator`); } - this.state.remove(term); + if (!this.state.remove(term)) { + throw new Error(`Cannot remove term ${termToString(term)} that was not added to max aggregator`); + } } - public termResult(): RDF.Term | undefined { + protected termResult(): RDF.Term | undefined { if (this.state === undefined) { return this.emptyValue(); } diff --git a/packages/actor-bindings-aggregator-factory-max/test/ActorBindingsAggregatorFactoryMax-test.ts b/packages/actor-bindings-aggregator-factory-max/test/ActorBindingsAggregatorFactoryMax-test.ts index 0d35689f..a4ac666c 100644 --- a/packages/actor-bindings-aggregator-factory-max/test/ActorBindingsAggregatorFactoryMax-test.ts +++ b/packages/actor-bindings-aggregator-factory-max/test/ActorBindingsAggregatorFactoryMax-test.ts @@ -1,16 +1,22 @@ -import { createTermCompMediator } from '@comunica/actor-term-comparator-factory-expression-evaluator/test/util'; +import { ActorFunctionFactoryTermEquality } from '@comunica/actor-function-factory-term-equality'; +import { ActorFunctionFactoryTermLesserThan } from '@comunica/actor-function-factory-term-lesser-than'; +import { + ActorTermComparatorFactoryExpressionEvaluator, +} from '@comunica/actor-term-comparator-factory-expression-evaluator'; import type { MediatorExpressionEvaluatorFactory } from '@comunica/bus-expression-evaluator-factory'; - import type { MediatorTermComparatorFactory } from '@comunica/bus-term-comparator-factory'; import { Bus } from '@comunica/core'; import type { IActionContext } from '@comunica/types'; import { BF, + createFuncMediator, DF, getMockEEActionContext, getMockMediatorExpressionEvaluatorFactory, + getMockMediatorMergeBindingsContext, + getMockMediatorQueryOperation, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; +} from '@incremunica/dev-tools'; import { ArrayIterator } from 'asynciterator'; import { ActorBindingsAggregatorFactoryMax } from '../lib'; import '@comunica/utils-jest'; @@ -41,7 +47,21 @@ describe('ActorBindingsAggregatorFactoryMax', () => { mediatorExpressionEvaluatorFactory = getMockMediatorExpressionEvaluatorFactory({ mediatorQueryOperation, }); - mediatorTermComparatorFactory = createTermCompMediator(); + // TODO [2025-02-01]: This can be replaced with createTermCompMediator in comunica + mediatorTermComparatorFactory = { + async mediate(action) { + return await new ActorTermComparatorFactoryExpressionEvaluator({ + name: 'actor', + bus: new Bus({ name: 'bus' }), + mediatorFunctionFactory: createFuncMediator([ + args => new ActorFunctionFactoryTermEquality(args), + args => new ActorFunctionFactoryTermLesserThan(args), + ], {}), + mediatorQueryOperation: getMockMediatorQueryOperation(), + mediatorMergeBindingsContext: getMockMediatorMergeBindingsContext(), + }).run(action); + }, + }; context = getMockEEActionContext(); }); diff --git a/packages/actor-bindings-aggregator-factory-max/test/MaxAggregator-test.ts b/packages/actor-bindings-aggregator-factory-max/test/MaxAggregator-test.ts index 3dc8a6c8..c7342b13 100644 --- a/packages/actor-bindings-aggregator-factory-max/test/MaxAggregator-test.ts +++ b/packages/actor-bindings-aggregator-factory-max/test/MaxAggregator-test.ts @@ -1,25 +1,35 @@ -import { createTermCompMediator } from '@comunica/actor-term-comparator-factory-expression-evaluator/test/util'; +import { ActorFunctionFactoryTermEquality } from '@comunica/actor-function-factory-term-equality'; +import { ActorFunctionFactoryTermLesserThan } from '@comunica/actor-function-factory-term-lesser-than'; +import { + ActorTermComparatorFactoryExpressionEvaluator, +} from '@comunica/actor-term-comparator-factory-expression-evaluator'; import type { ActorExpressionEvaluatorFactory } from '@comunica/bus-expression-evaluator-factory'; import type { MediatorTermComparatorFactory } from '@comunica/bus-term-comparator-factory'; +import { Bus } from '@comunica/core'; import type { IActionContext } from '@comunica/types'; +import type { Bindings } from '@comunica/utils-bindings-factory'; +import type { IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +import { KeysBindings } from '@incremunica/context-entries'; import { BF, + createFuncMediator, date, DF, double, float, getMockEEActionContext, getMockEEFactory, + getMockMediatorMergeBindingsContext, + getMockMediatorQueryOperation, int, makeAggregate, nonLiteral, string, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; -import type { IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +} from '@incremunica/dev-tools'; import type * as RDF from '@rdfjs/types'; import { MaxAggregator } from '../lib'; -async function runAggregator(aggregator: IBindingsAggregator, input: RDF.Bindings[]): Promise { +async function runAggregator(aggregator: IBindingsAggregator, input: Bindings[]): Promise { for (const bindings of input) { await aggregator.putBindings(bindings); } @@ -31,13 +41,11 @@ async function createAggregator({ mediatorTermComparatorFactory, context, distinct, - throwError, }: { expressionEvaluatorFactory: ActorExpressionEvaluatorFactory; mediatorTermComparatorFactory: MediatorTermComparatorFactory; context: IActionContext; distinct: boolean; - throwError?: boolean; }): Promise { return new MaxAggregator( await expressionEvaluatorFactory.run({ @@ -46,7 +54,7 @@ async function createAggregator({ }, undefined), distinct, await mediatorTermComparatorFactory.mediate({ context }), - throwError, + true, ); } @@ -57,7 +65,21 @@ describe('MaxAggregator', () => { beforeEach(() => { expressionEvaluatorFactory = getMockEEFactory(); - mediatorTermComparatorFactory = createTermCompMediator(); + // TODO [2025-02-01]: This can be replaced with createTermCompMediator in comunica + mediatorTermComparatorFactory = { + async mediate(action) { + return await new ActorTermComparatorFactoryExpressionEvaluator({ + name: 'actor', + bus: new Bus({ name: 'bus' }), + mediatorFunctionFactory: createFuncMediator([ + args => new ActorFunctionFactoryTermEquality(args), + args => new ActorFunctionFactoryTermLesserThan(args), + ], {}), + mediatorQueryOperation: getMockMediatorQueryOperation(), + mediatorMergeBindingsContext: getMockMediatorMergeBindingsContext(), + }).run(action); + }, + }; context = getMockEEActionContext(); }); @@ -74,23 +96,38 @@ describe('MaxAggregator', () => { }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), - BF.bindings([[ DF.variable('x'), int('4') ]]), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('4')); }); + it('a list of bindings 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('3')); + }); + it('a list of string bindings', async() => { const input = [ - BF.bindings([[ DF.variable('x'), string('11') ]]), - BF.bindings([[ DF.variable('x'), string('2') ]]), - BF.bindings([[ DF.variable('x'), string('1') ]]), - BF.bindings([[ DF.variable('x'), string('3') ]]), + BF.bindings([[ DF.variable('x'), string('11') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), string('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), string('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), string('3') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(string('3')); @@ -98,10 +135,10 @@ describe('MaxAggregator', () => { it('a list of date bindings', async() => { const input = [ - BF.bindings([[ DF.variable('x'), date('2010-06-21Z') ]]), - BF.bindings([[ DF.variable('x'), date('2010-06-21-08:00') ]]), - BF.bindings([[ DF.variable('x'), date('2001-07-23') ]]), - BF.bindings([[ DF.variable('x'), date('2010-06-21+09:00') ]]), + BF.bindings([[ DF.variable('x'), date('2010-06-21Z') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), date('2010-06-21-08:00') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), date('2001-07-23') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), date('2010-06-21+09:00') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(date('2010-06-21-08:00')); @@ -109,9 +146,9 @@ describe('MaxAggregator', () => { it('should work with different types', async() => { const input = [ - BF.bindings([[ DF.variable('x'), double('11.0') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), float('3') ]]), + BF.bindings([[ DF.variable('x'), double('11.0') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), float('3') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(double('11.0')); @@ -119,24 +156,65 @@ describe('MaxAggregator', () => { it('passing a non-literal should not be accepted', async() => { const input = [ - BF.bindings([[ DF.variable('x'), nonLiteral() ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), + BF.bindings([[ DF.variable('x'), nonLiteral() ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), ]; - await expect(runAggregator(aggregator, input)).resolves.toBeUndefined(); + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error(`Term with value ${nonLiteral().value} has type ${nonLiteral().termType} and is not a literal`), + ); }); it('passing a non-literal should not be accepted even in non-first place', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), nonLiteral() ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), nonLiteral() ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), ]; - await expect(runAggregator(aggregator, input)).resolves.toBeUndefined(); + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error(`Term with value ${nonLiteral().value} has type ${nonLiteral().termType} and is not a literal`), + ); }); it('with respect to empty input', async() => { - await expect(runAggregator(aggregator, [])).resolves.toBeUndefined(); + await expect(runAggregator(aggregator, [])).rejects.toThrow( + new Error(`Empty aggregate expression`), + ); + }); + + it('should error on a deletion if aggregator empty', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "2"^^http://www.w3.org/2001/XMLSchema#integer from empty max aggregator'), + ); + }); + + it('should error on a deletion that has not been added', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "3"^^http://www.w3.org/2001/XMLSchema#integer that was not added to max aggregator'), + ); + }); + + it('delete everything', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error(`Empty aggregate expression`), + ); }); }); @@ -152,19 +230,63 @@ describe('MaxAggregator', () => { }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('1') ], [ DF.variable('y'), int('1') ]]), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('2')); }); + it('a list of bindings 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('2')); + }); + + it('a list of bindings 3', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('1')); + }); + it('with respect to empty input', async() => { - await expect(runAggregator(aggregator, [])).resolves.toBeUndefined(); + await expect(runAggregator(aggregator, [])).rejects.toThrow( + new Error(`Empty aggregate expression`), + ); }); }); @@ -178,18 +300,17 @@ describe('MaxAggregator', () => { mediatorTermComparatorFactory, context, distinct: false, - throwError: true, }); }); it('and the input is empty', async() => { - const input: RDF.Bindings[] = []; + const input: Bindings[] = []; await expect(runAggregator(aggregator, input)).rejects.toThrow('Empty aggregate expression'); }); it('and the first value errors', async() => { const input = [ - BF.bindings([[ DF.variable('x'), nonLiteral() ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), + BF.bindings([[ DF.variable('x'), nonLiteral() ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).rejects .toThrow('Term with value http://example.org/ has type NamedNode and is not a literal'); @@ -197,8 +318,8 @@ describe('MaxAggregator', () => { it('and any value in the stream errors', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), nonLiteral() ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), nonLiteral() ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).rejects .toThrow('Term with value http://example.org/ has type NamedNode and is not a literal'); diff --git a/packages/actor-bindings-aggregator-factory-min/lib/MinAggregator.ts b/packages/actor-bindings-aggregator-factory-min/lib/MinAggregator.ts index e87ba720..93045f57 100644 --- a/packages/actor-bindings-aggregator-factory-min/lib/MinAggregator.ts +++ b/packages/actor-bindings-aggregator-factory-min/lib/MinAggregator.ts @@ -19,12 +19,12 @@ export class MinAggregator extends AggregateEvaluator implements IBindingsAggreg super(evaluator, distinct, throwError); } - public putTerm(term: RDF.Term): void { + protected putTerm(term: RDF.Term): void { if (term.termType !== 'Literal') { throw new Error(`Term with value ${term.value} has type ${term.termType} and is not a literal`); } if (this.state === undefined) { - this.state = new AVLTree((a, b) => this.orderByEvaluator.orderTypes(a, b), this.distinct); + this.state = new AVLTree((a, b) => this.orderByEvaluator.orderTypes(a, b)); } this.state.insert(term, term); } @@ -33,10 +33,12 @@ export class MinAggregator extends AggregateEvaluator implements IBindingsAggreg if (this.state === undefined) { throw new Error(`Cannot remove term ${termToString(term)} from empty min aggregator`); } - this.state.remove(term); + if (!this.state.remove(term)) { + throw new Error(`Cannot remove term ${termToString(term)} that was not added to min aggregator`); + } } - public termResult(): RDF.Term | undefined { + protected termResult(): RDF.Term | undefined { if (this.state === undefined) { return this.emptyValue(); } diff --git a/packages/actor-bindings-aggregator-factory-min/test/ActorBindingsAggregatorFactoryMin-test.ts b/packages/actor-bindings-aggregator-factory-min/test/ActorBindingsAggregatorFactoryMin-test.ts index 40855f9c..ea547080 100644 --- a/packages/actor-bindings-aggregator-factory-min/test/ActorBindingsAggregatorFactoryMin-test.ts +++ b/packages/actor-bindings-aggregator-factory-min/test/ActorBindingsAggregatorFactoryMin-test.ts @@ -1,15 +1,22 @@ -import { createTermCompMediator } from '@comunica/actor-term-comparator-factory-expression-evaluator/test/util'; +import { ActorFunctionFactoryTermEquality } from '@comunica/actor-function-factory-term-equality'; +import { ActorFunctionFactoryTermLesserThan } from '@comunica/actor-function-factory-term-lesser-than'; +import { + ActorTermComparatorFactoryExpressionEvaluator, +} from '@comunica/actor-term-comparator-factory-expression-evaluator'; import type { MediatorExpressionEvaluatorFactory } from '@comunica/bus-expression-evaluator-factory'; import type { MediatorTermComparatorFactory } from '@comunica/bus-term-comparator-factory'; import { Bus } from '@comunica/core'; import type { IActionContext } from '@comunica/types'; import { BF, + createFuncMediator, DF, getMockEEActionContext, getMockMediatorExpressionEvaluatorFactory, + getMockMediatorMergeBindingsContext, + getMockMediatorQueryOperation, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; +} from '@incremunica/dev-tools'; import { ArrayIterator } from 'asynciterator'; import { ActorBindingsAggregatorFactoryMin } from '../lib'; import '@comunica/utils-jest'; @@ -39,7 +46,21 @@ describe('ActorBindingsAggregatorFactoryMin', () => { mediatorExpressionEvaluatorFactory = getMockMediatorExpressionEvaluatorFactory({ mediatorQueryOperation, }); - mediatorTermComparatorFactory = createTermCompMediator(); + // TODO [2025-02-01]: This can be replaced with createTermCompMediator in comunica + mediatorTermComparatorFactory = { + async mediate(action) { + return await new ActorTermComparatorFactoryExpressionEvaluator({ + name: 'actor', + bus: new Bus({ name: 'bus' }), + mediatorFunctionFactory: createFuncMediator([ + args => new ActorFunctionFactoryTermEquality(args), + args => new ActorFunctionFactoryTermLesserThan(args), + ], {}), + mediatorQueryOperation: getMockMediatorQueryOperation(), + mediatorMergeBindingsContext: getMockMediatorMergeBindingsContext(), + }).run(action); + }, + }; }); describe('An ActorBindingsAggregatorFactoryMin instance', () => { diff --git a/packages/actor-bindings-aggregator-factory-min/test/MinAggregator-test.ts b/packages/actor-bindings-aggregator-factory-min/test/MinAggregator-test.ts index c109085e..9ec0184e 100644 --- a/packages/actor-bindings-aggregator-factory-min/test/MinAggregator-test.ts +++ b/packages/actor-bindings-aggregator-factory-min/test/MinAggregator-test.ts @@ -1,25 +1,35 @@ -import { createTermCompMediator } from '@comunica/actor-term-comparator-factory-expression-evaluator/test/util'; +import { ActorFunctionFactoryTermEquality } from '@comunica/actor-function-factory-term-equality'; +import { ActorFunctionFactoryTermLesserThan } from '@comunica/actor-function-factory-term-lesser-than'; +import { + ActorTermComparatorFactoryExpressionEvaluator, +} from '@comunica/actor-term-comparator-factory-expression-evaluator'; import type { ActorExpressionEvaluatorFactory } from '@comunica/bus-expression-evaluator-factory'; import type { MediatorTermComparatorFactory } from '@comunica/bus-term-comparator-factory'; +import { Bus } from '@comunica/core'; import type { IActionContext } from '@comunica/types'; +import type { Bindings } from '@comunica/utils-bindings-factory'; +import type { IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +import { KeysBindings } from '@incremunica/context-entries'; import { BF, + createFuncMediator, date, DF, double, float, getMockEEActionContext, getMockEEFactory, + getMockMediatorMergeBindingsContext, + getMockMediatorQueryOperation, int, makeAggregate, nonLiteral, string, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; -import type { IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +} from '@incremunica/dev-tools'; import type * as RDF from '@rdfjs/types'; import { MinAggregator } from '../lib/MinAggregator'; -async function runAggregator(aggregator: IBindingsAggregator, input: RDF.Bindings[]): Promise { +async function runAggregator(aggregator: IBindingsAggregator, input: Bindings[]): Promise { for (const bindings of input) { await aggregator.putBindings(bindings); } @@ -30,14 +40,12 @@ async function createAggregator({ expressionEvaluatorFactory, context, distinct, - throwError, mediatorTermComparatorFactory, }: { expressionEvaluatorFactory: ActorExpressionEvaluatorFactory; mediatorTermComparatorFactory: MediatorTermComparatorFactory; context: IActionContext; distinct: boolean; - throwError?: boolean; }): Promise { return new MinAggregator( await expressionEvaluatorFactory.run({ @@ -46,7 +54,7 @@ async function createAggregator({ }, undefined), distinct, await mediatorTermComparatorFactory.mediate({ context }), - throwError, + true, ); } describe('MinAggregator', () => { @@ -56,7 +64,21 @@ describe('MinAggregator', () => { beforeEach(() => { expressionEvaluatorFactory = getMockEEFactory(); - mediatorTermComparatorFactory = createTermCompMediator(); + // TODO [2025-02-01]: This can be replaced with createTermCompMediator in comunica + mediatorTermComparatorFactory = { + async mediate(action) { + return await new ActorTermComparatorFactoryExpressionEvaluator({ + name: 'actor', + bus: new Bus({ name: 'bus' }), + mediatorFunctionFactory: createFuncMediator([ + args => new ActorFunctionFactoryTermEquality(args), + args => new ActorFunctionFactoryTermLesserThan(args), + ], {}), + mediatorQueryOperation: getMockMediatorQueryOperation(), + mediatorMergeBindingsContext: getMockMediatorMergeBindingsContext(), + }).run(action); + }, + }; context = getMockEEActionContext(); }); @@ -73,23 +95,38 @@ describe('MinAggregator', () => { }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), - BF.bindings([[ DF.variable('x'), int('4') ]]), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('1')); }); + it('a list of bindings 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('2')); + }); + it('a list of string bindings', async() => { const input = [ - BF.bindings([[ DF.variable('x'), string('11') ]]), - BF.bindings([[ DF.variable('x'), string('2') ]]), - BF.bindings([[ DF.variable('x'), string('1') ]]), - BF.bindings([[ DF.variable('x'), string('3') ]]), + BF.bindings([[ DF.variable('x'), string('11') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), string('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), string('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), string('3') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(string('1')); @@ -97,10 +134,10 @@ describe('MinAggregator', () => { it('a list of date bindings', async() => { const input = [ - BF.bindings([[ DF.variable('x'), date('2010-06-21Z') ]]), - BF.bindings([[ DF.variable('x'), date('2010-06-21-08:00') ]]), - BF.bindings([[ DF.variable('x'), date('2001-07-23') ]]), - BF.bindings([[ DF.variable('x'), date('2010-06-21+09:00') ]]), + BF.bindings([[ DF.variable('x'), date('2010-06-21Z') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), date('2010-06-21-08:00') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), date('2001-07-23') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), date('2010-06-21+09:00') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(date('2001-07-23')); @@ -108,9 +145,9 @@ describe('MinAggregator', () => { it('should work with different types', async() => { const input = [ - BF.bindings([[ DF.variable('x'), double('11.0') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), float('3') ]]), + BF.bindings([[ DF.variable('x'), double('11.0') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), float('3') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('2')); @@ -118,24 +155,65 @@ describe('MinAggregator', () => { it('passing a non-literal should not be accepted', async() => { const input = [ - BF.bindings([[ DF.variable('x'), nonLiteral() ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), + BF.bindings([[ DF.variable('x'), nonLiteral() ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), ]; - await expect(runAggregator(aggregator, input)).resolves.toBeUndefined(); + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error(`Term with value ${nonLiteral().value} has type ${nonLiteral().termType} and is not a literal`), + ); }); it('passing a non-literal should not be accepted even in non-first place', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), nonLiteral() ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), nonLiteral() ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), ]; - await expect(runAggregator(aggregator, input)).resolves.toBeUndefined(); + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error(`Term with value ${nonLiteral().value} has type ${nonLiteral().termType} and is not a literal`), + ); }); it('with respect to empty input', async() => { - await expect(runAggregator(aggregator, [])).resolves.toBeUndefined(); + await expect(runAggregator(aggregator, [])).rejects.toThrow( + new Error(`Empty aggregate expression`), + ); + }); + + it('should error on a deletion if aggregator empty', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "2"^^http://www.w3.org/2001/XMLSchema#integer from empty min aggregator'), + ); + }); + + it('should error on a deletion that has not been added', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "2"^^http://www.w3.org/2001/XMLSchema#integer that was not added to min aggregator'), + ); + }); + + it('delete everything', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error(`Empty aggregate expression`), + ); }); }); @@ -151,19 +229,63 @@ describe('MinAggregator', () => { }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('1')); + }); + + it('a list of bindings 2', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('1') ], [ DF.variable('y'), int('1') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, false), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('1')); }); + it('a list of bindings 3', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('2')); + }); + it('with respect to empty input', async() => { - await expect(runAggregator(aggregator, [])).resolves.toBeUndefined(); + await expect(runAggregator(aggregator, [])).rejects.toThrow( + new Error(`Empty aggregate expression`), + ); }); }); }); diff --git a/packages/actor-bindings-aggregator-factory-sample/lib/SampleAggregator.ts b/packages/actor-bindings-aggregator-factory-sample/lib/SampleAggregator.ts index 38186c8e..84e7d627 100644 --- a/packages/actor-bindings-aggregator-factory-sample/lib/SampleAggregator.ts +++ b/packages/actor-bindings-aggregator-factory-sample/lib/SampleAggregator.ts @@ -11,7 +11,7 @@ export class SampleAggregator extends AggregateEvaluator implements IBindingsAgg super(evaluator, distinct, throwError); } - public putTerm(term: RDF.Term): void { + protected putTerm(term: RDF.Term): void { if (this.state === undefined) { this.state = new Map([[ term.value, { value: term, count: 1 }]]); return; @@ -19,13 +19,13 @@ export class SampleAggregator extends AggregateEvaluator implements IBindingsAgg this.state.set(term.value, { value: term, count: (this.state.get(term.value)?.count ?? 0) + 1 }); } - public removeTerm(term: RDF.Term): void { + protected removeTerm(term: RDF.Term): void { if (this.state === undefined) { throw new Error(`Cannot remove term ${termToString(term)} from empty sample aggregator`); } const count = this.state.get(term.value); if (count === undefined) { - throw new Error(`Cannot remove term ${termToString(term)}, it was not added`); + throw new Error(`Cannot remove term ${termToString(term)} that was not added to sample aggregator`); } if (count.count === 1) { this.state.delete(term.value); @@ -34,7 +34,7 @@ export class SampleAggregator extends AggregateEvaluator implements IBindingsAgg } } - public termResult(): RDF.Term | undefined { + protected termResult(): RDF.Term | undefined { if (this.state === undefined) { return this.emptyValue(); } diff --git a/packages/actor-bindings-aggregator-factory-sample/test/ActorBindingsAggregatorFactorySample-test.ts b/packages/actor-bindings-aggregator-factory-sample/test/ActorBindingsAggregatorFactorySample-test.ts index 5a312fed..9071b2ef 100644 --- a/packages/actor-bindings-aggregator-factory-sample/test/ActorBindingsAggregatorFactorySample-test.ts +++ b/packages/actor-bindings-aggregator-factory-sample/test/ActorBindingsAggregatorFactorySample-test.ts @@ -7,7 +7,7 @@ import { getMockEEActionContext, getMockMediatorExpressionEvaluatorFactory, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; +} from '@incremunica/dev-tools'; import { ArrayIterator } from 'asynciterator'; import { ActorBindingsAggregatorFactorySample } from '../lib'; import '@comunica/utils-jest'; diff --git a/packages/actor-bindings-aggregator-factory-sample/test/SampleAggregator-test.ts b/packages/actor-bindings-aggregator-factory-sample/test/SampleAggregator-test.ts index 158ff394..0ac9f378 100644 --- a/packages/actor-bindings-aggregator-factory-sample/test/SampleAggregator-test.ts +++ b/packages/actor-bindings-aggregator-factory-sample/test/SampleAggregator-test.ts @@ -1,5 +1,8 @@ import type { ActorExpressionEvaluatorFactory } from '@comunica/bus-expression-evaluator-factory'; import type { IActionContext } from '@comunica/types'; +import type { Bindings } from '@comunica/utils-bindings-factory'; +import type { IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +import { KeysBindings } from '@incremunica/context-entries'; import { BF, DF, @@ -7,12 +10,11 @@ import { getMockEEFactory, int, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; -import type { IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +} from '@incremunica/dev-tools'; import type * as RDF from '@rdfjs/types'; import { SampleAggregator } from '../lib'; -async function runAggregator(aggregator: IBindingsAggregator, input: RDF.Bindings[]): Promise { +async function runAggregator(aggregator: IBindingsAggregator, input: Bindings[]): Promise { for (const bindings of input) { await aggregator.putBindings(bindings); } @@ -30,6 +32,7 @@ async function createAggregator({ expressionEvaluatorFactory, context, distinct context, }, undefined), distinct, + true, ); } describe('SampleAggregator', () => { @@ -49,19 +52,83 @@ describe('SampleAggregator', () => { aggregator = await createAggregator({ expressionEvaluatorFactory, context, distinct: false }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), - BF.bindings([[ DF.variable('x'), int('4') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('1')); }); + it('a list of bindings 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('2')); + }); + + it('a list of bindings 3', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('2')); + }); + it('with respect to empty input', async() => { - await expect(runAggregator(aggregator, [])).resolves.toBeUndefined(); + await expect(runAggregator(aggregator, [])).rejects.toThrow( + new Error('Empty aggregate expression'), + ); + }); + + it('should error on a deletion if aggregator empty', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "2"^^http://www.w3.org/2001/XMLSchema#integer from empty sample aggregator'), + ); + }); + + it('should error on a deletion that has not been added', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "2"^^http://www.w3.org/2001/XMLSchema#integer that was not added to sample aggregator'), + ); + }); + + it('delete everything', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Empty aggregate expression'), + ); }); }); @@ -72,19 +139,43 @@ describe('SampleAggregator', () => { aggregator = await createAggregator({ expressionEvaluatorFactory, context, distinct: true }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('1')); + }); + + it('a list of bindings 2', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('1') ], [ DF.variable('y'), int('1') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, false), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('1')); }); it('with respect to empty input', async() => { - await expect(runAggregator(aggregator, [])).resolves.toBeUndefined(); + await expect(runAggregator(aggregator, [])).rejects.toThrow( + new Error('Empty aggregate expression'), + ); }); }); }); diff --git a/packages/actor-bindings-aggregator-factory-sum/lib/SumAggregator.ts b/packages/actor-bindings-aggregator-factory-sum/lib/SumAggregator.ts index 20329b23..0bb1fb9e 100644 --- a/packages/actor-bindings-aggregator-factory-sum/lib/SumAggregator.ts +++ b/packages/actor-bindings-aggregator-factory-sum/lib/SumAggregator.ts @@ -6,10 +6,13 @@ import { AggregateEvaluator } from '@incremunica/bus-bindings-aggregator-factory import type * as RDF from '@rdfjs/types'; import { termToString } from 'rdf-string'; -type SumState = NumericLiteral; +interface ISumState { + index: Map; + sum: NumericLiteral; +} export class SumAggregator extends AggregateEvaluator { - private state: SumState | undefined = undefined; + private state: ISumState | undefined = undefined; public constructor( evaluator: IExpressionEvaluator, @@ -26,28 +29,55 @@ export class SumAggregator extends AggregateEvaluator { return typedLiteral('0', TypeURL.XSD_INTEGER); } - public putTerm(term: RDF.Term): void { + protected putTerm(term: RDF.Term): void { + const hash = termToString(term); + const value = this.termToNumericOrError(term); if (this.state === undefined) { - this.state = this.termToNumericOrError(term); - } else { - const internalTerm = this.termToNumericOrError(term); - this.state = this.additionFunction.applyOnTerms([ this.state, internalTerm ], this.evaluator); + this.state = { + index: new Map([[ hash, 1 ]]), + sum: value, + }; + return; + } + let count = this.state.index.get(hash); + if (count === undefined) { + count = 0; + } + this.state.index.set(hash, count + 1); + if (!this.distinct || count === 0) { + this.state.sum = this.additionFunction.applyOnTerms([ this.state.sum, value ], this.evaluator); } } - public removeTerm(term: RDF.Term): void { + protected removeTerm(term: RDF.Term): void { + const hash = termToString(term); + const value = this.termToNumericOrError(term); if (this.state === undefined) { - throw new Error(`Cannot remove term ${termToString(term)} from empty sum aggregator`); + throw new Error(`Cannot remove term ${hash} from empty sum aggregator`); + } + const count = this.state.index.get(hash); + if (count === undefined) { + throw new Error(`Cannot remove term ${hash} that was not added to sum aggregator`); + } + if (count === 1) { + this.state.index.delete(hash); + if (this.state.index.size === 0) { + this.state = undefined; + return; + } } else { - const internalTerm = this.termToNumericOrError(term); - this.state = this.subtractionFunction.applyOnTerms([ this.state, internalTerm ], this.evaluator); + this.state.index.set(hash, count - 1); + } + if (!this.distinct || count === 1) { + this.state.sum = this.subtractionFunction + .applyOnTerms([ this.state.sum, value ], this.evaluator); } } - public termResult(): RDF.Term | undefined { + protected termResult(): RDF.Term | undefined { if (this.state === undefined) { return this.emptyValue(); } - return this.state.toRDF(this.dataFactory); + return this.state.sum.toRDF(this.dataFactory); } } diff --git a/packages/actor-bindings-aggregator-factory-sum/test/ActorBindingsAggregatorFactorySum-test.ts b/packages/actor-bindings-aggregator-factory-sum/test/ActorBindingsAggregatorFactorySum-test.ts index 8ac99c50..b662bed5 100644 --- a/packages/actor-bindings-aggregator-factory-sum/test/ActorBindingsAggregatorFactorySum-test.ts +++ b/packages/actor-bindings-aggregator-factory-sum/test/ActorBindingsAggregatorFactorySum-test.ts @@ -1,18 +1,19 @@ import { ActorFunctionFactoryTermAddition } from '@comunica/actor-function-factory-term-addition'; +import { ActorFunctionFactoryTermSubtraction } from '@comunica/actor-function-factory-term-subtraction'; import type { MediatorExpressionEvaluatorFactory, } from '@comunica/bus-expression-evaluator-factory'; import type { MediatorFunctionFactory } from '@comunica/bus-function-factory'; -import { createFuncMediator } from '@comunica/bus-function-factory/test/util'; import { Bus } from '@comunica/core'; import type { IActionContext } from '@comunica/types'; import { + createFuncMediator, BF, DF, getMockEEActionContext, getMockMediatorExpressionEvaluatorFactory, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; +} from '@incremunica/dev-tools'; import { ArrayIterator } from 'asynciterator'; import { ActorBindingsAggregatorFactorySum } from '../lib'; import '@comunica/utils-jest'; @@ -44,6 +45,7 @@ describe('ActorBindingsAggregatorFactorySum', () => { }); mediatorFunctionFactory = createFuncMediator([ args => new ActorFunctionFactoryTermAddition(args), + args => new ActorFunctionFactoryTermSubtraction(args), ], {}); }); diff --git a/packages/actor-bindings-aggregator-factory-sum/test/SumAggeregator-test.ts b/packages/actor-bindings-aggregator-factory-sum/test/SumAggeregator-test.ts index 056171c1..ee8a3e82 100644 --- a/packages/actor-bindings-aggregator-factory-sum/test/SumAggeregator-test.ts +++ b/packages/actor-bindings-aggregator-factory-sum/test/SumAggeregator-test.ts @@ -1,11 +1,15 @@ import { ActorFunctionFactoryTermAddition } from '@comunica/actor-function-factory-term-addition'; +import { ActorFunctionFactoryTermSubtraction } from '@comunica/actor-function-factory-term-subtraction'; import type { ActorExpressionEvaluatorFactory } from '@comunica/bus-expression-evaluator-factory'; import type { MediatorFunctionFactory } from '@comunica/bus-function-factory'; -import { createFuncMediator } from '@comunica/bus-function-factory/test/util'; import { KeysInitQuery } from '@comunica/context-entries'; import type { IActionContext } from '@comunica/types'; +import type { Bindings } from '@comunica/utils-bindings-factory'; import { SparqlOperator } from '@comunica/utils-expression-evaluator'; +import type { AggregateEvaluator, IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +import { KeysBindings } from '@incremunica/context-entries'; import { + createFuncMediator, BF, decimal, DF, @@ -15,12 +19,11 @@ import { int, makeAggregate, nonLiteral, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; -import type { IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +} from '@incremunica/dev-tools'; import type * as RDF from '@rdfjs/types'; import { SumAggregator } from '../lib'; -async function runAggregator(aggregator: IBindingsAggregator, input: RDF.Bindings[]): Promise { +async function runAggregator(aggregator: IBindingsAggregator, input: Bindings[]): Promise { for (const bindings of input) { await aggregator.putBindings(bindings); } @@ -45,6 +48,12 @@ async function createAggregator({ expressionEvaluatorFactory, mediatorFunctionFa functionName: SparqlOperator.ADDITION, requireTermExpression: true, }), + await mediatorFunctionFactory.mediate({ + context, + functionName: SparqlOperator.SUBTRACTION, + requireTermExpression: true, + }), + true, ); } @@ -57,6 +66,7 @@ describe('SumAggregator', () => { expressionEvaluatorFactory = getMockEEFactory(); mediatorFunctionFactory = createFuncMediator([ args => new ActorFunctionFactoryTermAddition(args), + args => new ActorFunctionFactoryTermSubtraction(args), ], {}); context = getMockEEActionContext(); @@ -74,61 +84,113 @@ describe('SumAggregator', () => { }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), - BF.bindings([[ DF.variable('x'), int('4') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('10')); }); + it('a list of bindings 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('7')); + }); + it('undefined when sum is undefined', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), DF.literal('1') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('1') ]]).setContextEntry(KeysBindings.isAddition, true), ]; - await expect(runAggregator(aggregator, input)).resolves.toBeUndefined(); + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Term datatype http://www.w3.org/2001/XMLSchema#string with value 1 has type Literal and is not a numeric literal'), + ); }); it('with respect to type promotion', async() => { const input = [ - BF.bindings([[ DF.variable('x'), DF.literal('1', DF.namedNode('http://www.w3.org/2001/XMLSchema#byte')) ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), float('3') ]]), - BF.bindings([[ DF.variable('x'), DF.literal('4', DF.namedNode('http://www.w3.org/2001/XMLSchema#nonNegativeInteger')) ]]), + BF.bindings([[ DF.variable('x'), DF.literal('1', DF.namedNode('http://www.w3.org/2001/XMLSchema#byte')) ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), float('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), DF.literal('4', DF.namedNode('http://www.w3.org/2001/XMLSchema#nonNegativeInteger')) ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(float('10')); }); it('with accurate results', async() => { const input = [ - BF.bindings([[ DF.variable('x'), decimal('1.0') ]]), - BF.bindings([[ DF.variable('x'), decimal('2.2') ]]), - BF.bindings([[ DF.variable('x'), decimal('2.2') ]]), - BF.bindings([[ DF.variable('x'), decimal('2.2') ]]), - BF.bindings([[ DF.variable('x'), decimal('3.5') ]]), + BF.bindings([[ DF.variable('x'), decimal('1.0') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), decimal('2.2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), decimal('2.2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), decimal('2.2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), decimal('3.5') ]]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(decimal('11.1')); }); it('passing a non-literal should not be accepted', async() => { const input = [ - BF.bindings([[ DF.variable('x'), nonLiteral() ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), - BF.bindings([[ DF.variable('x'), int('4') ]]), + BF.bindings([[ DF.variable('x'), nonLiteral() ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('4') ]]).setContextEntry(KeysBindings.isAddition, true), ]; - await expect(runAggregator(aggregator, input)).resolves.toBeUndefined(); + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error(`Term with value ${nonLiteral().value} has type ${nonLiteral().termType} and is not a numeric literal`), + ); }); it('with respect to empty input', async() => { await expect(runAggregator(aggregator, [])).resolves.toEqual(int('0')); }); + + it('should error on a deletion if aggregator empty', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "2"^^http://www.w3.org/2001/XMLSchema#integer from empty sum aggregator'), + ); + }); + + it('should error on a deletion that has not been added', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove term "2"^^http://www.w3.org/2001/XMLSchema#integer that was not added to sum aggregator'), + ); + }); + + it('delete everything', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves + .toEqual((aggregator).emptyValueTerm()); + }); }); describe('distinctive sum', () => { @@ -143,12 +205,34 @@ describe('SumAggregator', () => { }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('3')); + }); + + it('a list of bindings 2', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('1') ], [ DF.variable('y'), int('1') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, false), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('3')); diff --git a/packages/actor-bindings-aggregator-factory-wildcard-count/lib/WildcardCountAggregator.ts b/packages/actor-bindings-aggregator-factory-wildcard-count/lib/WildcardCountAggregator.ts index a3369b9d..8d3a0dc4 100644 --- a/packages/actor-bindings-aggregator-factory-wildcard-count/lib/WildcardCountAggregator.ts +++ b/packages/actor-bindings-aggregator-factory-wildcard-count/lib/WildcardCountAggregator.ts @@ -8,14 +8,14 @@ import type * as RDF from '@rdfjs/types'; import * as RdfString from 'rdf-string'; export class WildcardCountAggregator extends AggregateEvaluator implements IBindingsAggregator { - private readonly bindingValues: Map> = new Map(); - private state: number | undefined = undefined; + private readonly bindingValues: Map> = new Map(); + private state = 0; public constructor(evaluator: IExpressionEvaluator, distinct: boolean, throwError?: boolean) { super(evaluator, distinct, throwError); } - public putTerm(_term: RDF.Term): void { + protected putTerm(_term: RDF.Term): void { // Do nothing, not needed } @@ -24,17 +24,11 @@ export class WildcardCountAggregator extends AggregateEvaluator implements IBind } public override async putBindings(bindings: Bindings): Promise { - if (!this.handleDistinct(bindings)) { + if (!this.skipDistinctBindings(bindings)) { if (bindings.getContextEntry(KeysBindings.isAddition)) { - if (this.state === undefined) { - this.state = 0; - } - this.state += 1; + this.state++; } else { - if (this.state === undefined) { - throw new Error(`Cannot remove bindings ${bindings.toString()} to empty wildcard-count aggregator`); - } - this.state -= 1; + this.state--; } } } @@ -43,8 +37,8 @@ export class WildcardCountAggregator extends AggregateEvaluator implements IBind return typedLiteral('0', TypeURL.XSD_INTEGER); } - public termResult(): RDF.Term | undefined { - if (this.state === undefined) { + protected termResult(): RDF.Term | undefined { + if (this.state === 0) { return this.emptyValue(); } return typedLiteral(String(this.state), TypeURL.XSD_INTEGER); @@ -55,24 +49,44 @@ export class WildcardCountAggregator extends AggregateEvaluator implements IBind * @param bindings * @private */ - private handleDistinct(bindings: RDF.Bindings): boolean { - if (this.distinct) { - const bindingList: [RDF.Variable, RDF.Term][] = [ ...bindings ]; - bindingList.sort((first, snd) => first[0].value.localeCompare(snd[0].value)); - const variables = bindingList.map(([ variable ]) => variable.value).join(','); - const terms = bindingList.map(([ , term ]) => RdfString.termToString(term)).join(','); + private skipDistinctBindings(bindings: Bindings): boolean { + const bindingList: [RDF.Variable, RDF.Term][] = [ ...bindings ]; + bindingList.sort((first, snd) => first[0].value.localeCompare(snd[0].value)); + const variables = bindingList.map(([ variable ]) => variable.value).join(','); + const terms = bindingList.map(([ , term ]) => RdfString.termToString(term)).join(','); + let termsMap = this.bindingValues.get(variables); - const set = this.bindingValues.get(variables); - const result = set !== undefined && set.has(terms); - - // Add to the set: - if (!set) { - this.bindingValues.set(variables, new Set()); + if (bindings.getContextEntry(KeysBindings.isAddition)) { + if (termsMap === undefined) { + termsMap = new Map([[ terms, 1 ]]); + this.bindingValues.set(variables, termsMap); + return false; } - this.bindingValues.get(variables)!.add(terms); - - return result; + const count = termsMap.get(terms); + if (count === undefined) { + termsMap.set(terms, 1); + return false; + } + termsMap.set(terms, count + 1); + // Return true if we are in distinct mode otherwise return false + return this.distinct; + } + if (termsMap === undefined) { + throw new Error(`Cannot remove bindings ${bindings.toString()} that was not added to wildcard-count aggregator`); + } + const count = termsMap.get(terms); + if (count === undefined) { + throw new Error(`Cannot remove bindings ${bindings.toString()} that was not added to wildcard-count aggregator`); + } + if (count === 1) { + termsMap.delete(terms); + if (termsMap.size === 0) { + this.bindingValues.delete(variables); + } + // Return true if we are in distinct mode otherwise return false + return false; } - return false; + termsMap.set(terms, count - 1); + return this.distinct; } } diff --git a/packages/actor-bindings-aggregator-factory-wildcard-count/test/ActorBindingsAggregatorFactoryWildcardCount-test.ts b/packages/actor-bindings-aggregator-factory-wildcard-count/test/ActorBindingsAggregatorFactoryWildcardCount-test.ts index 88a8af38..897f1b76 100644 --- a/packages/actor-bindings-aggregator-factory-wildcard-count/test/ActorBindingsAggregatorFactoryWildcardCount-test.ts +++ b/packages/actor-bindings-aggregator-factory-wildcard-count/test/ActorBindingsAggregatorFactoryWildcardCount-test.ts @@ -7,7 +7,7 @@ import { getMockEEActionContext, getMockMediatorExpressionEvaluatorFactory, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; +} from '@incremunica/dev-tools'; import { ArrayIterator } from 'asynciterator'; import { ActorBindingsAggregatorFactoryWildcardCount } from '../lib'; import '@comunica/utils-jest'; diff --git a/packages/actor-bindings-aggregator-factory-wildcard-count/test/WildcardCountAggregator-test.ts b/packages/actor-bindings-aggregator-factory-wildcard-count/test/WildcardCountAggregator-test.ts index 96ac7dff..881ae73d 100644 --- a/packages/actor-bindings-aggregator-factory-wildcard-count/test/WildcardCountAggregator-test.ts +++ b/packages/actor-bindings-aggregator-factory-wildcard-count/test/WildcardCountAggregator-test.ts @@ -1,5 +1,8 @@ import type { ActorExpressionEvaluatorFactory } from '@comunica/bus-expression-evaluator-factory'; import type { IActionContext } from '@comunica/types'; +import type { Bindings } from '@comunica/utils-bindings-factory'; +import type { AggregateEvaluator, IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +import { KeysBindings } from '@incremunica/context-entries'; import { BF, DF, @@ -7,12 +10,11 @@ import { getMockEEFactory, int, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; -import type { IBindingsAggregator } from '@incremunica/bus-bindings-aggregator-factory'; +} from '@incremunica/dev-tools'; import type * as RDF from '@rdfjs/types'; -import { WildcardCountAggregator } from '../lib/WildcardCountAggregator'; +import { WildcardCountAggregator } from '../lib'; -async function runAggregator(aggregator: IBindingsAggregator, input: RDF.Bindings[]): Promise { +async function runAggregator(aggregator: IBindingsAggregator, input: Bindings[]): Promise { for (const bindings of input) { await aggregator.putBindings(bindings); } @@ -30,6 +32,7 @@ async function createAggregator({ expressionEvaluatorFactory, context, distinct context, }, undefined), distinct, + true, ); } @@ -50,26 +53,88 @@ describe('WildcardCountAggregator', () => { aggregator = await createAggregator({ expressionEvaluatorFactory, context, distinct: false }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('y'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), - BF.bindings([]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('y'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('4')); }); + it('a list of bindings 2', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('y'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('y'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('1')); + }); + + it('a list of bindings 3', async() => { + const input = [ + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('2')); + }); + it('with respect to empty input', async() => { await expect(runAggregator(aggregator, [])).resolves.toEqual(int('0')); }); + it('should error on a deletion if aggregator empty', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove bindings {\n "x": "\\"2\\"^^http://www.w3.org/2001/XMLSchema#integer"\n} that was not added to wildcard-count aggregator'), + ); + }); + + it('should error on a deletion that has not been added', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).rejects.toThrow( + new Error('Cannot remove bindings {\n "x": "\\"2\\"^^http://www.w3.org/2001/XMLSchema#integer"\n} that was not added to wildcard-count aggregator'), + ); + }); + + it('delete everything', async() => { + const input = [ + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves + .toEqual((aggregator).emptyValueTerm()); + }); + it('extends the AggregateEvaluator', () => { - expect(( aggregator).termResult).toBeInstanceOf(Function); - expect(( aggregator).putTerm).toBeInstanceOf(Function); - // Put term does nothing - expect(() => ( aggregator).putTerm( undefined)).not.toThrow(); + expect(( aggregator).termResult).toBeInstanceOf(Function); + expect(( aggregator).putTerm).toBeInstanceOf(Function); + expect(( aggregator).removeTerm).toBeInstanceOf(Function); + expect(() => ( aggregator).putTerm( undefined)).not.toThrow(); + expect(() => ( aggregator).removeTerm( undefined)).not.toThrow(); }); }); @@ -80,54 +145,102 @@ describe('WildcardCountAggregator', () => { aggregator = await createAggregator({ expressionEvaluatorFactory, context, distinct: true }); }); - it('a list of bindings', async() => { + it('a list of bindings 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('1') ], [ DF.variable('y'), int('1') ]]), - BF.bindings([]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('4')); }); - it('a list of bindings containing 2 empty', async() => { + it('a list of bindings 2', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('y'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('3') ]]), - BF.bindings([]), - BF.bindings([]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, false), ]; - await expect(runAggregator(aggregator, input)).resolves.toEqual(int('4')); + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('2')); }); - it('a list of bindings 2', async() => { + it('a list of bindings 3', async() => { const input = [ - BF.bindings([]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('1') ], [ DF.variable('y'), int('1') ]]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([ + [ DF.variable('x'), int('1') ], + [ DF.variable('y'), int('1') ], + ]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, false), ]; - await expect(runAggregator(aggregator, input)).resolves.toEqual(int('4')); + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('1')); }); - it('a list of bindings 3', async() => { + it('a list of bindings containing 2 empty 1', async() => { const input = [ - BF.bindings([[ DF.variable('x'), int('1') ], [ DF.variable('y'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([[ DF.variable('x'), int('2') ]]), - BF.bindings([[ DF.variable('x'), int('1') ]]), - BF.bindings([]), + BF.bindings([[ DF.variable('x'), int('1') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('y'), int('2') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([[ DF.variable('x'), int('3') ]]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), ]; await expect(runAggregator(aggregator, input)).resolves.toEqual(int('4')); }); + it('a list of bindings containing 3 empty 1', async() => { + const input = [ + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('1')); + }); + + it('a list of bindings containing 3 empty 2', async() => { + const input = [ + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, true), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, false), + BF.bindings([]).setContextEntry(KeysBindings.isAddition, false), + ]; + + await expect(runAggregator(aggregator, input)).resolves.toEqual(int('0')); + }); + it('with respect to empty input', async() => { await expect(runAggregator(aggregator, [])).resolves.toEqual(int('0')); }); diff --git a/packages/bus-bindings-aggregator-factory/lib/ActorBindingsAggregatorFactory.ts b/packages/bus-bindings-aggregator-factory/lib/ActorBindingsAggregatorFactory.ts index 36a96b0a..4642949d 100644 --- a/packages/bus-bindings-aggregator-factory/lib/ActorBindingsAggregatorFactory.ts +++ b/packages/bus-bindings-aggregator-factory/lib/ActorBindingsAggregatorFactory.ts @@ -31,7 +31,6 @@ TS * \ @defaultNested { a } bus * \ @defaultNested {Creation of Aggregator failed: none of the configured actors were able to handle ${action.expr.aggregator}} busFailMessage */ - /* eslint-enable max-len */ protected constructor(args: IActorBindingsAggregatorFactoryArgs) { super(args); this.mediatorExpressionEvaluatorFactory = args.mediatorExpressionEvaluatorFactory; diff --git a/packages/bus-bindings-aggregator-factory/lib/AggregateEvaluator.ts b/packages/bus-bindings-aggregator-factory/lib/AggregateEvaluator.ts index 1762750c..2b1e9689 100644 --- a/packages/bus-bindings-aggregator-factory/lib/AggregateEvaluator.ts +++ b/packages/bus-bindings-aggregator-factory/lib/AggregateEvaluator.ts @@ -4,7 +4,6 @@ import type { Bindings } from '@comunica/utils-bindings-factory'; import * as Eval from '@comunica/utils-expression-evaluator'; import { KeysBindings } from '@incremunica/context-entries'; import type * as RDF from '@rdfjs/types'; -import * as RdfString from 'rdf-string'; /** * This is the base class for all aggregators. @@ -14,8 +13,6 @@ export abstract class AggregateEvaluator { private errorOccurred = false; private lastResult: undefined | null | RDF.Term = undefined; - protected readonly variableValues: Map; - protected readonly superTypeProvider: ISuperTypeProvider; protected readonly termTransformer: Eval.TermTransformer; @@ -27,8 +24,6 @@ export abstract class AggregateEvaluator { this.errorOccurred = false; this.superTypeProvider = evaluator.context.getSafe(KeysExpressionEvaluator.superTypeProvider); this.termTransformer = new Eval.TermTransformer(this.superTypeProvider); - - this.variableValues = new Map(); } protected abstract putTerm(term: RDF.Term): void; @@ -69,30 +64,7 @@ export abstract class AggregateEvaluator { } if (bindings.getContextEntry(KeysBindings.isAddition)) { - // Handle DISTINCT before putting the term - if (this.distinct) { - const hash = RdfString.termToString(term); - let count = this.variableValues.get(hash); - if (!count) { - this.putTerm(term); - count = 0; - } - this.variableValues.set(hash, count + 1); - } else { - this.putTerm(term); - } - } else if (this.distinct) { - // Handle DISTINCT before putting the term - const hash = RdfString.termToString(term); - const count = this.variableValues.get(hash); - if (count === 1) { - this.removeTerm(term); - this.variableValues.delete(hash); - } else if (count === undefined) { - this.safeThrow('count is undefined, this shouldn\'t happen'); - } else { - this.variableValues.set(hash, count - 1); - } + this.putTerm(term); } else { this.removeTerm(term); } diff --git a/packages/bus-bindings-aggregator-factory/test/AggregateEvaluator-test.ts b/packages/bus-bindings-aggregator-factory/test/AggregateEvaluator-test.ts index 604aa74b..1dbf00f8 100644 --- a/packages/bus-bindings-aggregator-factory/test/AggregateEvaluator-test.ts +++ b/packages/bus-bindings-aggregator-factory/test/AggregateEvaluator-test.ts @@ -1,11 +1,12 @@ import type { IExpressionEvaluator } from '@comunica/types'; import { BindingsFactory } from '@comunica/utils-bindings-factory'; +import { KeysBindings } from '@incremunica/context-entries'; import { getMockEEActionContext, getMockEEFactory, int, makeAggregate, -} from '@comunica/utils-expression-evaluator/test/util/helpers'; +} from '@incremunica/dev-tools'; import type * as RDF from '@rdfjs/types'; import { DataFactory } from 'rdf-data-factory'; import { AggregateEvaluator } from '../lib'; @@ -18,7 +19,11 @@ class EmptyEvaluator extends AggregateEvaluator { super(evaluator, distinct, throwError); } - public putTerm(_: RDF.Term): void { + protected putTerm(_: RDF.Term): void { + // Empty + } + + protected removeTerm(_: RDF.Term): void { // Empty } @@ -28,7 +33,7 @@ class EmptyEvaluator extends AggregateEvaluator { } describe('aggregate evaluator', () => { - it('handles errors using async evaluations', async() => { + it('handles errors using evaluations 1', async() => { const temp = await getMockEEFactory().run({ algExpr: makeAggregate('sum').expression, context: getMockEEActionContext(), @@ -41,8 +46,60 @@ describe('aggregate evaluator', () => { } return int('1'); }; - const evaluator: AggregateEvaluator = new EmptyEvaluator(temp, false); - await Promise.all([ evaluator.putBindings(BF.bindings()), evaluator.putBindings(BF.bindings()) ]); - await expect(evaluator.result()).resolves.toBeUndefined(); + const evaluator: AggregateEvaluator = new EmptyEvaluator(temp, false, false); + await Promise.all([ + evaluator.putBindings(BF.bindings().setContextEntry(KeysBindings.isAddition, true)), + evaluator.putBindings(BF.bindings().setContextEntry(KeysBindings.isAddition, true)), + evaluator.putBindings(BF.bindings().setContextEntry(KeysBindings.isAddition, true)), + ]); + expect(evaluator.result()).toBeNull(); + expect(evaluator.result()).toBeUndefined(); + await evaluator.putBindings(BF.bindings().setContextEntry(KeysBindings.isAddition, true)); + expect(evaluator.result()).toBeUndefined(); + }); + + it('handles errors using evaluations 2', async() => { + const temp = await getMockEEFactory().run({ + algExpr: makeAggregate('sum').expression, + context: getMockEEActionContext(), + }, undefined); + let first = true; + temp.evaluate = async() => { + if (first) { + first = false; + throw new Error('We only want the first to succeed'); + } + return int('1'); + }; + const evaluator: AggregateEvaluator = new EmptyEvaluator(temp, false, false); + await Promise.all([ + evaluator.putBindings(BF.bindings().setContextEntry(KeysBindings.isAddition, true)), + evaluator.putBindings(BF.bindings().setContextEntry(KeysBindings.isAddition, true)), + evaluator.putBindings(BF.bindings().setContextEntry(KeysBindings.isAddition, false)), + evaluator.putBindings(BF.bindings().setContextEntry(KeysBindings.isAddition, false)), + ]); + expect(evaluator.result()).toBeNull(); + }); + + it('returns undefined if result hasn\'t changed', async() => { + const temp = await getMockEEFactory().run({ + algExpr: makeAggregate('sum').expression, + context: getMockEEActionContext(), + }, undefined); + const evaluator: AggregateEvaluator = new EmptyEvaluator(temp, false, false); + (evaluator).termResult = () => int('1'); + expect(evaluator.result()).toEqual(int('1')); + expect(evaluator.result()).toBeUndefined(); + }); + + it('returns undefined if result was undefined twice', async() => { + const temp = await getMockEEFactory().run({ + algExpr: makeAggregate('sum').expression, + context: getMockEEActionContext(), + }, undefined); + const evaluator: AggregateEvaluator = new EmptyEvaluator(temp, false, false); + (evaluator).termResult = () => undefined; + expect(evaluator.result()).toBeUndefined(); + expect(evaluator.result()).toBeUndefined(); }); });