Skip to content

Commit

Permalink
refactor: remove redundant tests for unknown formats in coordinate pa…
Browse files Browse the repository at this point in the history
…rsers and update README with additional examples
  • Loading branch information
reskume committed Dec 4, 2024
1 parent 628ac42 commit 4826486
Show file tree
Hide file tree
Showing 19 changed files with 406 additions and 59 deletions.
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# OpenAIP Coordinate Parser

Easily extendable and configurable parser for NodeJS and the browser that extracts coordinates from a variety of formatted lat/lon strings.
Easily extendable and configurable parser for NodeJS and the browser that extracts coordinates from a variety of formatted lat/lon strings. The main focus is parsing various decimal or DMS format variations. Contributions that
add other format parsers are always welcome!

### Usage

Expand Down Expand Up @@ -113,7 +114,11 @@ 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'23"N 74°7'23"W`
- `40°7'23"N, 74°7'23"W`
- `40°7'23.123"N 74°7'23.123"W`
- `N40°7'23" W74°7'23"`
- `N40°7'23", W74°7'23"`
- `N40°7'23"W74°7'23"`
- `N 40°7'23.123" W 74°7'23.123"`
- ``
3 changes: 0 additions & 3 deletions src/formats/dm-unsigned-prefixed-hemisphere-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@ export class DmUnsignedPrefixedHemisphereFormat extends BaseFormat {

/**
* 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' });
Expand Down
3 changes: 0 additions & 3 deletions src/formats/dm-unsigned-suffixed-hemisphere-format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,6 @@ export class DmUnsignedSuffixedHemisphereFormat extends BaseFormat {

/**
* 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' });
Expand Down
81 changes: 81 additions & 0 deletions src/formats/dms-signed-prefixed-hemisphere-format.ts
Original file line number Diff line number Diff line change
@@ -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 = /^([NS])\s*(\d+)°\s*(\d+)'\s*(\d+(?:\.\d+)?)\"\s*,?\s*([EW])\s*(\d+)°\s*(\d+)'\s*(\d+(?:\.\d+)?)\"$/;

/**
* Parses coordinates strings in DMS signed prefixed hemisphere format. Coordinate ordering is
* always latitude, longitude.
*
* Supported formats:
*
* N40°7'23" W74°7'23"
* N40°7'23", W74°7'23"
* N40°7'23"W74°7'23"
* N 40°7'23.123" W 74°7'23.123"
*/
export class DmsSignedPrefixedHemisphereFormat extends BaseFormat {
parse(coordinateString: string): Coordinate {
validateSchema(coordinateString, z.string(), { assert: true, name: 'coordinateString' });

if (DmsSignedPrefixedHemisphereFormat.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[2];
const matchLatMinutes = match[3];
const matchLatSeconds = match[4];
const matchLatDirection = match[1];
const matchLonDegree = match[6];
const matchLonMinutes = match[7];
const matchLonSeconds = match[8];
const matchLonDirection = match[5];

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);
}
}
81 changes: 81 additions & 0 deletions src/formats/dms-signed-suffixed-hemisphere-format.ts
Original file line number Diff line number Diff line change
@@ -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+)°\s*(\d+)'\s*(\d+(?:\.\d+)?)\"\s*([NS])\s*,?\s*(\d+)°\s*(\d+)'\s*(\d+(?:\.\d+)?)\"\s*([EW])$/;

/**
* Parses coordinates strings in DMS signed suffixed hemisphere format. Coordinate ordering is
* always latitude, longitude.
*
* Supported formats:
*
* 40°7'23"N 74°7'23"W
* 40°7'23"N, 74°7'23"W
* 40°7'23"N74°7'23"W
* 40°7'23.123"N 74°7'23.123"W
*/
export class DmsSignedSuffixedHemisphereFormat extends BaseFormat {
parse(coordinateString: string): Coordinate {
validateSchema(coordinateString, z.string(), { assert: true, name: 'coordinateString' });

if (DmsSignedSuffixedHemisphereFormat.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);
}
}
4 changes: 4 additions & 0 deletions src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { DecimalUnsignedSuffixedHemisphereFormat } from './formats/decimal-unsig
import { DmUnsignedPrefixedHemisphereFormat } from './formats/dm-unsigned-prefixed-hemisphere-format.js';
import { DmUnsignedSuffixedHemisphereFormat } from './formats/dm-unsigned-suffixed-hemisphere-format.js';
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 { DmsUnsignedFormat } from './formats/dms-unsigned-format.js';
import type { Coordinate } from './types.js';
import { validateSchema } from './validate-schema.js';
Expand Down Expand Up @@ -55,6 +57,8 @@ export class Parser {
new DmUnsignedPrefixedHemisphereFormat({ precision: precision }),
new DmUnsignedSuffixedHemisphereFormat({ precision: precision }),
new DmsSignedFormat({ precision: precision }),
new DmsSignedPrefixedHemisphereFormat({ precision: precision }),
new DmsSignedSuffixedHemisphereFormat({ precision: precision }),
new DmsUnsignedFormat({ precision: precision }),
];
let formatParsers = options?.formatParsers || defaultParsers;
Expand Down
4 changes: 0 additions & 4 deletions tests/decimal-signed-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ describe('canParse', () => {
expect(DecimalSignedFormat.canParse('1.234° 5.678°')).toBe(true);
expect(DecimalSignedFormat.canParse('1.234 °, 5.678 °')).toBe(true);
});

