From d1c192cc867509599dfe2ec40624eb770efbad24 Mon Sep 17 00:00:00 2001 From: Stephan Besser Date: Wed, 4 Dec 2024 15:07:54 +0100 Subject: [PATCH] feat: add DmsUnsignedDelimitedFormat for parsing DMS unsigned delimited coordinates and include related tests --- README.md | 9 ++ src/formats/dms-unsigned-delimited-format.ts | 78 +++++++++++++++++ src/parser.ts | 2 + tests/dms-unsigned-delimited-format.test.ts | 41 +++++++++ tests/parser.test.ts | 92 ++++++++++---------- 5 files changed, 178 insertions(+), 44 deletions(-) create mode 100644 src/formats/dms-unsigned-delimited-format.ts create mode 100644 tests/dms-unsigned-delimited-format.test.ts diff --git a/README.md b/README.md index f64bce3..f6ace97 100644 --- a/README.md +++ b/README.md @@ -121,4 +121,13 @@ Currently the out-of-the-box format parsers supports various formats and handles - `N40°7'23", W74°7'23"` - `N40°7'23"W74°7'23"` - `N 40°7'23.123" W 74°7'23.123"` +- `40:7:23 -74:7:23` +- `40:7:23, -74:7:23` +- `40:7:23.123, -74:7:23.123` +- `` +- `` +- `` +- `` +- `` +- `` - `` diff --git a/src/formats/dms-unsigned-delimited-format.ts b/src/formats/dms-unsigned-delimited-format.ts new file mode 100644 index 0000000..6867b34 --- /dev/null +++ b/src/formats/dms-unsigned-delimited-format.ts @@ -0,0 +1,78 @@ +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*\,?\s*(-?\d+):(\d+):(\d+(?:\.\d+)?)$/; + +/** + * Parses coordinates strings in DMS unsigned delimited format. Coordinate ordering is + * always latitude, longitude. + * + * Supported formats: + * + * 40:7:23 -74:7:23 + * 40:7:23, -74:7:23 + * 40:7:23.123, -74:7:23.123 + */ +export class DmsUnsignedDelimitedFormat extends BaseFormat { + parse(coordinateString: string): Coordinate { + validateSchema(coordinateString, z.string(), { assert: true, name: 'coordinateString' }); + + if (DmsUnsignedDelimitedFormat.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 matchLonDegree = match[4]; + const matchLonMinutes = match[5]; + const matchLonSeconds = match[6]; + + this.enforceValidLatitudeDegrees(matchLatDegree); + this.enforceValidMinutes(matchLatMinutes); + this.enforceValidSeconds(matchLatSeconds); + this.enforceValidLongitudeDegrees(matchLonDegree); + 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: latDegree < 0 ? 'S' : 'N', + }); + const decimalLon = this.dmsToDecimal({ + degrees: Math.abs(lonDegree), + minutes: lonMinutes, + seconds: lonSeconds, + direction: lonDegree < 0 ? 'W' : 'E', + }); + + 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 e8d2f96..99d6a7d 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -11,6 +11,7 @@ import { DmUnsignedSuffixedHemisphereFormat } from './formats/dm-unsigned-suffix 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 { DmsUnsignedFormat } from './formats/dms-unsigned-format.js'; import type { Coordinate } from './types.js'; import { validateSchema } from './validate-schema.js'; @@ -59,6 +60,7 @@ export class Parser { new DmsSignedFormat({ precision: precision }), new DmsSignedPrefixedHemisphereFormat({ precision: precision }), new DmsSignedSuffixedHemisphereFormat({ precision: precision }), + new DmsUnsignedDelimitedFormat({ precision: precision }), new DmsUnsignedFormat({ precision: precision }), ]; let formatParsers = options?.formatParsers || defaultParsers; diff --git a/tests/dms-unsigned-delimited-format.test.ts b/tests/dms-unsigned-delimited-format.test.ts new file mode 100644 index 0000000..4fd3d84 --- /dev/null +++ b/tests/dms-unsigned-delimited-format.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from 'vitest'; +import { DmsUnsignedDelimitedFormat } from '../src/formats/dms-unsigned-delimited-format.js'; + +describe('canParse', () => { + it('returns true for known formats', () => { + expect(DmsUnsignedDelimitedFormat.canParse(`40:7:23 -74:7:23`)).toBe(true); + expect(DmsUnsignedDelimitedFormat.canParse(`40:7:23, -74:7:23`)).toBe(true); + expect(DmsUnsignedDelimitedFormat.canParse(`40:7:23,-74:7:23`)).toBe(true); + expect(DmsUnsignedDelimitedFormat.canParse(`40:7:23.9999, -74:7:23.9999`)).toBe(true); + }); +}); +describe('parse', () => { + it(`returns the correct latitude and longitude for 40:7:23 -74:7:23`, () => { + const formatParser = new DmsUnsignedDelimitedFormat(); + const result = formatParser.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 formatParser = new DmsUnsignedDelimitedFormat(); + const result = formatParser.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 formatParser = new DmsUnsignedDelimitedFormat(); + const result = formatParser.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 formatParser = new DmsUnsignedDelimitedFormat({ precision: 5 }); + const result = formatParser.parse(`40:7:23.9999, -74:7:23.9999`); + + 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 55b96af..98632e3 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -334,50 +334,6 @@ describe('Test that all configured format parsers do not interfere', () => { expect(result.longitude).toBe(-74.12333); }); }); - describe('test dsm unsigned 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 -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 signed prefixed hemisphere format', () => { it(`returns the correct latitude and longitude for N40°7'23" W74°7'23"`, () => { const parser = new Parser(); @@ -479,5 +435,53 @@ describe('Test that all configured format parsers do not interfere', () => { expect(result.longitude).toBe(-74.12333); }); }); + describe('test dms unsigned delimited format', () => { + + }); + describe('test dms unsigned 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 -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', () => {}); // describe('test', () => {}); });