diff --git a/.releases/4.18.0.md b/.releases/4.18.0.md index 1bd63e3..ac22d75 100644 --- a/.releases/4.18.0.md +++ b/.releases/4.18.0.md @@ -1,5 +1,6 @@ **New Features** * Added a `Changes` specification. +* Added a `Crosses` specification. * Added a `Null` specification. * Added an `Undefined` specification. \ No newline at end of file diff --git a/specifications/Crosses.js b/specifications/Crosses.js new file mode 100644 index 0000000..12b6631 --- /dev/null +++ b/specifications/Crosses.js @@ -0,0 +1,57 @@ +const assert = require('./../lang/assert'); + is = require('./../lang/is'); + +const Specification = require('./Specification'); + +module.exports = (() => { + 'use strict'; + + /** + * A stateful {@link Specification} that passes when the value of the data item + * crosses a value passed to the constructor. The specification will never pass + * on the first evaluation. Instead, the first data item is used to determine + * if the value is currently greater than (or less than) the threshold value + * (passed to the constructor). This determines if the passing condition means + * the value must be less than (or greater than) the threshold. + * + * @public + * @extends {Specification} + * @param {Number} threshold + */ + class CrossesSpecification extends Specification { + constructor(threshold) { + super(); + + assert.argumentIsRequired(threshold, 'threshold', Number); + + this._threshold = threshold; + + this._previous = null; + } + + _evaluate(data) { + if (!is.number(data)) { + return false; + } + + const current = data; + const previous = this._previous; + + const crossed = previous !== null && + ( + (previous > this._threshold && !(current > this._threshold)) || + (previous < this._threshold && !(current < this._threshold)) + ); + + this._previous = current; + + return crossed; + } + + toString() { + return '[CrossesSpecification]'; + } + } + + return CrossesSpecification; +})(); \ No newline at end of file diff --git a/test/specs/specifications/CrossesSpec.js b/test/specs/specifications/CrossesSpec.js new file mode 100644 index 0000000..c31cc29 --- /dev/null +++ b/test/specs/specifications/CrossesSpec.js @@ -0,0 +1,189 @@ +const Crosses = require('./../../../specifications/Crosses'); + +describe('When a Crosses specification is initialized with a threshold of 1000', () => { + 'use strict'; + + let specification; + + beforeEach(() => { + specification = new Crosses(1000); + }); + + describe('and the first value evaluated is 900', () => { + let r1; + + beforeEach(() => { + r1 = specification.evaluate(900); + }); + + it('should not pass', () => { + expect(r1).toEqual(false); + }); + + describe('and the second value evaluated is 1100', () => { + let r2; + + beforeEach(() => { + r2 = specification.evaluate(1100); + }); + + it('should pass', () => { + expect(r2).toEqual(true); + }); + + describe('and the third value evaluated is 999', () => { + let r3; + + beforeEach(() => { + r3 = specification.evaluate(999); + }); + + it('should pass', () => { + expect(r3).toEqual(true); + }); + }); + + describe('and the third value evaluated is 1001', () => { + let r3; + + beforeEach(() => { + r3 = specification.evaluate(1001); + }); + + it('should not pass', () => { + expect(r3).toEqual(false); + }); + }); + }); + + describe('and the second value evaluated is 950', () => { + let r2; + + beforeEach(() => { + r2 = specification.evaluate(950); + }); + + it('should not pass', () => { + expect(r2).toEqual(false); + }); + }); + }); + + describe('and the first value evaluated is 1200', () => { + let r1; + + beforeEach(() => { + r1 = specification.evaluate(1200); + }); + + it('should not pass', () => { + expect(r1).toEqual(false); + }); + + describe('and the second value evaluated is 1100', () => { + let r2; + + beforeEach(() => { + r2 = specification.evaluate(1100); + }); + + it('should not pass', () => { + expect(r2).toEqual(false); + }); + }); + + describe('and the second value evaluated is 950', () => { + let r2; + + beforeEach(() => { + r2 = specification.evaluate(950); + }); + + it('should pass', () => { + expect(r2).toEqual(true); + }); + }); + }); +}); + +describe('When a Crosses specification is initialized with a threshold of zero', () => { + 'use strict'; + + let specification; + + beforeEach(() => { + specification = new Crosses(0); + }); + + describe('and the first value evaluated is 1', () => { + let r1; + + beforeEach(() => { + r1 = specification.evaluate(1); + }); + + it('should not pass', () => { + expect(r1).toEqual(false); + }); + + describe('and the second value evaluated is -1', () => { + let r2; + + beforeEach(() => { + r2 = specification.evaluate(-1); + }); + + it('should pass', () => { + expect(r2).toEqual(true); + }); + }); + + describe('and the second value evaluated is 0.5', () => { + let r2; + + beforeEach(() => { + r2 = specification.evaluate(0.5); + }); + + it('should not pass', () => { + expect(r2).toEqual(false); + }); + }); + }); + + describe('and the first value evaluated is -1', () => { + let r1; + + beforeEach(() => { + r1 = specification.evaluate(-1); + }); + + it('should not pass', () => { + expect(r1).toEqual(false); + }); + + describe('and the second value evaluated is -0.5', () => { + let r2; + + beforeEach(() => { + r2 = specification.evaluate(-0.5); + }); + + it('should not pass', () => { + expect(r2).toEqual(false); + }); + }); + + describe('and the second value evaluated is 1', () => { + let r2; + + beforeEach(() => { + r2 = specification.evaluate(1); + }); + + it('should pass', () => { + expect(r2).toEqual(true); + }); + }); + }); +});