From 6833c490aa7d5bb4682c565ea6363bcc8a444efb Mon Sep 17 00:00:00 2001 From: Stephan Besser Date: Wed, 4 Dec 2024 15:24:06 +0100 Subject: [PATCH] feat: add DmsUnsignedDelimitedSuffixedHemisphereFormat for parsing DMS unsigned delimited suffixed hemisphere coordinates and include related tests --- README.md | 8 +- ...ed-delimited-suffixed-hemisphere-format.ts | 81 +++++++++++++++++++ src/parser.ts | 2 + ...signed-prefixed-hemisphere-format.test.ts} | 0 ...s-signed-suffixed-hemisphereformat.test.ts | 65 --------------- ...limited-suffixed-hemisphere-format.test.ts | 49 +++++++++++ tests/parser.test.ts | 62 ++++++++++++++ 7 files changed, 198 insertions(+), 69 deletions(-) create mode 100644 src/formats/dms-unsigned-delimited-suffixed-hemisphere-format.ts rename tests/{dms-signed-prefixed-hemisphereformat.test.ts => dms-signed-prefixed-hemisphere-format.test.ts} (100%) delete mode 100644 tests/dms-signed-suffixed-hemisphereformat.test.ts create mode 100644 tests/dms-unsigned-delimited-suffixed-hemisphere-format.test.ts diff --git a/README.md b/README.md index f6ace97..6c79f16 100644 --- a/README.md +++ b/README.md @@ -124,10 +124,10 @@ Currently the out-of-the-box format parsers supports various formats and handles - `40:7:23 -74:7:23` - `40:7:23, -74:7:23` - `40:7:23.123, -74:7:23.123` -- `` -- `` -- `` -- `` +- `40:7:23N 74:7:23W` +- `40:7:23N, 74:7:23W` +- `40:7:23N74:7:23W` +- `40:7:23.123N 74:7:23.123W` - `` - `` - `` diff --git a/src/formats/dms-unsigned-delimited-suffixed-hemisphere-format.ts b/src/formats/dms-unsigned-delimited-suffixed-hemisphere-format.ts new file mode 100644 index 0000000..d5a7096 --- /dev/null +++ b/src/formats/dms-unsigned-delimited-suffixed-hemisphere-format.ts @@ -0,0 +1,81 @@ +import { z } from 'zod'; +import type { Coordinate } from '../types.js'; +import { validateSchema } from '../validate-schema.js'; +import { BaseFormat } from './base-format.js'; + +const REGEX = /^(\d+):(\d+):(\d+(?:\.\d+)?)\s*([NS])\s*,?\s*(\d+):(\d+):(\d+(?:\.\d+)?)\s*([EW])$/; + +/** + * Parses coordinates strings in DMS unsigned delimited suffixed hemisphere format. Coordinate ordering is + * always latitude, longitude. + * + * Supported formats: + * + * 40:7:23N 74:7:23W + * 40:7:23N, 74:7:23W + * 40:7:23N74:7:23W + * 40:7:23.123N 74:7:23.123W + */ +export class DmsUnsignedDelimitedSuffixedHemisphereFormat extends BaseFormat { + parse(coordinateString: string): Coordinate { + validateSchema(coordinateString, z.string(), { assert: true, name: 'coordinateString' }); + + if (DmsUnsignedDelimitedSuffixedHemisphereFormat.canParse(coordinateString) === false) { + throw new Error('Invalid coordinate string'); + } + // use the regex to parse the latitude and longitude + const match = coordinateString.match(REGEX); + if (match == null) { + throw new Error('Invalid coordinate string'); + } + const matchLatDegree = match[1]; + const matchLatMinutes = match[2]; + const matchLatSeconds = match[3]; + const matchLatDirection = match[4]; + const matchLonDegree = match[5]; + const matchLonMinutes = match[6]; + const matchLonSeconds = match[7]; + const matchLonDirection = match[8]; + + this.enforceValidLatitudeDegrees(matchLatDegree, false); + this.enforceValidMinutes(matchLatMinutes); + this.enforceValidSeconds(matchLatSeconds); + this.enforceValidLongitudeDegrees(matchLonDegree, false); + this.enforceValidMinutes(matchLonMinutes); + this.enforceValidSeconds(matchLonSeconds); + + const latDegree = Number.parseInt(matchLatDegree); + const latMinutes = Number.parseInt(matchLatMinutes); + const latSeconds = Number.parseFloat(matchLatSeconds); + const lonDegree = Number.parseInt(matchLonDegree); + const lonMinutes = Number.parseInt(matchLonMinutes); + const lonSeconds = Number.parseFloat(matchLonSeconds); + + const decimalLat = this.dmsToDecimal({ + degrees: Math.abs(latDegree), + minutes: latMinutes, + seconds: latSeconds, + direction: matchLatDirection, + }); + const decimalLon = this.dmsToDecimal({ + degrees: Math.abs(lonDegree), + minutes: lonMinutes, + seconds: lonSeconds, + direction: matchLonDirection, + }); + + const lat = decimalLat.toFixed(this.precision); + const lon = decimalLon.toFixed(this.precision); + + return { + latitude: parseFloat(lat), + longitude: parseFloat(lon), + }; + } + + static canParse(coordinateString: string): boolean { + validateSchema(coordinateString, z.string(), { assert: true, name: 'coordinateString' }); + + return REGEX.test(coordinateString); + } +} diff --git a/src/parser.ts b/src/parser.ts index 99d6a7d..d608142 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -12,6 +12,7 @@ import { DmsSignedFormat } from './formats/dms-signed-format.js'; import { DmsSignedPrefixedHemisphereFormat } from './formats/dms-signed-prefixed-hemisphere-format.js'; import { DmsSignedSuffixedHemisphereFormat } from './formats/dms-signed-suffixed-hemisphere-format.js'; import { DmsUnsignedDelimitedFormat } from './formats/dms-unsigned-delimited-format.js'; +import { DmsUnsignedDelimitedSuffixedHemisphereFormat } from './formats/dms-unsigned-delimited-suffixed-hemisphere-format.js'; import { DmsUnsignedFormat } from './formats/dms-unsigned-format.js'; import type { Coordinate } from './types.js'; import { validateSchema } from './validate-schema.js'; @@ -61,6 +62,7 @@ export class Parser { new DmsSignedPrefixedHemisphereFormat({ precision: precision }), new DmsSignedSuffixedHemisphereFormat({ precision: precision }), new DmsUnsignedDelimitedFormat({ precision: precision }), + new DmsUnsignedDelimitedSuffixedHemisphereFormat({ precision: precision }), new DmsUnsignedFormat({ precision: precision }), ]; let formatParsers = options?.formatParsers || defaultParsers; diff --git a/tests/dms-signed-prefixed-hemisphereformat.test.ts b/tests/dms-signed-prefixed-hemisphere-format.test.ts similarity index 100% rename from tests/dms-signed-prefixed-hemisphereformat.test.ts rename to tests/dms-signed-prefixed-hemisphere-format.test.ts diff --git a/tests/dms-signed-suffixed-hemisphereformat.test.ts b/tests/dms-signed-suffixed-hemisphereformat.test.ts deleted file mode 100644 index 6833cd9..0000000 --- a/tests/dms-signed-suffixed-hemisphereformat.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { DmsSignedSuffixedHemisphereFormat } from '../src/formats/dms-signed-suffixed-hemisphere-format.js'; - -describe('canParse', () => { - it('returns true for known formats', () => { - expect(DmsSignedSuffixedHemisphereFormat.canParse(`40°7'23"N 74°7'23"W`)).toBe(true); - expect(DmsSignedSuffixedHemisphereFormat.canParse(`40°7'23"N74°7'23"W`)).toBe(true); - expect(DmsSignedSuffixedHemisphereFormat.canParse(`40° 7' 23" N 74° 7' 23" W`)).toBe(true); - expect(DmsSignedSuffixedHemisphereFormat.canParse(`40°7'23"N, 74°7'23"W`)).toBe(true); - expect(DmsSignedSuffixedHemisphereFormat.canParse(`40°7'23"N,74°7'23"W`)).toBe(true); - expect(DmsSignedSuffixedHemisphereFormat.canParse(`40° 7' 23" N, 74° 7' 23" W`)).toBe(true); - expect(DmsSignedSuffixedHemisphereFormat.canParse(`40° 7' 23.9999" N, 74° 7' 23.9999" W`)).toBe(true); - }); -}); -describe('parse', () => { - it(`returns the correct latitude and longitude for 40°7'23"N 74°7'23"W`, () => { - const formatParser = new DmsSignedSuffixedHemisphereFormat(); - const result = formatParser.parse(`40°7'23"N 74°7'23"W`); - expect(result.latitude).toBe(40.123); - expect(result.longitude).toBe(-74.123); - }); - - it(`returns the correct latitude and longitude for 40°7'23"N74°7'23"W`, () => { - const formatParser = new DmsSignedSuffixedHemisphereFormat(); - const result = formatParser.parse(`40°7'23"N74°7'23"W`); - expect(result.latitude).toBe(40.123); - expect(result.longitude).toBe(-74.123); - }); - - it(`returns the correct latitude and longitude for 40° 7' 23" N 74° 7' 23" W`, () => { - const formatParser = new DmsSignedSuffixedHemisphereFormat(); - const result = formatParser.parse(`40° 7' 23" N 74° 7' 23" W`); - expect(result.latitude).toBe(40.123); - expect(result.longitude).toBe(-74.123); - }); - - it(`returns the correct latitude and longitude for 40°7'23"N, 74°7'23"W`, () => { - const formatParser = new DmsSignedSuffixedHemisphereFormat(); - const result = formatParser.parse(`40°7'23"N, 74°7'23"W`); - expect(result.latitude).toBe(40.123); - expect(result.longitude).toBe(-74.123); - }); - - it(`returns the correct latitude and longitude for 40°7'23"N,74°7'23"W`, () => { - const formatParser = new DmsSignedSuffixedHemisphereFormat(); - const result = formatParser.parse(`40°7'23"N,74°7'23"W`); - expect(result.latitude).toBe(40.123); - expect(result.longitude).toBe(-74.123); - }); - - it(`returns the correct latitude and longitude for 40° 7' 23" N, 74° 7' 23" W`, () => { - const formatParser = new DmsSignedSuffixedHemisphereFormat(); - const result = formatParser.parse(`40° 7' 23" N, 74° 7' 23" W`); - expect(result.latitude).toBe(40.123); - expect(result.longitude).toBe(-74.123); - }); - - it(`returns the correct latitude and longitude for 40° 7' 23.9999" N, 74° 7' 23.9999" W`, () => { - const formatParser = new DmsSignedSuffixedHemisphereFormat({ precision: 5 }); - const result = formatParser.parse(`40° 7' 23.9999" N, 74° 7' 23.9999" W`); - - expect(result.latitude).toBe(40.12333); - expect(result.longitude).toBe(-74.12333); - }); -}); diff --git a/tests/dms-unsigned-delimited-suffixed-hemisphere-format.test.ts b/tests/dms-unsigned-delimited-suffixed-hemisphere-format.test.ts new file mode 100644 index 0000000..e4216a3 --- /dev/null +++ b/tests/dms-unsigned-delimited-suffixed-hemisphere-format.test.ts @@ -0,0 +1,49 @@ +import { describe, expect, it } from 'vitest'; +import { DmsUnsignedDelimitedSuffixedHemisphereFormat } from '../src/formats/dms-unsigned-delimited-suffixed-hemisphere-format.js'; + +describe('canParse', () => { + it('returns true for known formats', () => { + expect(DmsUnsignedDelimitedSuffixedHemisphereFormat.canParse(`40:7:23N 74:7:23W`)).toBe(true); + expect(DmsUnsignedDelimitedSuffixedHemisphereFormat.canParse(`40:7:23N74:7:23W`)).toBe(true); + expect(DmsUnsignedDelimitedSuffixedHemisphereFormat.canParse(`40:7:23N, 74:7:23W`)).toBe(true); + expect(DmsUnsignedDelimitedSuffixedHemisphereFormat.canParse(`40:7:23N,74:7:23W`)).toBe(true); + expect(DmsUnsignedDelimitedSuffixedHemisphereFormat.canParse(`40:7:23.9999N, 74:7:23.9999W`)).toBe(true); + }); +}); +describe('parse', () => { + it(`returns the correct latitude and longitude for 40:7:23N 74:7:23W`, () => { + const formatParser = new DmsUnsignedDelimitedSuffixedHemisphereFormat(); + const result = formatParser.parse(`40:7:23N 74:7:23W`); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it(`returns the correct latitude and longitude for 40:7:23N74:7:23W`, () => { + const formatParser = new DmsUnsignedDelimitedSuffixedHemisphereFormat(); + const result = formatParser.parse(`40:7:23N74:7:23W`); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it(`returns the correct latitude and longitude for 40:7:23N, 74:7:23W`, () => { + const formatParser = new DmsUnsignedDelimitedSuffixedHemisphereFormat(); + const result = formatParser.parse(`40:7:23N, 74:7:23W`); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it(`returns the correct latitude and longitude for 40:7:23N,74:7:23W`, () => { + const formatParser = new DmsUnsignedDelimitedSuffixedHemisphereFormat(); + const result = formatParser.parse(`40:7:23N,74:7:23W`); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it(`returns the correct latitude and longitude for 40:7:23.9999N, 74:7:23.9999W`, () => { + const formatParser = new DmsUnsignedDelimitedSuffixedHemisphereFormat({ precision: 5 }); + const result = formatParser.parse(`40:7:23.9999N, 74:7:23.9999W`); + + expect(result.latitude).toBe(40.12333); + expect(result.longitude).toBe(-74.12333); + }); +}); diff --git a/tests/parser.test.ts b/tests/parser.test.ts index 98632e3..389dbb1 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -436,7 +436,69 @@ describe('Test that all configured format parsers do not interfere', () => { }); }); describe('test dms unsigned delimited format', () => { + it(`returns the correct latitude and longitude for 40:7:23 -74:7:23`, () => { + const parser = new Parser(); + const result = parser.parse(`40:7:23 -74:7:23`); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it(`returns the correct latitude and longitude for 40:7:23, -74:7:23`, () => { + const parser = new Parser(); + const result = parser.parse(`40:7:23, -74:7:23`); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it(`returns the correct latitude and longitude for 40:7:23,-74:7:23`, () => { + const parser = new Parser(); + const result = parser.parse(`40:7:23,-74:7:23`); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + it(`returns the correct latitude and longitude for 40:7:23.9999", -74:7:23.9999`, () => { + const parser = new Parser({ precision: 5 }); + const result = parser.parse(`40:7:23.9999, -74:7:23.9999`); + expect(result.latitude).toBe(40.12333); + expect(result.longitude).toBe(-74.12333); + }); + }); + describe('test dms unsigned delimited suffixed hemisphere format', () => { + it(`returns the correct latitude and longitude for 40:7:23N 74:7:23W`, () => { + const parser = new Parser(); + const result = parser.parse(`40:7:23N 74:7:23W`); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it(`returns the correct latitude and longitude for 40:7:23N74:7:23W`, () => { + const parser = new Parser(); + const result = parser.parse(`40:7:23N74:7:23W`); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it(`returns the correct latitude and longitude for 40:7:23N, 74:7:23W`, () => { + const parser = new Parser(); + const result = parser.parse(`40:7:23N, 74:7:23W`); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it(`returns the correct latitude and longitude for 40:7:23N,74:7:23W`, () => { + const parser = new Parser(); + const result = parser.parse(`40:7:23N,74:7:23W`); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it(`returns the correct latitude and longitude for 40:7:23.9999N, 74:7:23.9999W`, () => { + const parser = new Parser({ precision: 5 }); + const result = parser.parse(`40:7:23.9999N, 74:7:23.9999W`); + expect(result.latitude).toBe(40.12333); + expect(result.longitude).toBe(-74.12333); + }); }); describe('test dms unsigned format', () => { it(`returns the correct latitude and longitude for 40 7 23 -74 7 23`, () => {