it('returns false for unknown formats', () => {
expect(DecimalSignedFormat.canParse('1.234 5.678')).toBe(false);
});
});
describe('parse', () => {
it("returns the correct latitude and longitude for '12° 5°'", () => {
Expand Down
6 changes: 0 additions & 6 deletions tests/decimal-signed-prefixed-hemisphere-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ describe('canParse', () => {
expect(DecimalSignedPrefixedHemisphereFormat.canParse('N 1.234°,E5.678°')).toBe(true);
expect(DecimalSignedPrefixedHemisphereFormat.canParse('N1.234°E5.678°')).toBe(true);
});

it('returns false for unknown formats', () => {
expect(DecimalSignedPrefixedHemisphereFormat.canParse('1.234 5.678')).toBe(false);
expect(DecimalSignedPrefixedHemisphereFormat.canParse('1.234 N 5.678 P')).toBe(false);
expect(DecimalSignedPrefixedHemisphereFormat.canParse('1.234 N 5.678 ')).toBe(false);
});
});
describe('parse', () => {
it("returns the correct latitude and longitude for 'N 12° E 5°'", () => {
Expand Down
6 changes: 0 additions & 6 deletions tests/decimal-signed-suffixed-hemisphere-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,6 @@ describe('canParse', () => {
expect(DecimalSignedSuffixedHemisphereFormat.canParse('1.234°N,5.678°E')).toBe(true);
expect(DecimalSignedSuffixedHemisphereFormat.canParse('1.234°N5.678°E')).toBe(true);
});

it('returns false for unknown formats', () => {
expect(DecimalSignedSuffixedHemisphereFormat.canParse('1.234 5.678')).toBe(false);
expect(DecimalSignedSuffixedHemisphereFormat.canParse('1.234 N 5.678 P')).toBe(false);
expect(DecimalSignedSuffixedHemisphereFormat.canParse('1.234 N 5.678 ')).toBe(false);
});
});
describe('parse', () => {
it("returns the correct latitude and longitude for '12° N 5° E'", () => {
Expand Down
4 changes: 0 additions & 4 deletions tests/decimal-unsigned-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ describe('canParse', () => {
expect(DecimalUnsignedFormat.canParse('1.234, 5.678')).toBe(true);
expect(DecimalUnsignedFormat.canParse('1.234 5.678')).toBe(true);
});

it('returns false for unknown formats', () => {
expect(DecimalUnsignedFormat.canParse('1.234N 5.678E')).toBe(false);
});
});
describe('parse', () => {
it("returns the correct latitude and longitude for '10, 13'", () => {
Expand Down
4 changes: 0 additions & 4 deletions tests/decimal-unsigned-prefixed-hemisphere-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ describe('canParse', () => {
expect(DecimalUnsignedPrefixedHemisphereFormat.canParse('N12.234,E56.678')).toBe(true);
expect(DecimalUnsignedPrefixedHemisphereFormat.canParse('N12.234E56.678')).toBe(true);
});

it('returns false for unknown formats', () => {
expect(DecimalUnsignedPrefixedHemisphereFormat.canParse('1.234 5.678')).toBe(false);
});
});
describe('parse', () => {
it("returns the correct latitude and longitude for 'N12 E56'", () => {
Expand Down
4 changes: 0 additions & 4 deletions tests/decimal-unsigned-suffixed-hemisphere-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,6 @@ describe('canParse', () => {
expect(DecimalUnsignedSuffixedHemisphereFormat.canParse('12.234N,56.678E')).toBe(true);
expect(DecimalUnsignedSuffixedHemisphereFormat.canParse('12.234N56.678E')).toBe(true);
});

it('returns false for unknown formats', () => {
expect(DecimalUnsignedSuffixedHemisphereFormat.canParse('1.234 5.678')).toBe(false);
});
});
describe('parse', () => {
it("returns the correct latitude and longitude for '12N 56E'", () => {
Expand Down
6 changes: 0 additions & 6 deletions tests/dm-unsigned-prefixed-hemisphere-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ describe('canParse', () => {
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'", () => {
Expand Down
6 changes: 0 additions & 6 deletions tests/dm-unsigned-suffixed-hemisphere-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@ describe('canParse', () => {
expect(DmUnsignedSuffixedHemisphereFormat.canParse('4007.38 N 7407.38 W')).toBe(true);
expect(DmUnsignedSuffixedHemisphereFormat.canParse('4007.38N 7407.38W')).toBe(true);
});

it('returns false for unknown formats', () => {
expect(DmUnsignedSuffixedHemisphereFormat.canParse('4007.38N740738W')).toBe(false);
expect(DmUnsignedSuffixedHemisphereFormat.canParse('4007.38N7407.38P')).toBe(false);
expect(DmUnsignedSuffixedHemisphereFormat.canParse('4007.38N7407.38 ')).toBe(false);
});
});
describe('parse', () => {
it("returns the correct latitude and longitude for '4007N 7407W'", () => {
Expand Down
4 changes: 0 additions & 4 deletions tests/dms-signed-format.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ describe('canParse', () => {
expect(DmsSignedFormat.canParse(`40° 7' 23", -74° 7' 23"`)).toBe(true);
expect(DmsSignedFormat.canParse(`40° 7' 23.9999", -74° 7' 23.9999"`)).toBe(true);
});

it('returns false for unknown formats', () => {
expect(DmsSignedFormat.canParse('1.234 5.678')).toBe(false);
});
});
describe('parse', () => {
it(`returns the correct latitude and longitude for 40°7'23" -74°7'23"`, () => {
Expand Down
65 changes: 65 additions & 0 deletions tests/dms-signed-prefixed-hemisphereformat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, expect, it } from 'vitest';
import { DmsSignedPrefixedHemisphereFormat } from '../src/formats/dms-signed-prefixed-hemisphere-format.js';

describe('canParse', () => {
it('returns true for known formats', () => {
expect(DmsSignedPrefixedHemisphereFormat.canParse(`N40°7'23" W74°7'23"`)).toBe(true);
expect(DmsSignedPrefixedHemisphereFormat.canParse(`N40°7'23"W74°7'23"`)).toBe(true);
expect(DmsSignedPrefixedHemisphereFormat.canParse(`N 40° 7' 23" W 74° 7' 23"`)).toBe(true);
expect(DmsSignedPrefixedHemisphereFormat.canParse(`N40°7'23", W74°7'23"`)).toBe(true);
expect(DmsSignedPrefixedHemisphereFormat.canParse(`N40°7'23",W74°7'23"`)).toBe(true);
expect(DmsSignedPrefixedHemisphereFormat.canParse(`N40° 7' 23", W74° 7' 23"`)).toBe(true);
expect(DmsSignedPrefixedHemisphereFormat.canParse(`N 40° 7' 23.9999", W 74° 7' 23.9999"`)).toBe(true);
});
});
describe('parse', () => {
it(`returns the correct latitude and longitude for N40°7'23" W74°7'23"`, () => {
const formatParser = new DmsSignedPrefixedHemisphereFormat();
const result = formatParser.parse(`N40°7'23" W74°7'23"`);
expect(result.latitude).toBe(40.123);
expect(result.longitude).toBe(-74.123);
});

it(`returns the correct latitude and longitude for N40°7'23"W74°7'23"`, () => {
const formatParser = new DmsSignedPrefixedHemisphereFormat();
const result = formatParser.parse(`N40°7'23"W74°7'23"`);
expect(result.latitude).toBe(40.123);
expect(result.longitude).toBe(-74.123);
});

it(`returns the correct latitude and longitude for N 40° 7' 23" W 74° 7' 23"`, () => {
const formatParser = new DmsSignedPrefixedHemisphereFormat();
const result = formatParser.parse(`N 40° 7' 23" W 74° 7' 23"`);
expect(result.latitude).toBe(40.123);
expect(result.longitude).toBe(-74.123);
});

it(`returns the correct latitude and longitude for N40°7'23", W74°7'23"`, () => {
const formatParser = new DmsSignedPrefixedHemisphereFormat();
const result = formatParser.parse(`N40°7'23", W74°7'23"`);
expect(result.latitude).toBe(40.123);
expect(result.longitude).toBe(-74.123);
});

it(`returns the correct latitude and longitude for N40°7'23",W74°7'23"`, () => {
const formatParser = new DmsSignedPrefixedHemisphereFormat();
const result = formatParser.parse(`N40°7'23",W74°7'23"`);
expect(result.latitude).toBe(40.123);
expect(result.longitude).toBe(-74.123);
});

it(`returns the correct latitude and longitude for N40° 7' 23", W74° 7' 23"`, () => {
const formatParser = new DmsSignedPrefixedHemisphereFormat();
const result = formatParser.parse(`N40° 7' 23", W74° 7' 23"`);
expect(result.latitude).toBe(40.123);
expect(result.longitude).toBe(-74.123);
});

it(`returns the correct latitude and longitude for N 40° 7' 23.9999", W 74° 7' 23.9999"`, () => {
const formatParser = new DmsSignedPrefixedHemisphereFormat({ precision: 5 });
const result = formatParser.parse(`N 40° 7' 23.9999", W 74° 7' 23.9999"`);

expect(result.latitude).toBe(40.12333);
expect(result.longitude).toBe(-74.12333);
});
});
Loading

0 comments on commit 4826486

Please sign in to comment.