Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Faster isObject and other util functions #507

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# jsonld ChangeLog

## 8.1.1 - 2023-02-dd

### Fixed
- Improved `types.isObject` internal API performance.
- Improved `graphTypes.*` internal API performance.
- Improved `util.addValue` performance.
- Improved `util.compareValues` performance.

## 8.1.0 - 2022-08-29

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ Use a command line with a test suite and a benchmark flag:
EARL reports with benchmark data can be generated with an optional environment
details:

JSONLD_TESTS=`pwd`/../json-ld.org/benchmarks/b001-manifiest.jsonld JSONLD_BENCHMARK=1 EARL=earl-test.jsonld TEST_ENV=1 npm test
JSONLD_TESTS=`pwd`/../json-ld.org/benchmarks/b001-manifest.jsonld JSONLD_BENCHMARK=1 EARL=earl-test.jsonld TEST_ENV=1 npm test

See `tests/test.js` for more `TEST_ENV` control and options.

Expand Down
21 changes: 11 additions & 10 deletions lib/graphTypes.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved.
/*!
* Copyright (c) 2017-2023 Digital Bazaar, Inc. All rights reserved.
*/
'use strict';

Expand Down Expand Up @@ -52,7 +52,7 @@ api.isValue = v =>
// Note: A value is a @value if all of these hold true:
// 1. It is an Object.
// 2. It has the @value property.
types.isObject(v) && ('@value' in v);
types.isObject(v) && v['@value'] !== undefined;

/**
* Returns true if the given value is a @list.
Expand All @@ -65,7 +65,7 @@ api.isList = v =>
// Note: A value is a @list if all of these hold true:
// 1. It is an Object.
// 2. It has the @list property.
types.isObject(v) && ('@list' in v);
types.isObject(v) && v['@list'] !== undefined;

/**
* Returns true if the given value is a @graph.
Expand All @@ -78,7 +78,7 @@ api.isGraph = v => {
// 2. It has an `@graph` key.
// 3. It may have '@id' or '@index'
return types.isObject(v) &&
'@graph' in v &&
v['@graph'] !== undefined &&
Object.keys(v)
.filter(key => key !== '@id' && key !== '@index').length === 1;
};
Expand All @@ -93,7 +93,7 @@ api.isSimpleGraph = v => {
// 1. It is an object.
// 2. It has an `@graph` key.
// 3. It has only 1 key or 2 keys where one of them is `@index`.
return api.isGraph(v) && !('@id' in v);
return api.isGraph(v) && v['@id'] === undefined;
};

/**
Expand All @@ -109,12 +109,13 @@ api.isBlankNode = v => {
// 2. If it has an @id key that is not a string OR begins with '_:'.
// 3. It has no keys OR is not a @value, @set, or @list.
if(types.isObject(v)) {
if('@id' in v) {
const id = v['@id'];
const id = v['@id'];
if(id !== undefined) {
return !types.isString(id) || id.indexOf('_:') === 0;
}
return (Object.keys(v).length === 0 ||
!(('@value' in v) || ('@set' in v) || ('@list' in v)));
return (v['@value'] === undefined &&
v['@set'] === undefined &&
v['@list'] === undefined) || Object.keys(v).length === 0;
}
return false;
};
3 changes: 2 additions & 1 deletion lib/nodeMap.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ api.createNodeMap = (input, graphs, graph, issuer, name, list) => {
{propertyIsArray: true, allowDuplicate: false});
api.createNodeMap(o, graphs, graph, issuer, id);
} else if(graphTypes.isValue(o)) {
// handle @value
util.addValue(
subject, property, o,
{propertyIsArray: true, allowDuplicate: false});
Expand All @@ -213,7 +214,7 @@ api.createNodeMap = (input, graphs, graph, issuer, name, list) => {
subject, property, o,
{propertyIsArray: true, allowDuplicate: false});
} else {
// handle @value
// handle remaining cases
api.createNodeMap(o, graphs, graph, issuer, name);
util.addValue(
subject, property, o, {propertyIsArray: true, allowDuplicate: false});
Expand Down
6 changes: 3 additions & 3 deletions lib/types.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017 Digital Bazaar, Inc. All rights reserved.
/*!
* Copyright (c) 2017-2023 Digital Bazaar, Inc. All rights reserved.
*/
'use strict';

Expand Down Expand Up @@ -70,7 +70,7 @@ api.isNumeric = v => !isNaN(parseFloat(v)) && isFinite(v);
*
* @return true if the value is an Object, false if not.
*/
api.isObject = v => Object.prototype.toString.call(v) === '[object Object]';
api.isObject = v => v !== null && typeof v === 'object' && !Array.isArray(v);

/**
* Returns true if the given value is a String.
Expand Down
212 changes: 146 additions & 66 deletions lib/util.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2017-2019 Digital Bazaar, Inc. All rights reserved.
/*!
* Copyright (c) 2017-2023 Digital Bazaar, Inc. All rights reserved.
*/
'use strict';

