diff --git a/src/core/coordinates/validCoordinate.js b/src/core/coordinates/validCoordinate.js new file mode 100644 index 0000000..9c31694 --- /dev/null +++ b/src/core/coordinates/validCoordinate.js @@ -0,0 +1,29 @@ +const { valid2DCoordinate } = require('./valid2DCoordinate') +const { valid3DCoordinate } = require('./valid3DCoordinate') + +/** + * A helper function used to verify a coordinate has appropriate longitude, latitude, and altitude values. + * + * @memberof Core.Coordinates + * @param {GeoJSON2DCoordinate|GeoJSON3DCoordinate} coordinate A WGS-84 array of [longitude, latitude] or [longitude, latitude, alititude] + * @returns {boolean} True if a valid 3D GeoJSON coordinate. If invalid, it will throw an error. +// * @throws {Error} Input must be an array of only three elments +// * @throws {Error} Altitude value must be numeric + */ +function validCoordinate(coordinate) { + if (!Array.isArray(coordinate) || coordinate.length < 2 || coordinate.length > 3) { + throw new Error('Input must be an array of either two or three elments.') + } + + if (coordinate.length === 2) { + valid2DCoordinate(coordinate) + } + + if (coordinate.length === 3) { + valid3DCoordinate(coordinate) + } + + return true +} + +exports.validCoordinate = validCoordinate diff --git a/src/index.js b/src/index.js index ab5606c..66f1d38 100644 --- a/src/index.js +++ b/src/index.js @@ -56,6 +56,7 @@ exports.isValid2DCoordinate = require('./matchers/coordinates/isValid2DCoordinate').isValid2DCoordinate exports.isValid3DCoordinate = require('./matchers/coordinates/isValid3DCoordinate').isValid3DCoordinate +exports.isValidCoordinate = require('./matchers/coordinates/isValidCoordinate').isValidCoordinate // Features // Geometries diff --git a/src/matchers/coordinates/isValid2DCoordinate.js b/src/matchers/coordinates/isValid2DCoordinate.js index 27a1f49..612a82e 100644 --- a/src/matchers/coordinates/isValid2DCoordinate.js +++ b/src/matchers/coordinates/isValid2DCoordinate.js @@ -12,8 +12,9 @@ const { valid2DCoordinate } = require('../../core/coordinates/valid2DCoordinate' * expect([22, 45.733]).isValid2DCoordinate() * expect([180, 90]).isValid2DCoordinate() * @example - * expect([22, 100.56]).not.isValid2DCoordinate() - * expect([22, 45.733, 0]).not.isValid2DCoordinate() + * expect([22, 100.56]).not.isValid2DCoordinate() // Latitude out of range + * expect([22, 45.733, 0]).not.isValid2DCoordinate() //3D coordinate + * // Nested Arrays * expect([[22, 45.733, 0]]).not.isValid2DCoordinate() * expect([[22, 45.733], [180, 90]]).not.isValid2DCoordinate() */ diff --git a/src/matchers/coordinates/isValid3DCoordinate.js b/src/matchers/coordinates/isValid3DCoordinate.js index c08c09b..d475545 100644 --- a/src/matchers/coordinates/isValid3DCoordinate.js +++ b/src/matchers/coordinates/isValid3DCoordinate.js @@ -15,8 +15,10 @@ const { valid3DCoordinate } = require('../../core/coordinates/valid3DCoordinate' * expect([22, 45.733, 20]).isValid3DCoordinate() * expect([180, 90, -10000]).isValid3DCoordinate() * @example - * expect([22, 100.56]).not.isValid3DCoordinate() - * expect([22, 45.733, '0']).not.isValid3DCoordinate() + * expect([22, 100.56, 0]).not.isValid3DCoordinate() // Latitude out of range + * expect([22, 45.733]).isValid3DCoordinate() // 2D coordinate + * expect([22, 45.733, '0']).not.isValid3DCoordinate() // Non-numeric altitude + * // Nested Arrays * expect([[22, 45.733, 0]]).not.isValid3DCoordinate() * expect([[22, 45.733, 0], [180, 90, 0]]).not.isValid3DCoordinate() */ diff --git a/src/matchers/coordinates/isValidCoordinate.js b/src/matchers/coordinates/isValidCoordinate.js new file mode 100644 index 0000000..c083084 --- /dev/null +++ b/src/matchers/coordinates/isValidCoordinate.js @@ -0,0 +1,62 @@ +const { validCoordinate } = require('../../core/coordinates/validCoordinate') + +// eslint-disable-next-line jsdoc/require-returns +/** + * Verifies a two or three element coordinate meets WGS-84 and GeoJSON validity requirements. + * + * @memberof Coordinates + * @param {number[]} coordinateArray A two or three element array of numbers in format + * [longitude, latitude] or [longitude, latitude, altitude]. + * + * Longitude must be between -180 to 180. + * Latitude must be between -90 to 90. + * Altitude must be a number between -Infinity to Infinity. + * The standard does not specify units altitude represents (i.e. meters, feet, etc.). + * @example + * expect([22, 45.733]).isValidCoordinate() + * expect([180, 90]).isValidCoordinate() + * expect([22, 45.733, 20]).isValidCoordinate() + * expect([180, 90, -10000]).isValidCoordinate() + * @example + * expect([220, 56]).not.isValidCoordinate() // Longitude out of range + * expect([22, 45.733, '0']).not.isValidCoordinate() + * // Nested Arrays + * expect([[22, 45.733, 0]]).not.isValidCoordinate() + * expect([[22, 45.733, 0], [180, 90, 0]]).not.isValidCoordinate() + */ +function isValidCoordinate(coordinateArray) { + const { printReceived, matcherHint } = this.utils + const passMessage = + // eslint-disable-next-line prefer-template + matcherHint('.not.isValidCoordinate', '[longitude, latitude, (altitude)]', '') + + '\n\n' + + `Expected input to not be a two or three element array with longitude between (-90 to 90), + latitude between (-180 to 180), and (if a 3D coordinate) numeric altitude.\n\n` + + `Received: ${printReceived(coordinateArray)}` + + /** + * Combines a custom error message with built in Jest tools to provide a more descriptive error + * meessage to the end user. + * + * @param {string} errorMessage Error message text to return to the user + * @returns {string} Concatenated Jest test result string + */ + function failMessage(errorMessage) { + return ( + // eslint-disable-next-line prefer-template, no-unused-expressions + matcherHint('.isValidCoordinate', '[longitude, latitude, (altitude)]', '') + + '\n\n' + + `${errorMessage}\n\n` + + `Received: ${printReceived(coordinateArray)}` + ) + } + + try { + validCoordinate(coordinateArray) + } catch (err) { + return { pass: false, message: () => failMessage(err.message) } + } + return { pass: true, message: () => passMessage } +} + +exports.isValidCoordinate = isValidCoordinate diff --git a/tests/coordinates/__snapshots__/isValidCoordinate.test.js.snap b/tests/coordinates/__snapshots__/isValidCoordinate.test.js.snap new file mode 100644 index 0000000..1c3ce7c --- /dev/null +++ b/tests/coordinates/__snapshots__/isValidCoordinate.test.js.snap @@ -0,0 +1,35 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Error Snapshot Testing. Throws error: expect([0, 0, 0]).isValidCoordinate 1`] = ` +"expect([longitude, latitude, (altitude)]).isValidCoordinate() + +Latitude must be a number between -90 and 90. + +Received: [0, 95, 0]" +`; + +exports[`Error Snapshot Testing. Throws error: expect([0, 0, 0]).not.isValidCoordinate 1`] = ` +"expect([longitude, latitude, (altitude)]).not.isValidCoordinate() + +Expected input to not be a two or three element array with longitude between (-90 to 90), + latitude between (-180 to 180), and (if a 3D coordinate) numeric altitude. + +Received: [0, 0, 0]" +`; + +exports[`Error Snapshot Testing. Throws error: expect([0, 0]).isValidCoordinate 1`] = ` +"expect([longitude, latitude, (altitude)]).isValidCoordinate() + +Latitude must be a number between -90 and 90. + +Received: [0, 95]" +`; + +exports[`Error Snapshot Testing. Throws error: expect([0, 0]).not.isValidCoordinate 1`] = ` +"expect([longitude, latitude, (altitude)]).not.isValidCoordinate() + +Expected input to not be a two or three element array with longitude between (-90 to 90), + latitude between (-180 to 180), and (if a 3D coordinate) numeric altitude. + +Received: [0, 0]" +`; diff --git a/tests/coordinates/isValid2DCoordinate.test.js b/tests/coordinates/isValid2DCoordinate.test.js index 8469796..41c1a5b 100644 --- a/tests/coordinates/isValid2DCoordinate.test.js +++ b/tests/coordinates/isValid2DCoordinate.test.js @@ -1,33 +1,33 @@ const goodCoordinates = [ - [0, 0], - [102.0, 0.5], - [172.0, -15], - [-10.9, 77], - [-152.0, -33.33333] + [[0, 0]], + [[102.0, 0.5]], + [[172.0, -15]], + [[-10.9, 77]], + [[-152.0, -33.33333]] ] const goodBoundaryCoordinates = [ - [180, 0], - [-180, 0], - [0, 90], - [0, -90], - [180, 90], - [180, -90], - [-180, 90], - [-180, -90] + [[180, 0]], + [[-180, 0]], + [[0, 90]], + [[0, -90]], + [[180, 90]], + [[180, -90]], + [[-180, 90]], + [[-180, -90]] ] const coordinatesOutOfRange = [ - [0, 90.0000001], - [0, -90.0000001], - [0, 900000], - [0, -900000], - [180.0000001, 0], - [-180.0000001, 0], - [1800000, 0], - [-1800000, 0], - [181, 91], - [181, -91], - [-181, 91], - [-181, -91] + [[0, 90.0000001]], + [[0, -90.0000001]], + [[0, 900000]], + [[0, -900000]], + [[180.0000001, 0]], + [[-180.0000001, 0]], + [[1800000, 0]], + [[-1800000, 0]], + [[181, 91]], + [[181, -91]], + [[-181, 91]], + [[-181, -91]] ] const invalidInputValues = [ undefined, @@ -47,14 +47,16 @@ const invalidInputValues = [ ] describe('Valid Use Cases', () => { describe('Expect to pass with good coordinates:', () => { - test.each([...goodCoordinates])('expect([%p, %p])', (longitude, latitude) => { - expect([longitude, latitude]).isValid2DCoordinate() + test.each([...goodCoordinates])('expect(%p)', (coordinate) => { + expect(coordinate).isValid2DCoordinate() + expect(coordinate).isValidCoordinate() }) }) describe('Expect to pass with good boundary coordinates:', () => { - test.each([...goodBoundaryCoordinates])('expect([%p, %p])', (longitude, latitude) => { - expect([longitude, latitude]).isValid2DCoordinate() + test.each([...goodBoundaryCoordinates])('expect(%p)', (coordinate) => { + expect(coordinate).isValid2DCoordinate() + expect(coordinate).isValidCoordinate() }) }) }) @@ -63,6 +65,7 @@ describe('Inalid Use Cases', () => { describe('Expect to fail with bad inputs:', () => { test.each([...invalidInputValues])('expect(%p)', (badInput) => { expect(badInput).not.isValid2DCoordinate() + expect(badInput).not.isValidCoordinate() }) }) @@ -76,8 +79,9 @@ describe('Inalid Use Cases', () => { }) describe('Expect to fail with out of range coordinate:', () => { - test.each([...coordinatesOutOfRange])('expect([%p, %p])', (longitude, latitude) => { - expect([longitude, latitude]).not.isValid2DCoordinate() + test.each([...coordinatesOutOfRange])('expect(%p)', (coordinate) => { + expect(coordinate).not.isValid2DCoordinate() + expect(coordinate).not.isValidCoordinate() }) }) @@ -85,18 +89,21 @@ describe('Inalid Use Cases', () => { describe('Expect to fail with bad longitude value:', () => { test.each([...invalidInputValues])('expect([%p, 0])', (longitude) => { expect([longitude, 0]).not.isValid2DCoordinate() + expect([longitude, 0]).not.isValidCoordinate() }) }) describe('Expect to fail with bad latitude value:', () => { test.each([...invalidInputValues])('expect([0, %p])', (latitude) => { expect([0, latitude]).not.isValid2DCoordinate() + expect([0, latitude]).not.isValidCoordinate() }) }) describe('Expect to fail with bad values for both:', () => { test.each([...invalidInputValues])('expect(, ), = %p', (input) => { expect([input, input]).not.isValid2DCoordinate() + expect([input, input]).not.isValidCoordinate() }) }) }) @@ -109,6 +116,7 @@ describe('Inalid Use Cases', () => { ] test.each([[testArray], [[testArray]], [[[testArray]]]])('expect(%p)', (badInput) => { expect([badInput]).not.isValid2DCoordinate() + expect([badInput]).not.isValidCoordinate() }) }) }) diff --git a/tests/coordinates/isValid3DCoordinate.test.js b/tests/coordinates/isValid3DCoordinate.test.js index 2c77593..2bdf0ae 100644 --- a/tests/coordinates/isValid3DCoordinate.test.js +++ b/tests/coordinates/isValid3DCoordinate.test.js @@ -1,33 +1,33 @@ const goodCoordinates = [ - [0, 0, 0], - [102.0, 0.5, 1000], - [172.0, -15, -1000], - [-10.9, 77, 5000], - [-152.0, -33.33333, -5000] + [[0, 0, 0]], + [[102.0, 0.5, 1000]], + [[172.0, -15, -1000]], + [[-10.9, 77, 5000]], + [[-152.0, -33.33333, -5000]] ] const goodBoundaryCoordinates = [ - [180, 0, Infinity], - [-180, 0, Infinity], - [0, 90, Infinity], - [0, -90, Infinity], - [180, 90, -Infinity], - [180, -90, -Infinity], - [-180, 90, -Infinity], - [-180, -90, -Infinity] + [[180, 0, Infinity]], + [[-180, 0, Infinity]], + [[0, 90, Infinity]], + [[0, -90, Infinity]], + [[180, 90, -Infinity]], + [[180, -90, -Infinity]], + [[-180, 90, -Infinity]], + [[-180, -90, -Infinity]] ] const coordinatesOutOfRange = [ - [0, 90.0000001, 0], - [0, -90.0000001, 0], - [0, 900000, 0], - [0, -900000, 0], - [180.0000001, 0, 0], - [-180.0000001, 0, 0], - [1800000, 0, 0], - [-1800000, 0, 0], - [181, 91, 0], - [181, -91, 0], - [-181, 91, 0], - [-181, -91, 0] + [[0, 90.0000001, 0]], + [[0, -90.0000001, 0]], + [[0, 900000, 0]], + [[0, -900000, 0]], + [[180.0000001, 0, 0]], + [[-180.0000001, 0, 0]], + [[1800000, 0, 0]], + [[-1800000, 0, 0]], + [[181, 91, 0]], + [[181, -91, 0]], + [[-181, 91, 0]], + [[-181, -91, 0]] ] const invalidInputValues = [ undefined, @@ -45,7 +45,7 @@ const invalidInputValues = [ '[0, 0, 0]', '[[0, 0, 0], [0, 0, 0]]' ] -const InvalidAltitudeValues = [ +const invalidAltitudeValues = [ undefined, null, true, @@ -60,18 +60,17 @@ const InvalidAltitudeValues = [ describe('Valid Use Cases', () => { describe('Expect to pass with good coordinates:', () => { - test.each([...goodCoordinates])('expect([%p, %p, %p])', (longitude, latitude, altitude) => { - expect([longitude, latitude, altitude]).isValid3DCoordinate() + test.each([...goodCoordinates])('expect(%p)', (coordinate) => { + expect(coordinate).isValid3DCoordinate() + expect(coordinate).isValidCoordinate() }) }) describe('Expect to pass with good boundary coordinates:', () => { - test.each([...goodBoundaryCoordinates])( - 'expect([%p, %p, %p])', - (longitude, latitude, altitude) => { - expect([longitude, latitude, altitude]).isValid3DCoordinate() - } - ) + test.each([...goodBoundaryCoordinates])('expect([%p])', (coordinate) => { + expect(coordinate).isValid3DCoordinate() + expect(coordinate).isValidCoordinate() + }) }) }) @@ -79,6 +78,7 @@ describe('Invalid Use Cases', () => { describe('Expect to fail with bad inputs:', () => { test.each([...invalidInputValues])('expect(%p)', (badInput) => { expect(badInput).not.isValid3DCoordinate() + expect(badInput).not.isValidCoordinate() }) }) @@ -92,30 +92,31 @@ describe('Invalid Use Cases', () => { }) describe('Expect to fail with out of range lon/lat coordinates:', () => { - test.each([...coordinatesOutOfRange])( - 'expect([%p, %p, %p])', - (longitude, latitude, altitude) => { - expect([longitude, latitude, altitude]).not.isValid3DCoordinate() - } - ) + test.each([...coordinatesOutOfRange])('expect(%p)', (coordinate) => { + expect(coordinate).not.isValid3DCoordinate() + expect(coordinate).not.isValidCoordinate() + }) }) describe('Passing Bad Individual Coordinate Values', () => { describe('Expect to fail with bad longitude value:', () => { test.each([...invalidInputValues])('expect([%p, 0, 0])', (longitude) => { expect([longitude, 0, 0]).not.isValid3DCoordinate() + expect([longitude, 0, 0]).not.isValidCoordinate() }) }) describe('Expect to fail with bad latitude value:', () => { test.each([...invalidInputValues])('expect([0, %p, 0])', (latitude) => { expect([0, latitude, 0]).not.isValid3DCoordinate() + expect([0, latitude, 0]).not.isValidCoordinate() }) }) describe('Expect to fail with bad altitude value:', () => { - test.each([...InvalidAltitudeValues])('expect([0, 0, %p])', (altitude) => { + test.each([...invalidAltitudeValues])('expect([0, 0, %p])', (altitude) => { expect([0, 0, altitude]).not.isValid3DCoordinate() + expect([0, 0, altitude]).not.isValidCoordinate() }) }) @@ -124,6 +125,7 @@ describe('Invalid Use Cases', () => { 'expect(, , ), = %p', (input) => { expect([input, input, input]).not.isValid3DCoordinate() + expect([input, input, input]).not.isValidCoordinate() } ) }) @@ -131,12 +133,13 @@ describe('Invalid Use Cases', () => { describe('Expect to fail when arrays are nested too deeply:', () => { const testArray = [ - [10, 20], - [2, 90], - [95, 5] + [10, 20, 0], + [2, 90, 0], + [95, 5, 0] ] test.each([[testArray], [[testArray]], [[[testArray]]]])('expect(%p)', (badInput) => { expect([badInput]).not.isValid2DCoordinate() + expect([badInput]).not.isValidCoordinate() }) }) }) diff --git a/tests/coordinates/isValidCoordinate.test.js b/tests/coordinates/isValidCoordinate.test.js new file mode 100644 index 0000000..a68d4dc --- /dev/null +++ b/tests/coordinates/isValidCoordinate.test.js @@ -0,0 +1,33 @@ +// This matcher works on all the same valid2DCoordinate and valid3DCoordinate cases. +// Accordingly, those specific tests check that both functions work. +// This test suite checks the array elements sizing because 2D and 3D have different acceptance criteria. +// Finally, it tests the unique snapshots. + +describe('Invalid Use Cases', () => { + describe('Expect to fail with incorrect number of array elements:', () => { + test.each([[[]], [[20]], [[20, 30, 0, 4]], [[20, 30, 0, 20, 30, 0, 20, 30, 0]]])( + 'expect(%p)', + (badInput) => { + expect(badInput).not.isValidCoordinate() + } + ) + }) +}) + +describe('Error Snapshot Testing. Throws error:', () => { + test('expect([0, 0]).not.isValidCoordinate', () => { + expect(() => expect([0, 0]).not.isValidCoordinate()).toThrowErrorMatchingSnapshot() + }) + + test('expect([0, 0, 0]).not.isValidCoordinate', () => { + expect(() => expect([0, 0, 0]).not.isValidCoordinate()).toThrowErrorMatchingSnapshot() + }) + + test('expect([0, 0]).isValidCoordinate', () => { + expect(() => expect([0, 95]).isValidCoordinate()).toThrowErrorMatchingSnapshot() + }) + + test('expect([0, 0, 0]).isValidCoordinate', () => { + expect(() => expect([0, 95, 0]).isValidCoordinate()).toThrowErrorMatchingSnapshot() + }) +})