Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 42 additions & 2 deletions src/DatasetWrapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,55 @@ export class DatasetWrapper implements DatasetCore {
return new klass(g, this.dataset, this.factory)
}

/**
* Matches quads in the dataset and wraps their subjects.
*
* @remarks
* Each distinct subject term is yielded at most once, even when it occurs in multiple matching quads. Terms are compared by value, following [Term.equals](https://rdf.js.org/data-model-spec/#dfn-equals) semantics.
*/
protected* matchSubjectsOf<T>(termWrapper: ITermWrapperConstructor<T>, predicate?: Term, object?: Term, graph?: Term): Iterable<T> {
const seen = new Set<string>()

for (const q of this.match(undefined, predicate, object, graph)) {
yield new termWrapper(q.subject, this, this.factory)
const key = DatasetWrapper.keyOf(q.subject)

if (!seen.has(key)) {
seen.add(key)
yield new termWrapper(q.subject, this, this.factory)
}
}
}

/**
* Matches quads in the dataset and wraps their objects.
*
* @remarks
* Each distinct object term is yielded at most once, even when it occurs in multiple matching quads. Terms are compared by value, following [Term.equals](https://rdf.js.org/data-model-spec/#dfn-equals) semantics.
*/
protected* matchObjectsOf<T>(termWrapper: ITermWrapperConstructor<T>, subject?: Term, predicate?: Term, graph?: Term): Iterable<T> {
const seen = new Set<string>()

for (const q of this.match(subject, predicate, undefined, graph)) {
yield new termWrapper(q.object, this, this.factory)
const key = DatasetWrapper.keyOf(q.object)

if (!seen.has(key)) {
seen.add(key)
yield new termWrapper(q.object, this, this.factory)
}
}
}

/**
* Derives a string key that identifies a term by value, so that terms equal per [Term.equals](https://rdf.js.org/data-model-spec/#dfn-equals) map to the same key.
*/
private static keyOf(term: Term): string {
switch (term.termType) {
case "Literal":
return `${term.termType} ${term.language} ${term.direction ?? ""} ${JSON.stringify(term.datatype.value)} ${JSON.stringify(term.value)}`
case "Quad":
return `${term.termType} ${DatasetWrapper.keyOf(term.subject)} ${DatasetWrapper.keyOf(term.predicate)} ${DatasetWrapper.keyOf(term.object)} ${DatasetWrapper.keyOf(term.graph)}`
default:
return `${term.termType} ${JSON.stringify(term.value)}`
}
}

Expand Down
68 changes: 68 additions & 0 deletions test/unit/dataset_wrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { describe, it } from "node:test"
import { DataFactory } from "n3"
import { ParentDataset } from "./model/ParentDataset.js"
import { datasetFromRdf } from "./util/datasetFromRdf.js"
import { Example } from "./vocabulary/Example.js"

const rdf = `
prefix : <https://example.org/>
Expand Down Expand Up @@ -80,3 +81,70 @@ await describe("Dataset Wrappers", async () => {
}
})
})

const rdfWithDuplicates = `
prefix : <https://example.org/>

<s>
a :Parent ;
:hasString "s" ;
:hasChild <c1>, <c2> ;
:hasLangString "chat"@en, "chat"@fr ;
.
<s2>
:hasString "s2" ;
:hasChild <c1> ;
:hasLangString "chat"@en ;
.
<c1> :hasString "c1" .
<c2> :hasString "c2" .

<g> {
<s> a :Parent .
}
`;


await describe("Dataset Wrappers yield distinct terms", async () => {
const parentDataset = new ParentDataset(datasetFromRdf(rdfWithDuplicates), DataFactory)

await it("get instances of Parent once despite duplicate rdf:type paths", async () => {
const parents = Array.from(parentDataset.instancesOfParent)

assert.equal(parents.length, 1)
assert.equal(parents[0]!.hasString, "s")
})

await it("get subjects of hasChild once despite multiple matching quads", async () => {
const parents = Array.from(parentDataset.subjectsOfHasChild)

assert.equal(parents.length, 2)
assert.deepStrictEqual(parents.map(parent => parent.hasString).sort(), ["s", "s2"])
})

await it("get objects of hasChild once despite multiple matching quads", async () => {
const children = Array.from(parentDataset.objectsOfHasChild)

assert.equal(children.length, 2)
assert.deepStrictEqual(children.map(child => child.hasString).sort(), ["c1", "c2"])
})

await it("keeps objects that are literals differing only in language", async () => {
const strings = Array.from(parentDataset.objectsOfHasLangString)

assert.equal(strings.length, 2)
assert.deepStrictEqual(strings.map(string => string.language).sort(), ["en", "fr"])
})

await it("get subjects of hasChild once when the subject is a quoted triple", async () => {
const { namedNode, quad } = DataFactory
const quoted = quad(namedNode("s"), namedNode("p"), namedNode("o"))
const dataset = datasetFromRdf("")
dataset.add(quad(quoted, namedNode(Example.hasChild), namedNode("c1")))
dataset.add(quad(quoted, namedNode(Example.hasChild), namedNode("c2")))

const parents = Array.from(new ParentDataset(dataset, DataFactory).subjectsOfHasChild)

assert.equal(parents.length, 1)
})
})
4 changes: 4 additions & 0 deletions test/unit/model/ParentDataset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ export class ParentDataset extends DatasetWrapper {
return this.objectsOf(Example.hasChild, Child)
}

public get objectsOfHasLangString(): Iterable<Child> {
return this.objectsOf(Example.hasLangString, Child)
}

public get matchSubjectsOfPropertyanyObjectparentGraphany(): Iterable<Parent> {
return this.matchSubjectsOf(Parent, undefined, this.factory.namedNode(Example.Parent), undefined)
}
Expand Down