Expand Down Expand Up @@ -191,7 +191,7 @@ api.validateTypeValue = (v, isFrame) => {
api.hasProperty = (subject, property) => {
if(subject.hasOwnProperty(property)) {
const value = subject[property];
return (!types.isArray(value) || value.length > 0);
return !types.isArray(value) || value.length > 0;
}
return false;
};
Expand All @@ -206,24 +206,16 @@ api.hasProperty = (subject, property) => {
* @return true if the value exists, false if not.
*/
api.hasValue = (subject, property, value) => {
if(api.hasProperty(subject, property)) {
let val = subject[property];
const isList = graphTypes.isList(val);
if(types.isArray(val) || isList) {
if(isList) {
val = val['@list'];
}
for(let i = 0; i < val.length; ++i) {
if(api.compareValues(value, val[i])) {
return true;
}
}
} else if(!types.isArray(value)) {
// avoid matching the set of values with an array value parameter
return api.compareValues(value, val);
}
if(!api.hasProperty(subject, property)) {
return false;
}
return false;
let target = subject[property];
const isList = graphTypes.isList(target);
if(isList) {
target = target['@list'];
}
const isArray = types.isArray(target);
return _hasValue(target, isArray, value);
};

/**
Expand All @@ -245,56 +237,20 @@ api.hasValue = (subject, property, value) => {
*/
api.addValue = (subject, property, value, options) => {
options = options || {};
if(!('propertyIsArray' in options)) {
if(options.propertyIsArray === undefined) {
options.propertyIsArray = false;
}
if(!('valueIsArray' in options)) {
if(options.valueIsArray === undefined) {
options.valueIsArray = false;
}
if(!('allowDuplicate' in options)) {
if(options.allowDuplicate === undefined) {
options.allowDuplicate = true;
}
if(!('prependValue' in options)) {
if(options.prependValue === undefined) {
options.prependValue = false;
}

if(options.valueIsArray) {
subject[property] = value;
} else if(types.isArray(value)) {
if(value.length === 0 && options.propertyIsArray &&
!subject.hasOwnProperty(property)) {
subject[property] = [];
}
if(options.prependValue) {
value = value.concat(subject[property]);
subject[property] = [];
}
for(let i = 0; i < value.length; ++i) {
api.addValue(subject, property, value[i], options);
}
} else if(subject.hasOwnProperty(property)) {
// check if subject already has value if duplicates not allowed
const hasValue = (!options.allowDuplicate &&
api.hasValue(subject, property, value));

// make property an array if value not present or always an array
if(!types.isArray(subject[property]) &&
(!hasValue || options.propertyIsArray)) {
subject[property] = [subject[property]];
}

// add new value
if(!hasValue) {
if(options.prependValue) {
subject[property].unshift(value);
} else {
subject[property].push(value);
}
}
} else {
// add new value as set or single value
subject[property] = options.propertyIsArray ? [value] : value;
}
_addValue(subject, property, value, options);
};

/**
Expand Down Expand Up @@ -389,11 +345,10 @@ api.compareValues = (v1, v2) => {
}

// 3. equal @ids
if(types.isObject(v1) &&
('@id' in v1) &&
types.isObject(v2) &&
('@id' in v2)) {
return v1['@id'] === v2['@id'];
if(types.isObject(v1) && types.isObject(v2)) {
const id1 = v1['@id'];
const id2 = v2['@id'];
return id1 !== undefined && id1 === id2;
}

return false;
Expand All @@ -420,6 +375,131 @@ api.compareShortestLeast = (a, b) => {
return (a < b) ? -1 : 1;
};

// internal helper for `api.addValue`
function _addValue(subject, property, value, options) {
// if value is an array, assume no special checks, just set it
if(options.valueIsArray) {
subject[property] = value;
return;
}

// handle adding multiple values
const multipleValues = types.isArray(value);
if(multipleValues) {
// array of length `1` is handled as a single value below
if(value.length !== 1) {
return _addValues(subject, property, value, options);
}
value = value[0];
}

if(subject.hasOwnProperty(property)) {
_addToExistingProperty(subject, property, value, options);
} else {
// add new value as set or single value
subject[property] = options.propertyIsArray ? [value] : value;
}
}

// internal helper for `api.addValue`; `value.length !== 1`
function _addValues(subject, property, value, options) {
// handle empty array
if(value.length === 0) {
// ensure property is set to an array
if(options.propertyIsArray && !subject.hasOwnProperty(property)) {
subject[property] = [];
}
return;
}

// add each element of `value` to the `target`, which may start out as
// `undefined` if `property` is not yet set in `subject`
let target = subject[property];
let isArray = types.isArray(target);
for(const nextValue of value) {
// if no target set yet...
if(target === undefined) {
if(options.propertyIsArray) {
target = [nextValue];
isArray = true;
} else {
target = nextValue;
}
continue;
}

// if duplicates not allowed, skip if `nextValue` is a dupe
if(!options.allowDuplicate) {
if(_hasValue(target, isArray, nextValue)) {
continue;
}
}

// add `nextValue` to target
if(isArray) {
if(options.prependValue) {
target.unshift(nextValue);
} else {
target.push(nextValue);
}
} else {
if(options.prependValue) {
target = [nextValue, target];
} else {
target = [target, nextValue];
}
isArray = true;
}
}
// ensure subject property value is updated to `target`
subject[property] = target;
}

// internal helper for `api.addValue`
function _addToExistingProperty(subject, property, value, options) {
const target = subject[property];
const isArray = types.isArray(target);

// consider subject has having value if duplicates are allowed or if target
// has no matching value
const hasValue = !options.allowDuplicate &&
_hasValue(target, isArray, value);

if(!isArray) {
// make property value an array if value not present or always an array
if(hasValue) {
if(options.propertyIsArray) {
// value already present, just convert to an array
subject[property] = [target];
}
} else if(options.prependValue) {
subject[property] = [value, target];
} else {
subject[property] = [target, value];
}
return;
}

// add new value to target array if not present
if(!hasValue) {
if(options.prependValue) {
target.unshift(value);
} else {
target.push(value);
}
}
}

// internal helper to see if `target` has `value`; `target` can be an array
// to look for `value` in or a simple value / undefined to compare against
// `value`
function _hasValue(target, isArray, value) {
if(isArray) {
return target.some(t => api.compareValues(value, t));
}
return api.compareValues(value, target);
}

/**
* Labels the blank nodes in the given value using the given IdentifierIssuer.
*
Expand Down