Skip to content

Commit

Permalink
Return memoized objects from constructor for duplicates
Browse files Browse the repository at this point in the history
  • Loading branch information
Fletcher91 committed Nov 19, 2018
1 parent fe756d7 commit 1c54ffb
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 33 deletions.
23 changes: 17 additions & 6 deletions src/blank-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,17 @@ const ClassOrder = require('./class-order')
const Term = require('./term')

class BlankNode extends Term {
/** @private */
static normalizeID (id) {
if (typeof id === 'string' && id.includes('#')) {
// Is a URI with hash fragment
let fragments = id.split('#')
return fragments[fragments.length - 1]
}

return id
}

constructor (id) {
super()
this.termType = BlankNode.termType
Expand All @@ -12,13 +23,13 @@ class BlankNode extends Term {
console.log('Bad blank id:', id)
throw new Error('Bad id argument to new blank node: ' + id)
}
if (id.includes('#')) {
// Is a URI with hash fragment
let fragments = id.split('#')
id = fragments[fragments.length - 1]

this.id = BlankNode.normalizeID(id)

const existing = Term.bnMap[this.id]
if (existing) {
return existing
}
this.id = id
// this.id = '' + BlankNode.nextId++
} else {
this.id = 'n' + BlankNode.nextId++
}
Expand Down
5 changes: 5 additions & 0 deletions src/literal.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ class Literal extends Term {
}
// If not specified, a literal has the implied XSD.string default datatype
this.datatype = datatype ? NamedNode.fromValue(datatype) : XSD.string

const existing = Term.findLiteralByValue(value, language, datatype)
if (existing) {
return existing
}
}
copy () {
return Term.literalByValue(this.value, this.lang, this.datatype)
Expand Down
5 changes: 5 additions & 0 deletions src/named-node.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ class NamedNode extends Term {
throw new Error(message)
}

const existing = Term.nsMap[iri]
if (existing) {
return existing
}

this.value = iri
}
/**
Expand Down
33 changes: 23 additions & 10 deletions src/term.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,16 +68,18 @@ class Term extends Node {

/**
* Retrieve or create a BlankNode by its ID
* @param id? {string} The ID of the blank node
* @param idOrIRI? {string} The ID of the blank node or a hash fragment IRI
* @return {BlankNode} The resolved or created BlankNode
*/
static blankNodeByID(id) {
static blankNodeByID(idOrIRI) {
const BlankNode = require('./blank-node')
const id = BlankNode.normalizeID(idOrIRI)
const fromMap = this.bnMap[id]
if (fromMap !== undefined) {
return fromMap
}
const BlankNode = require('./blank-node')
return this.addBn(new BlankNode(id))
const newBN = new BlankNode(id)
return this.addBn(newBN)
}

/**
Expand All @@ -89,6 +91,18 @@ class Term extends Node {
*/
static literalByValue(value, lang = undefined, datatype) {
const strValue = value.toString()

const existing = Term.findLiteralByValue(strValue, lang, datatype)
if (existing) {
return existing
}

const Literal = require('./literal')
return this.addLit(new Literal(strValue, lang, datatype))
}

/** @private */
static findLiteralByValue(strValue, lang = undefined, datatype) {
let fromMap
// Language strings need an additional index layer
if (lang) {
Expand All @@ -105,12 +119,7 @@ class Term extends Node {
fromMap = this.litMap[dtIndex] && this.litMap[dtIndex][strValue]
}

if (fromMap) {
return fromMap
}

const Literal = require('./literal')
return this.addLit(new Literal(strValue, lang, datatype))
return fromMap
}

/**
Expand Down Expand Up @@ -187,6 +196,7 @@ class Term extends Node {

/**
* Running counter which assigns ids
* @private
* @type {number}
*/
Term.termIndex = 0
Expand All @@ -199,18 +209,21 @@ Term.termMap = []

/**
* Maps IRIs to their NamedNode counterparts
* @private
* @type {Object<string, NamedNode>}
*/
Term.nsMap = {}

/**
* Maps blank ids to their BlankNode counterparts
* @private
* @type {Object<string, BlankNode>}
*/
Term.bnMap = {}

/**
* Maps literals to their Literal counterparts
* @private
* @type {Array<Array<Literal|Array<Literal>>>}
*/
Term.litMap = []
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/blank-node-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import fs from 'fs'
import rdf from '../../src/index'

import BlankNode from '../../src/blank-node'
import Term from '../../src/term'

describe('BlankNode', () => {
after(() => {
Expand All @@ -15,6 +16,15 @@ describe('BlankNode', () => {
it('should throw an error when passed a non-string id', () => {
expect(() => { new BlankNode(1) }).to.throw(/Bad id argument to new blank node/)
})

it('should return a fresh object', () => {
expect(new BlankNode('http://example.com/1#subresource').sI).to.be.undefined()
})

it('should return an existing instance if present', () => {
const existing = Term.blankNodeByID('http://example.com/2#subresource')
expect(new BlankNode('http://example.com/2#subresource').sI).to.equal(existing.sI)
})
})

describe('serialize', () => {
Expand Down
45 changes: 28 additions & 17 deletions tests/unit/literal-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,48 +6,59 @@ import Literal from '../../src/literal'
import XSD from '../../src/xsd'

describe('Literal', () => {
describe('constructor()', () => {
it('should return a fresh object', () => {
expect(new Literal('Some nice text').sI).to.be.undefined()
})

it('should return an existing instance if present', () => {
const existing = Term.literalByValue('Some neat text')
expect(new Literal('Some neat text').sI).to.equal(existing.sI)
})
})

describe('fromValue', () => {
describe('for numbers', () => {
it('detects integers', () => {
expect(Literal.fromValue(0)).to.eql(Term.literalByValue('0', null, XSD.integer))
expect(Literal.fromValue(1)).to.eql(Term.literalByValue('1', null, XSD.integer))
expect(Literal.fromValue(0)).to.eql(new Literal('0', null, XSD.integer))
expect(Literal.fromValue(1)).to.eql(new Literal('1', null, XSD.integer))
expect(Literal.fromValue(Number.MAX_SAFE_INTEGER))
.to.eql(Term.literalByValue(Number.MAX_SAFE_INTEGER.toString(), null, XSD.integer))
.to.eql(new Literal(Number.MAX_SAFE_INTEGER.toString(), null, XSD.integer))
expect(Literal.fromValue(Number.MIN_SAFE_INTEGER))
.to.eql(Term.literalByValue(Number.MIN_SAFE_INTEGER.toString(), null, XSD.integer))
.to.eql(new Literal(Number.MIN_SAFE_INTEGER.toString(), null, XSD.integer))
})

it('detects decimals', () => {
expect(Literal.fromValue(1.1)).to.eql(Term.literalByValue('1.1', null, XSD.decimal))
expect(Literal.fromValue(1.1)).to.eql(new Literal('1.1', null, XSD.decimal))
})

it('detects doubles', () => {
expect(Literal.fromValue(Number.MAX_SAFE_INTEGER + 1))
.to.eql(Term.literalByValue((Number.MAX_SAFE_INTEGER + 1).toString(), null, XSD.double))
.to.eql(new Literal((Number.MAX_SAFE_INTEGER + 1).toString(), null, XSD.double))
expect(Literal.fromValue(Number.MIN_SAFE_INTEGER - 1))
.to.eql(Term.literalByValue((Number.MIN_SAFE_INTEGER - 1).toString(), null, XSD.double))
.to.eql(new Literal((Number.MIN_SAFE_INTEGER - 1).toString(), null, XSD.double))
expect(Literal.fromValue(Number.MAX_VALUE))
.to.eql(Term.literalByValue(Number.MAX_VALUE.toString(), null, XSD.double))
.to.eql(new Literal(Number.MAX_VALUE.toString(), null, XSD.double))
expect(Literal.fromValue(-Number.MAX_VALUE))
.to.eql(Term.literalByValue((-Number.MAX_VALUE).toString(), null, XSD.double))
.to.eql(new Literal((-Number.MAX_VALUE).toString(), null, XSD.double))
expect(Literal.fromValue(Number.MIN_VALUE))
.to.eql(Term.literalByValue(Number.MIN_VALUE.toString(), null, XSD.double))
.to.eql(new Literal(Number.MIN_VALUE.toString(), null, XSD.double))
})
})

it('detects string values', () => {
expect(Literal.fromValue('foo')).to.eql(Term.literalByValue('foo', null, XSD.string))
expect(Literal.fromValue('foo')).to.eql(new Literal('foo', null, null))
})

it('detects boolean values', () => {
expect(Literal.fromValue(true)).to.eql(Term.literalByValue('1', null, XSD.boolean))
expect(Literal.fromValue(false)).to.eql(Term.literalByValue('0', null, XSD.boolean))
expect(Literal.fromValue(true)).to.eql(new Literal('1', null, XSD.boolean))
expect(Literal.fromValue(false)).to.eql(new Literal('0', null, XSD.boolean))
})

it('constructs a literal representing a date value', () => {
const date = new Date(Date.UTC(2010, 5, 10, 1, 2, 3))
expect(Literal.fromValue(date))
.to.eql(Term.literalByValue('2010-06-10T01:02:03Z', null, XSD.dateTime))
.to.eql(new Literal('2010-06-10T01:02:03Z', null, XSD.dateTime))
})
})

Expand All @@ -59,7 +70,7 @@ describe('Literal', () => {
})

it('serializes strings with a language', () => {
const node = Term.literalByValue('foo', 'en')
const node = new Literal('foo', 'en')
expect(node.toNT()).to.equal('"foo"@en')
})
})
Expand Down Expand Up @@ -103,8 +114,8 @@ describe('Literal', () => {

describe('equals', () => {
it('compares termType, value, language, and datatype', () => {
const a = Term.literalByValue('hello world', 'en', XSD.langString)
const b = Term.literalByValue('', '', null)
const a = new Literal('hello world', 'en', XSD.langString)
const b = new Literal('', '', null)
expect(a.equals(b)).to.be.false
expect(b.equals(a)).to.be.false
b.value = 'hello world'
Expand Down
10 changes: 10 additions & 0 deletions tests/unit/named-node-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { expect } from 'chai'

import Term from '../../src/term'
import NamedNode from '../../src/named-node'

describe('NamedNode', () => {
Expand All @@ -13,5 +14,14 @@ describe('NamedNode', () => {
it('should throw an error on relative uri', () => {
expect(() => { new NamedNode('./local') }).to.throw(Error)
})

it('should return a fresh object', () => {
expect(new NamedNode('http://example.com/1').sI).to.be.undefined()
})

it('should return an existing instance if present', () => {
const existing = Term.namedNodeByIRI('http://example.com/2')
expect(new NamedNode('http://example.com/2').sI).to.equal(existing.sI)
})
})
})

0 comments on commit 1c54ffb

Please sign in to comment.