Sorry for yet another algorithm, but I needed to run this on user-provided input, so I need to check whether it matches the desired format at all. To achieve this, I’m using a regexp on the whole string.
const lonLatRegexp = (() => {
const number = `[-\u2212]?\\s*\\d+([.,]\\d+)?`;
const getCoordinate = (n: number) => (
`(` +
`(?<hemispherePrefix${n}>[NWSE])` +
`)?(` +
`(?<degrees${n}>${number})` +
`(\\s*[°]\\s*|\\s*deg\\s*|\\s+|$|(?!\\d))` +
`)(` +
`(?<minutes${n}>${number})` +
`(\\s*['\u2032\u2019]\\s*)` +
`)?(` +
`(?<seconds${n}>${number})` +
`(\\s*["\u2033\u201d]\\s*)` +
`)?(` +
`(?<hemisphereSuffix${n}>[NWSE])` +
`)?`
);
const coords = (
`(geo\\s*:\\s*)?` +
`\\s*` +
getCoordinate(1) +
`(?<separator>\\s*[,;]\\s*|\\s+)` +
getCoordinate(2) +
`(\\?z=(?<zoom>\\d+))?`
);
return new RegExp(`^\\s*${coords}\\s*$`, "i");
})();
export function matchLonLat(query: string): (Point & { zoom?: number }) | undefined {
const m = lonLatRegexp.exec(query);
const prepareNumber = (str: string) => Number(str.replace(",", ".").replace("\u2212", "-").replace(/\s+/, ""));
const prepareCoords = (deg: string, min: string | undefined, sec: string | undefined, hem: string | undefined) => {
const degrees = prepareNumber(deg);
const result = Math.abs(degrees) + (min ? prepareNumber(min) / 60 : 0) + (sec ? prepareNumber(sec) / 3600 : 0);
return result * (degrees < 0 ? -1 : 1) * (hem && ["s", "S", "w", "W"].includes(hem) ? -1 : 1);
};
if (m) {
const { hemispherePrefix1, degrees1, minutes1, seconds1, hemisphereSuffix1, separator, hemispherePrefix2, degrees2, minutes2, seconds2, hemisphereSuffix2, zoom } = m.groups!;
let hemisphere1: string | undefined = undefined, hemisphere2: string | undefined = undefined;
if (hemispherePrefix1 && !hemisphereSuffix1 && hemispherePrefix2 && !hemisphereSuffix2) {
[hemisphere1, hemisphere2] = [hemispherePrefix1, hemispherePrefix2];
} else if (!hemispherePrefix1 && hemisphereSuffix1 && !hemispherePrefix2 && hemisphereSuffix2) {
[hemisphere1, hemisphere2] = [hemisphereSuffix1, hemisphereSuffix2];
} else if (hemispherePrefix1 && hemisphereSuffix1 && !hemispherePrefix2 && !hemisphereSuffix2 && !separator.trim()) {
// Coordinate 2 has a hemisphere prefix, but because the two coordinates are separated by whitespace only, it was matched as a coordinate 1 suffix
[hemisphere1, hemisphere2] = [hemispherePrefix1, hemisphereSuffix1];
} else if (hemispherePrefix1 || hemisphereSuffix1 || hemispherePrefix2 || hemisphereSuffix2) {
// Unsupported combination of hemisphere prefixes/suffixes
return undefined;
} // else: no hemispheres specified
const coordinate1 = prepareCoords(degrees1, minutes1, seconds1, hemisphere1);
const coordinate2 = prepareCoords(degrees2, minutes2, seconds2, hemisphere2);
const zoomNumber = zoom ? Number(zoom) : undefined;
const zoomObj = zoomNumber != null && isFinite(zoomNumber) ? { zoom: zoomNumber } : {};
// Handle cases where lat/lon are switched
if ([undefined, "n", "N", "s", "S"].includes(hemisphere1) && [undefined, "w", "W", "e", "E"].includes(hemisphere2)) {
return { lat: coordinate1, lon: coordinate2, ...zoomObj };
} else if ((["w", "W", "e", "E"] as Array<string | undefined>).includes(hemisphere1) && [undefined, "n", "N", "s", "S"].includes(hemisphere2)) {
return { lat: coordinate2, lon: coordinate1, ...zoomObj };
}
}
}
// Tests
test("matchLonLat", () => {
// Simple coordinates
expect(matchLonLat("1.234,2.345")).toEqual({ lat: 1.234, lon: 2.345 });
expect(matchLonLat("-1.234,2.345")).toEqual({ lat: -1.234, lon: 2.345 });
expect(matchLonLat("1.234,-2.345")).toEqual({ lat: 1.234, lon: -2.345 });
// Integers
expect(matchLonLat("1,2")).toEqual({ lat: 1, lon: 2 });
expect(matchLonLat("-1,2")).toEqual({ lat: -1, lon: 2 });
expect(matchLonLat("1,-2")).toEqual({ lat: 1, lon: -2 });
// With unicode minus
expect(matchLonLat("−1.234,2.345")).toEqual({ lat: -1.234, lon: 2.345 });
expect(matchLonLat("1.234,−2.345")).toEqual({ lat: 1.234, lon: -2.345 });
// With spaces
expect(matchLonLat(" - 1.234 , - 2.345 ")).toEqual({ lat: -1.234, lon: -2.345 });
// With different separators
expect(matchLonLat("-1.234;-2.345")).toEqual({ lat: -1.234, lon: -2.345 });
expect(matchLonLat("-1.234 -2.345")).toEqual({ lat: -1.234, lon: -2.345 });
// Using decimal comma
expect(matchLonLat("-1,234,-2,345")).toEqual({ lat: -1.234, lon: -2.345 });
expect(matchLonLat("-1,234;-2,345")).toEqual({ lat: -1.234, lon: -2.345 });
expect(matchLonLat("-1,234 -2,345")).toEqual({ lat: -1.234, lon: -2.345 });
// Geo URI
expect(matchLonLat("geo:-1.234,-2.345")).toEqual({ lat: -1.234, lon: -2.345 });
expect(matchLonLat("geo:-1.234,-2.345?z=10")).toEqual({ lat: -1.234, lon: -2.345, zoom: 10 });
// With degree sign
expect(matchLonLat("-1.234° -2.345°")).toEqual({ lat: -1.234, lon: -2.345 });
expect(matchLonLat("-1.234 ° -2.345 °")).toEqual({ lat: -1.234, lon: -2.345 });
expect(matchLonLat("-1.234 °, -2.345 °")).toEqual({ lat: -1.234, lon: -2.345 });
// With "deg"
expect(matchLonLat("-1.234deg -2.345deg")).toEqual({ lat: -1.234, lon: -2.345 });
expect(matchLonLat("-1.234 deg -2.345 deg")).toEqual({ lat: -1.234, lon: -2.345 });
expect(matchLonLat("-1.234 deg, -2.345 deg")).toEqual({ lat: -1.234, lon: -2.345 });
// With minutes
expect(matchLonLat("-1° 24' -2° 36'")).toEqual({ lat: -1.4, lon: -2.6 });
expect(matchLonLat("-1° 24', -2° 36'")).toEqual({ lat: -1.4, lon: -2.6 });
expect(matchLonLat("-1 ° 24 ' -2 ° 36 '")).toEqual({ lat: -1.4, lon: -2.6 });
// With unicode minute sign
expect(matchLonLat("-1deg 24′ -2deg 36′")).toEqual({ lat: -1.4, lon: -2.6 });
expect(matchLonLat("-1deg 24′, -2deg 36′")).toEqual({ lat: -1.4, lon: -2.6 });
expect(matchLonLat("-1 deg 24 ′ -2 deg 36 ′")).toEqual({ lat: -1.4, lon: -2.6 });
// With seconds
expect(matchLonLat("-1° 24' 36\" -2° 36' 72\"")).toEqual({ lat: -1.41, lon: -2.62 });
expect(matchLonLat("-1° 24' 36\", -2° 36' 72\"")).toEqual({ lat: -1.41, lon: -2.62 });
expect(matchLonLat("-1 ° 24 ' 36 \" -2 ° 36 ' 72 \"")).toEqual({ lat: -1.41, lon: -2.62 });
expect(matchLonLat("-1° 36\" -2° 72\"")).toEqual({ lat: -1.01, lon: -2.02 });
// With unicode second sign
expect(matchLonLat("-1deg 24′ 36″ -2deg 36′ 72″")).toEqual({ lat: -1.41, lon: -2.62 });
expect(matchLonLat("-1deg 24′ 36″, -2deg 36′ 72″")).toEqual({ lat: -1.41, lon: -2.62 });
expect(matchLonLat("-1 deg 24 ′ 36 ″ -2 deg 36 ′ 72 ″")).toEqual({ lat: -1.41, lon: -2.62 });
expect(matchLonLat("-1deg 36″ -2deg 72″")).toEqual({ lat: -1.01, lon: -2.02 });
// With unicode quote signs
expect(matchLonLat("-1deg 24’ 36” -2deg 36’ 72”")).toEqual({ lat: -1.41, lon: -2.62 });
expect(matchLonLat("-1deg 24’ 36”, -2deg 36’ 72”")).toEqual({ lat: -1.41, lon: -2.62 });
expect(matchLonLat("-1 deg 24 ’ 36 ” -2 deg 36 ’ 72 ”")).toEqual({ lat: -1.41, lon: -2.62 });
expect(matchLonLat("-1deg 36” -2deg 72”")).toEqual({ lat: -1.01, lon: -2.02 });
// Other hemisphere
expect(matchLonLat("1° 24' N 2° 36' E")).toEqual({ lat: 1.4, lon: 2.6 });
expect(matchLonLat("N 1° 24' E 2° 36'")).toEqual({ lat: 1.4, lon: 2.6 });
expect(matchLonLat("1° 24' S 2° 36' E")).toEqual({ lat: -1.4, lon: 2.6 });
expect(matchLonLat("S 1° 24' E 2° 36'")).toEqual({ lat: -1.4, lon: 2.6 });
expect(matchLonLat("1° 24' N 2° 36' W")).toEqual({ lat: 1.4, lon: -2.6 });
expect(matchLonLat("N 1° 24' W 2° 36'")).toEqual({ lat: 1.4, lon: -2.6 });
expect(matchLonLat("1° 24' s 2° 36' w")).toEqual({ lat: -1.4, lon: -2.6 });
expect(matchLonLat("s 1° 24' w 2° 36'")).toEqual({ lat: -1.4, lon: -2.6 });
// Switch lon/lat
expect(matchLonLat("1° 24' E 2° 36' N")).toEqual({ lat: 2.6, lon: 1.4 });
expect(matchLonLat("E 1° 24' N 2° 36'")).toEqual({ lat: 2.6, lon: 1.4 });
expect(matchLonLat("1° 24' E 2° 36' S")).toEqual({ lat: -2.6, lon: 1.4 });
expect(matchLonLat("E 1° 24' S 2° 36'")).toEqual({ lat: -2.6, lon: 1.4 });
expect(matchLonLat("1° 24' W 2° 36' N")).toEqual({ lat: 2.6, lon: -1.4 });
expect(matchLonLat("W 1° 24' N 2° 36'")).toEqual({ lat: 2.6, lon: -1.4 });
expect(matchLonLat("1° 24' W 2° 36' S")).toEqual({ lat: -2.6, lon: -1.4 });
expect(matchLonLat("W 1° 24' S 2° 36'")).toEqual({ lat: -2.6, lon: -1.4 });
// Practical examples
expect(matchLonLat("N 53°53’42.8928” E 10°44’13.4844”")).toEqual({ lat: 53.895248, lon: 10.737079 }); // Park4night
expect(matchLonLat("53°53'42.8928\"N 10°44'13.4844\"E")).toEqual({ lat: 53.895248, lon: 10.737079 }); // Google Maps
// Invalid lon/lat combination
expect(matchLonLat("1° 24' N 2° 36' N")).toEqual(undefined);
expect(matchLonLat("1° 24' E 2° 36' E")).toEqual(undefined);
expect(matchLonLat("1° 24' S 2° 36' S")).toEqual(undefined);
expect(matchLonLat("1° 24' W 2° 36' W")).toEqual(undefined);
expect(matchLonLat("1° 24' N 2° 36' S")).toEqual(undefined);
expect(matchLonLat("1° 24' S 2° 36' N")).toEqual(undefined);
expect(matchLonLat("1° 24' W 2° 36' E")).toEqual(undefined);
expect(matchLonLat("1° 24' E 2° 36' W")).toEqual(undefined);
// Invalid hemisphere prefix/suffix combination
expect(matchLonLat("N 1° 24' 2° 36'")).toEqual(undefined);
expect(matchLonLat("1° 24' E 2° 36'")).toEqual(undefined);
expect(matchLonLat("1° 24' 2° 36' E")).toEqual(undefined);
expect(matchLonLat("N 1° 24' E 2° 36' E")).toEqual(undefined);
expect(matchLonLat("N 1° 24' 2° 36' E")).toEqual(undefined);
expect(matchLonLat("1° 24' E N 2° 36'")).toEqual(undefined);
});
My code supports the following formats (and combinations thereof and variants with additional white spaces):
-1.234,-2.345
geo:-1.234,-2.345
geo:-1.234,-2.345?z=10
-1.234;-2.345
(semicolon separator)
-1.234 -2.345
(space separator)
-1,234 -2,345
(decimal comma)
−1.234,−2.345
(Unicode minus)
-1.234°, -2.345°
1° 23', 2° 34'
1° 23' 45.67", 2° 34' 56.78"
1 deg 23' 45.67", 2 deg 34' 56.78"
1° 45.67", 2° 56.78"
(seconds but no minutes)
1° 23′ 45.67″, 2° 34′ 56.78″
(Unicode minutes and seconds)
1° 23’ 45.67”, 2° 34’ 56.78”
(Unicode quotes for minutes and seconds)
-1° 23' 45.67", -2° 34' 56.78"
1° 23' 45.67" S, 2° 34' 56.78" W
(hemisphere suffix)
S 1° 23' 45.67", W 2° 34' 56.78"
(hemisphere prefix)
2° 34' 56.78" W 1° 23' 45.67" S
(latitude/longitude switched)
If the input has an invalid format, undefined is returned.