From 89badb1d88cc1a1c2e23448d95ca1e1378f02f6e Mon Sep 17 00:00:00 2001 From: Stephan Besser Date: Wed, 4 Dec 2024 11:33:32 +0100 Subject: [PATCH] feat: add DmUnsignedPrefixedHemisphereFormat for parsing DM coordinates and enhance tests --- README.md | 34 ++++---- .../dm-unsigned-prefixed-hemisphere-format.ts | 77 +++++++++++++++++++ src/parser.ts | 2 + tests/decimal-unsigned-format.test.ts | 21 ----- ...nsigned-prefixed-hemisphere-format.test.ts | 46 +++++++++++ ...nsigned-suffixed-hemisphere-format.test.ts | 9 +-- tests/parser.test.ts | 57 +++++++------- 7 files changed, 173 insertions(+), 73 deletions(-) create mode 100644 src/formats/dm-unsigned-prefixed-hemisphere-format.ts create mode 100644 tests/dm-unsigned-prefixed-hemisphere-format.test.ts diff --git a/README.md b/README.md index 225562e..00559cf 100644 --- a/README.md +++ b/README.md @@ -84,29 +84,29 @@ const parser = new Parser({ formatParsers: [customFormatParser, decimalParser] } ### Supported Formats -Currently the out-of-the-box format parsers support the following formats and to a degree their respective variants with or without whitespaces: +Currently the out-of-the-box format parsers supports various formats and handles their various variations ,e.g. with or without whitespaces, gracefully: -- `10, 12` -- `1.234, 5.678` -- `1.234,5.678` -- `1.234 5.678` -- `12N,56E` -- `12.234 N 56.678 E` -- `12.234 N, 56.678 E` -- `12.234N,56.678E` -- `12.234N56.678E` +- `1° 5°` - `1.234° 5.678°` - `1.234°, 5.678°` -- `1.234°,5.678°` +- `N 12° E 5°` +- `N 1.234° E 5.678°` +- `N 1.234°, E5.678°` +- `1° N 5° E` - `1.234° N 5.678° E` - `1.234° N, 5.678° E` -- `1.234°N,5.678°E` -- `1.234°N5.678°E` -- `4007N 7407W` -- `4007.38N7407.38W` +- `10, 12` +- `1.234, 5.678` +- `N 12, E 56` +- `N 12.234 E 56.678` +- `N 12.234, E 56.678` +- `12 N, 56 E` +- `12.234 N 56.678 E` +- `12.234 N, 56.678 E` +- `N 4007 W 7407` +- `N 4007.38 W 7407.38` +- `4007 N 7407 W` - `4007.38 N 7407.38 W` -- `4007.38N 7407.38W` -- `` - `` - `` - `` diff --git a/src/formats/dm-unsigned-prefixed-hemisphere-format.ts b/src/formats/dm-unsigned-prefixed-hemisphere-format.ts new file mode 100644 index 0000000..4113dad --- /dev/null +++ b/src/formats/dm-unsigned-prefixed-hemisphere-format.ts @@ -0,0 +1,77 @@ +import { z } from 'zod'; +import type { Coordinate, DmsCoordinate } from '../types.js'; +import { validateSchema } from '../validate-schema.js'; +import { BaseFormat } from './base-format.js'; + +const REGEX = /^([NS]\s*\d{3,4}(\.\d+)?\s*)\s*([EW]\s*\d{3,5}(\.\d+)?\s*)$/; + +/** + * Parses coordinates strings in DM format with decimal minutes. + * + * Supported formats: + * + * N4007 W7407 + * N4007.38W7407.38 + * N 4007.38 W 7407.38 + * N4007.38 W7407.38 + */ +export class DmUnsignedPrefixedHemisphereFormat extends BaseFormat { + parse(coordinateString: string): Coordinate { + validateSchema(coordinateString, z.string(), { assert: true, name: 'coordinateString' }); + + if (DmUnsignedPrefixedHemisphereFormat.canParse(coordinateString) === false) { + throw new Error('Invalid coordinate string'); + } + this.enforceNoHyphen(coordinateString); + // use the regex to parse the latitude and longitude + const match = coordinateString.match(REGEX); + if (match == null) { + throw new Error('Invalid coordinate string'); + } + const latitude = match[1]; + const longitude = match[3]; + // to DMS + const dmsLat = this.toDms(latitude.replace(/\s/g, '')); + const dmsLon = this.toDms(longitude.replace(/\s/g, '')); + // DMS to decimal + const decimalLat = this.dmsToDecimal(dmsLat); + const decimalLon = this.dmsToDecimal(dmsLon); + this.enforceValidLatitude(decimalLat); + this.enforceValidLongitude(decimalLon); + const lat = decimalLat.toFixed(this.precision); + const lon = decimalLon.toFixed(this.precision); + + return { + latitude: parseFloat(lat), + longitude: parseFloat(lon), + }; + } + + /** + * Converts a DMS notation coordinate like "4007.38N" to DMS parts. + * + * @param {string} value - The parsed DMS value, e.g. "4007.38N" or "74007.38W" + * @return {import('../types').openaip.CoordinateParser.DmsCoordinate} + */ + toDms(value: string): DmsCoordinate { + validateSchema(value, z.string(), { assert: true, name: 'value' }); + + const match = value.match(/^([NSWE])(\d{2,3})(\d{2})(\.\d+)?$/); + if (match) { + return { + degrees: parseInt(match[2], 10), + minutes: parseInt(match[3], 10), + seconds: Math.round(60 * parseFloat(`0${match[4]}`)), + direction: match[1], + }; + } else { + throw new Error('Invalid coordinate string'); + } + } + + 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 e7adeee..9f898c3 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -6,6 +6,7 @@ import { DecimalSignedSuffixedHemisphereFormat } from './formats/decimal-signed- import { DecimalUnsignedFormat } from './formats/decimal-unsigned-format.js'; import { DecimalUnsignedPrefixedHemisphereFormat } from './formats/decimal-unsigned-prefixed-hemisphere-format.js'; import { DecimalUnsignedSuffixedHemisphereFormat } from './formats/decimal-unsigned-suffixed-hemisphere-format.js'; +import { DmUnsignedPrefixedHemisphereFormat } from './formats/dm-unsigned-prefixed-hemisphere-format.js'; import { DmUnsignedSuffixedHemisphereFormat } from './formats/dm-unsigned-suffixed-hemisphere-format.js'; import type { Coordinate } from './types.js'; import { validateSchema } from './validate-schema.js'; @@ -49,6 +50,7 @@ export class Parser { new DecimalSignedFormat({ precision: precision }), new DecimalUnsignedPrefixedHemisphereFormat({ precision: precision }), new DecimalSignedSuffixedHemisphereFormat({ precision: precision }), + new DmUnsignedPrefixedHemisphereFormat({ precision: precision }), new DmUnsignedSuffixedHemisphereFormat({ precision: precision }), ]; let formatParsers = options?.formatParsers || defaultParsers; diff --git a/tests/decimal-unsigned-format.test.ts b/tests/decimal-unsigned-format.test.ts index 4c7c83b..e7eb724 100644 --- a/tests/decimal-unsigned-format.test.ts +++ b/tests/decimal-unsigned-format.test.ts @@ -39,25 +39,4 @@ describe('parse', () => { expect(result.latitude).toBe(1.234); expect(result.longitude).toBe(5.678); }); - - it("returns the correct latitude and longitude for '1.23412312 5.6782356' with precision 4", () => { - const formatParser = new DecimalUnsignedFormat({ precision: 4 }); - const result = formatParser.parse('1.23412312 5.6782356'); - expect(result.latitude).toBe(1.2341); - expect(result.longitude).toBe(5.6782); - }); - - it("returns the correct latitude and longitude for '-1.23412312 -5.6782356' with precision 4", () => { - const formatParser = new DecimalUnsignedFormat({ precision: 4 }); - const result = formatParser.parse('-1.23412312 -5.6782356'); - expect(result.latitude).toBe(-1.2341); - expect(result.longitude).toBe(-5.6782); - }); - - it("returns the correct latitude and longitude for '1.23412312 -5.6782356' with precision 4", () => { - const formatParser = new DecimalUnsignedFormat({ precision: 4 }); - const result = formatParser.parse('1.23412312 -5.6782356'); - expect(result.latitude).toBe(1.2341); - expect(result.longitude).toBe(-5.6782); - }); }); diff --git a/tests/dm-unsigned-prefixed-hemisphere-format.test.ts b/tests/dm-unsigned-prefixed-hemisphere-format.test.ts new file mode 100644 index 0000000..ed05254 --- /dev/null +++ b/tests/dm-unsigned-prefixed-hemisphere-format.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { DmUnsignedPrefixedHemisphereFormat } from '../src/formats/dm-unsigned-prefixed-hemisphere-format.js'; + +describe('canParse', () => { + it('returns true for known formats', () => { + expect(DmUnsignedPrefixedHemisphereFormat.canParse('N4007 W7407')).toBe(true); + expect(DmUnsignedPrefixedHemisphereFormat.canParse('N4007.38W7407.38')).toBe(true); + expect(DmUnsignedPrefixedHemisphereFormat.canParse('N 4007.38 W 7407.38')).toBe(true); + expect(DmUnsignedPrefixedHemisphereFormat.canParse('N4007.38 W7407.38')).toBe(true); + }); + + it('returns false for unknown formats', () => { + expect(DmUnsignedPrefixedHemisphereFormat.canParse('4007.38N740738W')).toBe(false); + expect(DmUnsignedPrefixedHemisphereFormat.canParse('4007.38N7407.38P')).toBe(false); + expect(DmUnsignedPrefixedHemisphereFormat.canParse('4007.38N7407.38 ')).toBe(false); + }); +}); +describe('parse', () => { + it("returns the correct latitude and longitude for 'N4007 W7407'", () => { + const formatParser = new DmUnsignedPrefixedHemisphereFormat(); + const result = formatParser.parse('N4007 W7407'); + expect(result.latitude).toBe(40.117); + expect(result.longitude).toBe(-74.117); + }); + + it("returns the correct latitude and longitude for 'N4007.38W7407.38'", () => { + const formatParser = new DmUnsignedPrefixedHemisphereFormat(); + const result = formatParser.parse('N4007.38W7407.38'); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it("returns the correct latitude and longitude for 'N 4007.38 W 7407.38'", () => { + const formatParser = new DmUnsignedPrefixedHemisphereFormat(); + const result = formatParser.parse('N 4007.38 W 7407.38'); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it("returns the correct latitude and longitude for 'N4007.38 W7407.38'", () => { + const formatParser = new DmUnsignedPrefixedHemisphereFormat(); + const result = formatParser.parse('N4007.38 W7407.38'); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); +}); diff --git a/tests/dm-unsigned-suffixed-hemisphere-format.test.ts b/tests/dm-unsigned-suffixed-hemisphere-format.test.ts index 3983888..8e2e48a 100644 --- a/tests/dm-unsigned-suffixed-hemisphere-format.test.ts +++ b/tests/dm-unsigned-suffixed-hemisphere-format.test.ts @@ -3,7 +3,9 @@ import { DmUnsignedSuffixedHemisphereFormat } from '../src/formats/dm-unsigned-s describe('canParse', () => { it('returns true for known formats', () => { + expect(DmUnsignedSuffixedHemisphereFormat.canParse('4007N 7407W')).toBe(true); expect(DmUnsignedSuffixedHemisphereFormat.canParse('4007.38N7407.38W')).toBe(true); + expect(DmUnsignedSuffixedHemisphereFormat.canParse('4007.38 N 7407.38 W')).toBe(true); expect(DmUnsignedSuffixedHemisphereFormat.canParse('4007.38N 7407.38W')).toBe(true); }); @@ -41,11 +43,4 @@ describe('parse', () => { expect(result.latitude).toBe(40.123); expect(result.longitude).toBe(-74.123); }); - - it("returns the correct latitude and longitude for '4007.3812342N 7407.38123W' with precision 4", () => { - const formatParser = new DmUnsignedSuffixedHemisphereFormat({ precision: 4 }); - const result = formatParser.parse('4007.3812342N 7407.38123W'); - expect(result.latitude).toBe(40.1231); - expect(result.longitude).toBe(-74.1231); - }); }); diff --git a/tests/parser.test.ts b/tests/parser.test.ts index d066929..3a8edb6 100644 --- a/tests/parser.test.ts +++ b/tests/parser.test.ts @@ -152,27 +152,6 @@ describe('Test that all configured format parsers do not interfere', () => { expect(result.latitude).toBe(1.234); expect(result.longitude).toBe(5.678); }); - - it("returns the correct latitude and longitude for '1.23412312 5.6782356' with precision 4", () => { - const parser = new Parser(); - const result = parser.parse('1.23412312 5.6782356'); - expect(result.latitude).toBe(1.234); - expect(result.longitude).toBe(5.678); - }); - - it("returns the correct latitude and longitude for '-1.23412312 -5.6782356' with precision 4", () => { - const parser = new Parser(); - const result = parser.parse('-1.23412312 -5.6782356'); - expect(result.latitude).toBe(-1.234); - expect(result.longitude).toBe(-5.678); - }); - - it("returns the correct latitude and longitude for '1.23412312 -5.6782356' with precision 4", () => { - const parser = new Parser(); - const result = parser.parse('1.23412312 -5.6782356'); - expect(result.latitude).toBe(1.234); - expect(result.longitude).toBe(-5.678); - }); }); describe('test decimal unsigned prefixed hemisphere format', () => { it("returns the correct latitude and longitude for 'N12 E56'", () => { @@ -253,6 +232,35 @@ describe('Test that all configured format parsers do not interfere', () => { expect(result.longitude).toBe(5.678); }); }); + describe('test dm unsigned prefixed hemisphere format', () => { + it("returns the correct latitude and longitude for 'N4007 W7407'", () => { + const parser = new Parser(); + const result = parser.parse('N4007 W7407'); + expect(result.latitude).toBe(40.117); + expect(result.longitude).toBe(-74.117); + }); + + it("returns the correct latitude and longitude for 'N4007.38W7407.38'", () => { + const parser = new Parser(); + const result = parser.parse('N4007.38W7407.38'); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it("returns the correct latitude and longitude for 'N 4007.38 W 7407.38'", () => { + const parser = new Parser(); + const result = parser.parse('N 4007.38 W 7407.38'); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + + it("returns the correct latitude and longitude for 'N4007.38 W7407.38'", () => { + const parser = new Parser(); + const result = parser.parse('N4007.38 W7407.38'); + expect(result.latitude).toBe(40.123); + expect(result.longitude).toBe(-74.123); + }); + }); describe('test dm unsigned suffixed hemisphere format', () => { it("returns the correct latitude and longitude for '4007N 7407W'", () => { const parser = new Parser(); @@ -281,12 +289,5 @@ describe('Test that all configured format parsers do not interfere', () => { expect(result.latitude).toBe(40.123); expect(result.longitude).toBe(-74.123); }); - - it("returns the correct latitude and longitude for '4007.3812342N 7407.38123W' with precision 4", () => { - const parser = new Parser(); - const result = parser.parse('4007.3812342N 7407.38123W'); - expect(result.latitude).toBe(40.123); - expect(result.longitude).toBe(-74.123); - }); }); });