@accelint/geo 0.6.1 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +98 -1
  3. package/catalog-info.yaml +1 -1
  4. package/dist/coordinates/latlon/decimal-degrees/formatter.d.ts +35 -2
  5. package/dist/coordinates/latlon/decimal-degrees/formatter.js +33 -4
  6. package/dist/coordinates/latlon/decimal-degrees/formatter.js.map +1 -1
  7. package/dist/coordinates/latlon/decimal-degrees/system.js +5 -3
  8. package/dist/coordinates/latlon/decimal-degrees/system.js.map +1 -1
  9. package/dist/coordinates/latlon/degrees-decimal-minutes/formatter.d.ts +42 -2
  10. package/dist/coordinates/latlon/degrees-decimal-minutes/formatter.js +60 -8
  11. package/dist/coordinates/latlon/degrees-decimal-minutes/formatter.js.map +1 -1
  12. package/dist/coordinates/latlon/degrees-decimal-minutes/system.js +7 -6
  13. package/dist/coordinates/latlon/degrees-decimal-minutes/system.js.map +1 -1
  14. package/dist/coordinates/latlon/degrees-minutes-seconds/formatter.d.ts +45 -2
  15. package/dist/coordinates/latlon/degrees-minutes-seconds/formatter.js +66 -12
  16. package/dist/coordinates/latlon/degrees-minutes-seconds/formatter.js.map +1 -1
  17. package/dist/coordinates/latlon/degrees-minutes-seconds/system.js +7 -6
  18. package/dist/coordinates/latlon/degrees-minutes-seconds/system.js.map +1 -1
  19. package/dist/coordinates/latlon/internal/format.d.ts +50 -5
  20. package/dist/coordinates/latlon/internal/format.js +39 -9
  21. package/dist/coordinates/latlon/internal/format.js.map +1 -1
  22. package/dist/coordinates/latlon/internal/index.d.ts +3 -2
  23. package/dist/coordinates/latlon/internal/index.js +3 -2
  24. package/dist/coordinates/latlon/internal/index.js.map +1 -1
  25. package/dist/coordinates/latlon/internal/lexer.js +2 -1
  26. package/dist/coordinates/latlon/internal/lexer.js.map +1 -1
  27. package/dist/coordinates/latlon/internal/ordinal.d.ts +39 -9
  28. package/dist/coordinates/latlon/internal/ordinal.js +31 -12
  29. package/dist/coordinates/latlon/internal/ordinal.js.map +1 -1
  30. package/dist/coordinates/latlon/internal/plain-decimal.d.ts +35 -0
  31. package/dist/coordinates/latlon/internal/plain-decimal.js +59 -0
  32. package/dist/coordinates/latlon/internal/plain-decimal.js.map +1 -0
  33. package/dist/coordinates/latlon/internal/validate.d.ts +28 -1
  34. package/dist/coordinates/latlon/internal/validate.js +30 -1
  35. package/dist/coordinates/latlon/internal/validate.js.map +1 -1
  36. package/dist/coordinates/mgrs/parts.d.ts +86 -0
  37. package/dist/coordinates/mgrs/parts.js +94 -0
  38. package/dist/coordinates/mgrs/parts.js.map +1 -0
  39. package/dist/coordinates/mgrs/system.js +4 -2
  40. package/dist/coordinates/mgrs/system.js.map +1 -1
  41. package/dist/coordinates/utm/parts.d.ts +134 -0
  42. package/dist/coordinates/utm/parts.js +142 -0
  43. package/dist/coordinates/utm/parts.js.map +1 -0
  44. package/dist/coordinates/utm/system.js +4 -3
  45. package/dist/coordinates/utm/system.js.map +1 -1
  46. package/dist/index.d.ts +10 -7
  47. package/dist/index.js +10 -7
  48. package/package.json +6 -3
@@ -11,13 +11,74 @@
11
11
  */
12
12
 
13
13
 
14
+ import { getHemisphere } from "../internal/ordinal.js";
14
15
  import { createFormatter } from "../internal/format.js";
15
16
 
16
17
  //#region src/coordinates/latlon/degrees-minutes-seconds/formatter.ts
18
+ /** Default number of decimal places for degrees-minutes-seconds formatting. */
19
+ const DMS_PRECISION = 2;
20
+ /**
21
+ * Splits a non-negative magnitude into whole degrees, whole minutes, and
22
+ * decimal seconds, rounding the seconds to `precision` and carrying
23
+ * `60″ → +1′ → +1°` so the output stays a valid coordinate. Shared core of
24
+ * {@link toDmsParts} and the display string formatter.
25
+ *
26
+ * @param magnitude - Non-negative coordinate magnitude in degrees.
27
+ * @param precision - Decimal places for the seconds.
28
+ * @returns The `{ degrees, minutes, seconds }` triple.
29
+ *
30
+ * @remarks pure function
31
+ */
32
+ const toDmsMagnitude = (magnitude, precision) => {
33
+ let degrees = Math.floor(magnitude);
34
+ const minutesFull = (magnitude - degrees) * 60;
35
+ let minutes = Math.floor(minutesFull);
36
+ let seconds = Number(((minutesFull - minutes) * 60).toFixed(precision));
37
+ minutes += Math.floor(seconds / 60);
38
+ seconds %= 60;
39
+ degrees += Math.floor(minutes / 60);
40
+ minutes %= 60;
41
+ return {
42
+ degrees,
43
+ minutes,
44
+ seconds
45
+ };
46
+ };
47
+ /**
48
+ * Converts a single signed coordinate value into degrees-minutes-seconds parts.
49
+ *
50
+ * Applies the seconds/minutes carry (`60″ → +1′`, `60′ → +1°`) after rounding
51
+ * so `seconds` and `minutes` never reach `60`, then attaches the hemisphere
52
+ * letter for the axis.
53
+ *
54
+ * This is the display path: it rounds to `precision` and carries. The
55
+ * lossless round-trip representation returned by `createCoordinate(...).dms()`
56
+ * lives separately in `degrees-minutes-seconds/system.ts` (`toFormat`, via the
57
+ * shared `formatCoordinateSystem`), which keeps full precision and applies no
58
+ * carry so a value survives format → parse unchanged. The two are
59
+ * intentionally not shared — do not route one through the other.
60
+ *
61
+ * @param value - The signed coordinate value.
62
+ * @param axis - Whether the value is a latitude (`'lat'`) or longitude (`'lon'`).
63
+ * @param precision - Decimal places for the seconds (default `2`).
64
+ * @returns The `{ degrees, minutes, seconds, hemisphere }` parts object.
65
+ *
66
+ * @remarks pure function
67
+ *
68
+ * @example
69
+ * ```typescript
70
+ * toDmsParts(-77.0369, 'lon');
71
+ * // { degrees: 77, minutes: 2, seconds: 12.84, hemisphere: 'W' }
72
+ * ```
73
+ */
74
+ const toDmsParts = (value, axis, precision = DMS_PRECISION) => ({
75
+ ...toDmsMagnitude(Math.abs(value), precision),
76
+ hemisphere: getHemisphere(value, axis)
77
+ });
17
78
  /**
18
79
  * Converts a coordinate value to degrees minutes seconds format.
19
80
  *
20
- * @param num - The coordinate value to format.
81
+ * @param value - The coordinate value to format.
21
82
  * @returns Formatted coordinate string with degrees, minutes, and seconds (e.g., "45° 30' 15.23″").
22
83
  *
23
84
  * @example
@@ -32,16 +93,9 @@ import { createFormatter } from "../internal/format.js";
32
93
  * // '122° 25' 9.84″'
33
94
  * ```
34
95
  */
35
- const toDegreesMinutesSeconds = (num) => {
36
- let degrees = Math.floor(Math.abs(num));
37
- const minutesFull = (Math.abs(num) - degrees) * 60;
38
- let minutes = Math.floor(minutesFull);
39
- let seconds = Number(((minutesFull - minutes) * 60).toFixed(2));
40
- minutes += Math.floor(seconds / 60);
41
- seconds %= 60;
42
- degrees += Math.floor(minutes / 60);
43
- minutes %= 60;
44
- return `${degrees}° ${minutes}' ${seconds.toFixed(2)}″`;
96
+ const toDegreesMinutesSeconds = (value) => {
97
+ const { degrees, minutes, seconds } = toDmsMagnitude(Math.abs(value), DMS_PRECISION);
98
+ return `${degrees}° ${minutes}' ${seconds.toFixed(DMS_PRECISION)}″`;
45
99
  };
46
100
  /**
47
101
  * Formats latitude/longitude coordinates in degrees minutes seconds notation.
@@ -65,5 +119,5 @@ const toDegreesMinutesSeconds = (num) => {
65
119
  const formatDegreesMinutesSeconds = createFormatter(toDegreesMinutesSeconds);
66
120
 
67
121
  //#endregion
68
- export { formatDegreesMinutesSeconds };
122
+ export { DMS_PRECISION, formatDegreesMinutesSeconds, toDmsParts };
69
123
  //# sourceMappingURL=formatter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"formatter.js","names":[],"sources":["../../../../src/coordinates/latlon/degrees-minutes-seconds/formatter.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport { createFormatter } from '../internal/format';\n\n/**\n * Converts a coordinate value to degrees minutes seconds format.\n *\n * @param num - The coordinate value to format.\n * @returns Formatted coordinate string with degrees, minutes, and seconds (e.g., \"45° 30' 15.23″\").\n *\n * @example\n * ```typescript\n * toDegreesMinutesSeconds(45.5042);\n * // '45° 30' 15.12″'\n * ```\n *\n * @example\n * ```typescript\n * toDegreesMinutesSeconds(-122.4194);\n * // '122° 25' 9.84″'\n * ```\n */\nconst toDegreesMinutesSeconds = (num: number): string => {\n let degrees = Math.floor(Math.abs(num));\n const minutesFull = (Math.abs(num) - degrees) * 60;\n let minutes = Math.floor(minutesFull);\n let seconds = Number(((minutesFull - minutes) * 60).toFixed(2));\n\n // Rounding can produce 60 seconds (e.g. 40.9999999 -> 40° 59' 60.00″);\n // carry into minutes (and degrees) so the output stays a valid coordinate.\n minutes += Math.floor(seconds / 60);\n seconds %= 60;\n degrees += Math.floor(minutes / 60);\n minutes %= 60;\n\n return `${degrees}° ${minutes}' ${seconds.toFixed(2)}″`;\n};\n\n/**\n * Formats latitude/longitude coordinates in degrees minutes seconds notation.\n *\n * @param coordinates - Tuple of [latitude, longitude] values.\n * @param config - Optional formatting configuration.\n * @returns Formatted coordinate string in degrees minutes seconds format.\n *\n * @example\n * ```typescript\n * formatDegreesMinutesSeconds([37.7749, -122.4194]);\n * // '37° 46' 29.64″ N, 122° 25' 9.84″ W'\n * ```\n *\n * @example\n * ```typescript\n * formatDegreesMinutesSeconds([37.7749, -122.4194], { separator: ' / ' });\n * // '37° 46' 29.64″ N / 122° 25' 9.84″ W'\n * ```\n */\nexport const formatDegreesMinutesSeconds = createFormatter(\n toDegreesMinutesSeconds,\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAM,2BAA2B,QAAwB;CACvD,IAAI,UAAU,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC;CACvC,MAAM,eAAe,KAAK,IAAI,IAAI,GAAG,WAAW;CAChD,IAAI,UAAU,KAAK,MAAM,YAAY;CACrC,IAAI,UAAU,SAAS,cAAc,WAAW,IAAI,QAAQ,EAAE,CAAC;AAI/D,YAAW,KAAK,MAAM,UAAU,GAAG;AACnC,YAAW;AACX,YAAW,KAAK,MAAM,UAAU,GAAG;AACnC,YAAW;AAEX,QAAO,GAAG,QAAQ,IAAI,QAAQ,IAAI,QAAQ,QAAQ,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;AAsBvD,MAAa,8BAA8B,gBACzC,wBACD"}
1
+ {"version":3,"file":"formatter.js","names":[],"sources":["../../../../src/coordinates/latlon/degrees-minutes-seconds/formatter.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport { createFormatter } from '../internal/format';\nimport { type Axis, getHemisphere, type Hemisphere } from '../internal/ordinal';\n\n/** Default number of decimal places for degrees-minutes-seconds formatting. */\nexport const DMS_PRECISION = 2;\n\n/**\n * Structured degrees-minutes-seconds parts for a single signed coordinate value.\n *\n * `degrees`, `minutes`, and `seconds` are non-negative; carry keeps `seconds`\n * and `minutes` below `60`. The signed value is recoverable from the axis and\n * `hemisphere`.\n */\nexport type DmsParts = {\n degrees: number;\n minutes: number;\n seconds: number;\n hemisphere: Hemisphere;\n};\n\n/**\n * Splits a non-negative magnitude into whole degrees, whole minutes, and\n * decimal seconds, rounding the seconds to `precision` and carrying\n * `60″ +1′ +1°` so the output stays a valid coordinate. Shared core of\n * {@link toDmsParts} and the display string formatter.\n *\n * @param magnitude - Non-negative coordinate magnitude in degrees.\n * @param precision - Decimal places for the seconds.\n * @returns The `{ degrees, minutes, seconds }` triple.\n *\n * @remarks pure function\n */\nconst toDmsMagnitude = (\n magnitude: number,\n precision: number,\n): Pick<DmsParts, 'degrees' | 'minutes' | 'seconds'> => {\n let degrees = Math.floor(magnitude);\n const minutesFull = (magnitude - degrees) * 60;\n let minutes = Math.floor(minutesFull);\n let seconds = Number(((minutesFull - minutes) * 60).toFixed(precision));\n\n // Rounding can produce 60 seconds (e.g. 40.9999999 -> 40° 59' 60.00″);\n // carry into minutes (and degrees) so the output stays a valid coordinate.\n minutes += Math.floor(seconds / 60);\n seconds %= 60;\n degrees += Math.floor(minutes / 60);\n minutes %= 60;\n\n return { degrees, minutes, seconds };\n};\n\n/**\n * Converts a single signed coordinate value into degrees-minutes-seconds parts.\n *\n * Applies the seconds/minutes carry (`60″ → +1′`, `60′ → +1°`) after rounding\n * so `seconds` and `minutes` never reach `60`, then attaches the hemisphere\n * letter for the axis.\n *\n * This is the display path: it rounds to `precision` and carries. The\n * lossless round-trip representation returned by `createCoordinate(...).dms()`\n * lives separately in `degrees-minutes-seconds/system.ts` (`toFormat`, via the\n * shared `formatCoordinateSystem`), which keeps full precision and applies no\n * carry so a value survives format → parse unchanged. The two are\n * intentionally not shared — do not route one through the other.\n *\n * @param value - The signed coordinate value.\n * @param axis - Whether the value is a latitude (`'lat'`) or longitude (`'lon'`).\n * @param precision - Decimal places for the seconds (default `2`).\n * @returns The `{ degrees, minutes, seconds, hemisphere }` parts object.\n *\n * @remarks pure function\n *\n * @example\n * ```typescript\n * toDmsParts(-77.0369, 'lon');\n * // { degrees: 77, minutes: 2, seconds: 12.84, hemisphere: 'W' }\n * ```\n */\nexport const toDmsParts = (\n value: number,\n axis: Axis,\n precision: number = DMS_PRECISION,\n): DmsParts => ({\n ...toDmsMagnitude(Math.abs(value), precision),\n hemisphere: getHemisphere(value, axis),\n});\n\n/**\n * Converts a coordinate value to degrees minutes seconds format.\n *\n * @param value - The coordinate value to format.\n * @returns Formatted coordinate string with degrees, minutes, and seconds (e.g., \"45° 30' 15.23″\").\n *\n * @example\n * ```typescript\n * toDegreesMinutesSeconds(45.5042);\n * // '45° 30' 15.12″'\n * ```\n *\n * @example\n * ```typescript\n * toDegreesMinutesSeconds(-122.4194);\n * // '122° 25' 9.84″'\n * ```\n */\nconst toDegreesMinutesSeconds = (value: number): string => {\n const { degrees, minutes, seconds } = toDmsMagnitude(\n Math.abs(value),\n DMS_PRECISION,\n );\n\n return `${degrees}° ${minutes}' ${seconds.toFixed(DMS_PRECISION)}″`;\n};\n\n/**\n * Formats latitude/longitude coordinates in degrees minutes seconds notation.\n *\n * @param coordinates - Tuple of [latitude, longitude] values.\n * @param config - Optional formatting configuration.\n * @returns Formatted coordinate string in degrees minutes seconds format.\n *\n * @example\n * ```typescript\n * formatDegreesMinutesSeconds([37.7749, -122.4194]);\n * // '37° 46' 29.64″ N, 122° 25' 9.84″ W'\n * ```\n *\n * @example\n * ```typescript\n * formatDegreesMinutesSeconds([37.7749, -122.4194], { separator: ' / ' });\n * // '37° 46' 29.64″ N / 122° 25' 9.84″ W'\n * ```\n */\nexport const formatDegreesMinutesSeconds = createFormatter(\n toDegreesMinutesSeconds,\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgBA,MAAa,gBAAgB;;;;;;;;;;;;;AA4B7B,MAAM,kBACJ,WACA,cACsD;CACtD,IAAI,UAAU,KAAK,MAAM,UAAU;CACnC,MAAM,eAAe,YAAY,WAAW;CAC5C,IAAI,UAAU,KAAK,MAAM,YAAY;CACrC,IAAI,UAAU,SAAS,cAAc,WAAW,IAAI,QAAQ,UAAU,CAAC;AAIvE,YAAW,KAAK,MAAM,UAAU,GAAG;AACnC,YAAW;AACX,YAAW,KAAK,MAAM,UAAU,GAAG;AACnC,YAAW;AAEX,QAAO;EAAE;EAAS;EAAS;EAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BtC,MAAa,cACX,OACA,MACA,YAAoB,mBACN;CACd,GAAG,eAAe,KAAK,IAAI,MAAM,EAAE,UAAU;CAC7C,YAAY,cAAc,OAAO,KAAK;CACvC;;;;;;;;;;;;;;;;;;;AAoBD,MAAM,2BAA2B,UAA0B;CACzD,MAAM,EAAE,SAAS,SAAS,YAAY,eACpC,KAAK,IAAI,MAAM,EACf,cACD;AAED,QAAO,GAAG,QAAQ,IAAI,QAAQ,IAAI,QAAQ,QAAQ,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;;AAsBnE,MAAa,8BAA8B,gBACzC,wBACD"}
@@ -11,7 +11,9 @@
11
11
  */
12
12
 
13
13
 
14
- import { BEARINGS, SYMBOLS, SYMBOL_PATTERNS } from "../internal/index.js";
14
+ import { toPlainDecimalString } from "../internal/plain-decimal.js";
15
+ import { SYMBOL_PATTERNS } from "../internal/index.js";
16
+ import { formatCoordinateSystem } from "../internal/format.js";
15
17
  import { parseDegreesMinutesSeconds } from "./parser.js";
16
18
 
17
19
  //#region src/coordinates/latlon/degrees-minutes-seconds/system.ts
@@ -53,14 +55,13 @@ const systemDegreesMinutesSeconds = {
53
55
  const [degrees, minutes, seconds, bear] = arg;
54
56
  return Number.parseFloat(((Number.parseFloat(degrees) + Number.parseFloat(minutes) / 60 + Number.parseFloat(seconds) / 3600) * (SYMBOL_PATTERNS.NEGATIVE_BEARINGS.test(bear) ? -1 : 1)).toFixed(9));
55
57
  },
56
- toFormat(format, [left, right]) {
57
- return [left, right].map((num, index) => {
58
- const abs = Math.abs(num);
58
+ toFormat(format, coordinates) {
59
+ return formatCoordinateSystem(format, coordinates, (abs) => {
59
60
  const deg = Math.floor(abs);
60
61
  const rem = (abs - deg) * 60;
61
62
  const min = Math.floor(rem);
62
- return `${deg} ${min} ${Number.parseFloat(((rem - min) * 60).toFixed(10))} ${BEARINGS[format][index][+(num < 0)]}`;
63
- }).join(` ${SYMBOLS.DIVIDER} `);
63
+ return `${deg} ${min} ${toPlainDecimalString(Number.parseFloat(((rem - min) * 60).toFixed(10)))}`;
64
+ });
64
65
  }
65
66
  };
66
67
 
@@ -1 +1 @@
1
- {"version":3,"file":"system.js","names":["systemDegreesMinutesSeconds: CoordinateSystem"],"sources":["../../../../src/coordinates/latlon/degrees-minutes-seconds/system.ts"],"sourcesContent":["// __private-exports\n/*\n * Copyright 2024 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport {\n BEARINGS,\n type Compass,\n type Format,\n SYMBOL_PATTERNS,\n SYMBOLS,\n} from '../internal';\nimport { parseDegreesMinutesSeconds } from './parser';\nimport type { CoordinateSystem } from '../internal/coordinate-system';\n\n/**\n * Degrees Minutes Seconds coordinate system implementation.\n *\n * Provides parsing, conversion, and formatting for coordinates in degrees minutes seconds notation.\n * Coordinates are expressed as integer degrees, integer minutes, and decimal seconds (e.g., 37° 46' 29.64″ N).\n *\n * @property name - Human-readable name of the coordinate system.\n * @property parse - Parses degrees minutes seconds coordinate strings.\n * @property toFloat - Converts parsed coordinate components to floating point numbers.\n * @property toFormat - Formats numeric coordinates back to degrees minutes seconds string.\n *\n * @example\n * ```typescript\n * // Parse a coordinate string\n * const [coords, errors] = systemDegreesMinutesSeconds.parse('37° 46' 29.64″ N / 122° 25' 9.84″ W', 'LATLON');\n * ```\n *\n * @example\n * ```typescript\n * // Convert to float\n * const lat = systemDegreesMinutesSeconds.toFloat(['37', '46', '29.64', 'N']);\n * // 37.7749\n * ```\n *\n * @example\n * ```typescript\n * // Format to string\n * const formatted = systemDegreesMinutesSeconds.toFormat('LATLON', [37.7749, -122.4194]);\n * // '37 46 29.64 N / 122 25 9.84 W'\n * ```\n */\nexport const systemDegreesMinutesSeconds: CoordinateSystem = {\n name: 'Degrees Minutes Seconds',\n\n parse: parseDegreesMinutesSeconds,\n\n toFloat(arg) {\n const [degrees, minutes, seconds, bear] = arg as [\n string,\n string,\n string,\n Compass,\n ];\n\n return Number.parseFloat(\n (\n (Number.parseFloat(degrees) +\n Number.parseFloat(minutes) / 60 +\n Number.parseFloat(seconds) / 3600) *\n (SYMBOL_PATTERNS.NEGATIVE_BEARINGS.test(bear) ? -1 : 1)\n ).toFixed(9),\n );\n },\n\n toFormat(format: Format, [left, right]: [number, number]) {\n return [left, right]\n .map((num, index) => {\n const abs = Math.abs(num);\n const deg = Math.floor(abs);\n const rem = (abs - deg) * 60;\n const min = Math.floor(rem);\n const sec = Number.parseFloat(((rem - min) * 60).toFixed(10));\n\n return `${deg} ${min} ${sec} ${BEARINGS[format][index as 0 | 1][+(num < 0)]}`;\n })\n .join(` ${SYMBOLS.DIVIDER} `);\n },\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAaA,8BAAgD;CAC3D,MAAM;CAEN,OAAO;CAEP,QAAQ,KAAK;EACX,MAAM,CAAC,SAAS,SAAS,SAAS,QAAQ;AAO1C,SAAO,OAAO,aAET,OAAO,WAAW,QAAQ,GACzB,OAAO,WAAW,QAAQ,GAAG,KAC7B,OAAO,WAAW,QAAQ,GAAG,SAC9B,gBAAgB,kBAAkB,KAAK,KAAK,GAAG,KAAK,IACrD,QAAQ,EAAE,CACb;;CAGH,SAAS,QAAgB,CAAC,MAAM,QAA0B;AACxD,SAAO,CAAC,MAAM,MAAM,CACjB,KAAK,KAAK,UAAU;GACnB,MAAM,MAAM,KAAK,IAAI,IAAI;GACzB,MAAM,MAAM,KAAK,MAAM,IAAI;GAC3B,MAAM,OAAO,MAAM,OAAO;GAC1B,MAAM,MAAM,KAAK,MAAM,IAAI;AAG3B,UAAO,GAAG,IAAI,GAAG,IAAI,GAFT,OAAO,aAAa,MAAM,OAAO,IAAI,QAAQ,GAAG,CAAC,CAEjC,GAAG,SAAS,QAAQ,OAAgB,EAAE,MAAM;IACxE,CACD,KAAK,IAAI,QAAQ,QAAQ,GAAG;;CAElC"}
1
+ {"version":3,"file":"system.js","names":["systemDegreesMinutesSeconds: CoordinateSystem"],"sources":["../../../../src/coordinates/latlon/degrees-minutes-seconds/system.ts"],"sourcesContent":["// __private-exports\n/*\n * Copyright 2024 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport { type Compass, type Format, SYMBOL_PATTERNS } from '../internal';\nimport { formatCoordinateSystem } from '../internal/format';\nimport { toPlainDecimalString } from '../internal/plain-decimal';\nimport { parseDegreesMinutesSeconds } from './parser';\nimport type { CoordinateSystem } from '../internal/coordinate-system';\n\n/**\n * Degrees Minutes Seconds coordinate system implementation.\n *\n * Provides parsing, conversion, and formatting for coordinates in degrees minutes seconds notation.\n * Coordinates are expressed as integer degrees, integer minutes, and decimal seconds (e.g., 37° 46' 29.64″ N).\n *\n * @property name - Human-readable name of the coordinate system.\n * @property parse - Parses degrees minutes seconds coordinate strings.\n * @property toFloat - Converts parsed coordinate components to floating point numbers.\n * @property toFormat - Formats numeric coordinates back to degrees minutes seconds string.\n *\n * @example\n * ```typescript\n * // Parse a coordinate string\n * const [coords, errors] = systemDegreesMinutesSeconds.parse('37° 46' 29.64″ N / 122° 25' 9.84″ W', 'LATLON');\n * ```\n *\n * @example\n * ```typescript\n * // Convert to float\n * const lat = systemDegreesMinutesSeconds.toFloat(['37', '46', '29.64', 'N']);\n * // 37.7749\n * ```\n *\n * @example\n * ```typescript\n * // Format to string\n * const formatted = systemDegreesMinutesSeconds.toFormat('LATLON', [37.7749, -122.4194]);\n * // '37 46 29.64 N / 122 25 9.84 W'\n * ```\n */\nexport const systemDegreesMinutesSeconds: CoordinateSystem = {\n name: 'Degrees Minutes Seconds',\n\n parse: parseDegreesMinutesSeconds,\n\n toFloat(arg) {\n const [degrees, minutes, seconds, bear] = arg as [\n string,\n string,\n string,\n Compass,\n ];\n\n return Number.parseFloat(\n (\n (Number.parseFloat(degrees) +\n Number.parseFloat(minutes) / 60 +\n Number.parseFloat(seconds) / 3600) *\n (SYMBOL_PATTERNS.NEGATIVE_BEARINGS.test(bear) ? -1 : 1)\n ).toFixed(9),\n );\n },\n\n toFormat(format: Format, coordinates: [number, number]) {\n return formatCoordinateSystem(format, coordinates, (abs) => {\n const deg = Math.floor(abs);\n const rem = (abs - deg) * 60;\n const min = Math.floor(rem);\n const sec = Number.parseFloat(((rem - min) * 60).toFixed(10));\n\n return `${deg} ${min} ${toPlainDecimalString(sec)}`;\n });\n },\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,MAAaA,8BAAgD;CAC3D,MAAM;CAEN,OAAO;CAEP,QAAQ,KAAK;EACX,MAAM,CAAC,SAAS,SAAS,SAAS,QAAQ;AAO1C,SAAO,OAAO,aAET,OAAO,WAAW,QAAQ,GACzB,OAAO,WAAW,QAAQ,GAAG,KAC7B,OAAO,WAAW,QAAQ,GAAG,SAC9B,gBAAgB,kBAAkB,KAAK,KAAK,GAAG,KAAK,IACrD,QAAQ,EAAE,CACb;;CAGH,SAAS,QAAgB,aAA+B;AACtD,SAAO,uBAAuB,QAAQ,cAAc,QAAQ;GAC1D,MAAM,MAAM,KAAK,MAAM,IAAI;GAC3B,MAAM,OAAO,MAAM,OAAO;GAC1B,MAAM,MAAM,KAAK,MAAM,IAAI;AAG3B,UAAO,GAAG,IAAI,GAAG,IAAI,GAAG,qBAFZ,OAAO,aAAa,MAAM,OAAO,IAAI,QAAQ,GAAG,CAAC,CAEZ;IACjD;;CAEL"}
@@ -1,10 +1,53 @@
1
+ /*
2
+ * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.
3
+ * This file is licensed to you under the Apache License, Version 2.0 (the "License");
4
+ * you may not use this file except in compliance with the License. You may obtain a copy
5
+ * of the License at https://www.apache.org/licenses/LICENSE-2.0
6
+ *
7
+ * Unless required by applicable law or agreed to in writing, software distributed under
8
+ * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
9
+ * OF ANY KIND, either express or implied. See the License for the specific language
10
+ * governing permissions and limitations under the License.
11
+ */
12
+
13
+ import { Format } from "./index.js";
14
+
1
15
  //#region src/coordinates/latlon/internal/format.d.ts
2
- interface FormatOptions {
16
+ /**
17
+ * Display options for the `format*` coordinate formatters: text wrapped around
18
+ * the whole string, the divider between axes, and whether each axis carries
19
+ * its `N`/`S`/`E`/`W` letter.
20
+ */
21
+ type FormatOptions = {
3
22
  prefix: string;
4
23
  suffix: string;
5
24
  separator: string;
6
25
  withOrdinal?: boolean;
7
- }
26
+ };
27
+ /**
28
+ * Shared scaffold for a `CoordinateSystem`'s `toFormat` round-trip string.
29
+ *
30
+ * This is the loss-less internal representation `createCoordinate(...).dd()` /
31
+ * `.ddm()` / `.dms()` returns — deliberately distinct from the display
32
+ * formatters in each system's `formatter.ts`. The `to*Parts` / `format*`
33
+ * functions round and carry to a fixed precision for human display; this path
34
+ * keeps full precision and applies **no** carry so a value survives a
35
+ * format → parse → float round-trip unchanged. The two must not be merged.
36
+ *
37
+ * Owns the parts every system's `toFormat` shares — the `[lat, lon]` map, the
38
+ * ` / ` divider join, and the signed → `N`/`S`/`E`/`W` direction lookup — and
39
+ * defers only the per-axis magnitude rendering (which is all that differs
40
+ * between DD, DDM, and DMS) to `renderMagnitude`.
41
+ *
42
+ * @param format - Axis ordering (`'LATLON'` or `'LONLAT'`).
43
+ * @param coordinates - Signed `[left, right]` values in the given ordering.
44
+ * @param renderMagnitude - Renders the space-separated magnitude components of
45
+ * one axis (e.g. `"37 46.494"` for DDM) from its absolute value.
46
+ * @returns The joined round-trip string, e.g. `"37 46.494 N / 122 25.164 W"`.
47
+ *
48
+ * @remarks pure function
49
+ */
50
+ declare const formatCoordinateSystem: (format: Format, [left, right]: [number, number], renderMagnitude: (magnitude: number) => string) => string;
8
51
  /**
9
52
  * Creates a coordinate formatter function from a coordinate conversion function.
10
53
  *
@@ -13,8 +56,10 @@ interface FormatOptions {
13
56
  *
14
57
  * @example
15
58
  * ```typescript
16
- * const formatDD = createFormatter((num) => `${num.toFixed(6)}°`);
17
- * formatDD([37.7749, -122.4194]);
59
+ * const formatDD = createFormatter(
60
+ * (value, withOrdinal) => `${(withOrdinal ? Math.abs(value) : value).toFixed(6)}°`,
61
+ * );
62
+ * formatDD([37.7749, -122.4194], { withOrdinal: true });
18
63
  * // '37.774900° N, 122.419400° W'
19
64
  * ```
20
65
  *
@@ -27,5 +72,5 @@ interface FormatOptions {
27
72
  */
28
73
  declare const createFormatter: (fn: (coord: number, withOrdinal?: boolean) => string) => (coordinates: [number, number], config?: FormatOptions) => string;
29
74
  //#endregion
30
- export { FormatOptions, createFormatter };
75
+ export { FormatOptions, createFormatter, formatCoordinateSystem };
31
76
  //# sourceMappingURL=format.d.ts.map
@@ -11,10 +11,38 @@
11
11
  */
12
12
 
13
13
 
14
- import { getOrdinal } from "./ordinal.js";
14
+ import { BEARINGS, SYMBOLS } from "./index.js";
15
+ import { getHemisphere } from "./ordinal.js";
15
16
 
16
17
  //#region src/coordinates/latlon/internal/format.ts
17
18
  /**
19
+ * Shared scaffold for a `CoordinateSystem`'s `toFormat` round-trip string.
20
+ *
21
+ * This is the loss-less internal representation `createCoordinate(...).dd()` /
22
+ * `.ddm()` / `.dms()` returns — deliberately distinct from the display
23
+ * formatters in each system's `formatter.ts`. The `to*Parts` / `format*`
24
+ * functions round and carry to a fixed precision for human display; this path
25
+ * keeps full precision and applies **no** carry so a value survives a
26
+ * format → parse → float round-trip unchanged. The two must not be merged.
27
+ *
28
+ * Owns the parts every system's `toFormat` shares — the `[lat, lon]` map, the
29
+ * ` / ` divider join, and the signed → `N`/`S`/`E`/`W` direction lookup — and
30
+ * defers only the per-axis magnitude rendering (which is all that differs
31
+ * between DD, DDM, and DMS) to `renderMagnitude`.
32
+ *
33
+ * @param format - Axis ordering (`'LATLON'` or `'LONLAT'`).
34
+ * @param coordinates - Signed `[left, right]` values in the given ordering.
35
+ * @param renderMagnitude - Renders the space-separated magnitude components of
36
+ * one axis (e.g. `"37 46.494"` for DDM) from its absolute value.
37
+ * @returns The joined round-trip string, e.g. `"37 46.494 N / 122 25.164 W"`.
38
+ *
39
+ * @remarks pure function
40
+ */
41
+ const formatCoordinateSystem = (format, [left, right], renderMagnitude) => [left, right].map((value, index) => {
42
+ const direction = BEARINGS[format][index][+(value < 0)];
43
+ return `${renderMagnitude(Math.abs(value))} ${direction}`;
44
+ }).join(` ${SYMBOLS.DIVIDER} `);
45
+ /**
18
46
  * Creates a coordinate formatter function from a coordinate conversion function.
19
47
  *
20
48
  * @param fn - Function that converts a single coordinate value to a formatted string.
@@ -22,8 +50,10 @@ import { getOrdinal } from "./ordinal.js";
22
50
  *
23
51
  * @example
24
52
  * ```typescript
25
- * const formatDD = createFormatter((num) => `${num.toFixed(6)}°`);
26
- * formatDD([37.7749, -122.4194]);
53
+ * const formatDD = createFormatter(
54
+ * (value, withOrdinal) => `${(withOrdinal ? Math.abs(value) : value).toFixed(6)}°`,
55
+ * );
56
+ * formatDD([37.7749, -122.4194], { withOrdinal: true });
27
57
  * // '37.774900° N, 122.419400° W'
28
58
  * ```
29
59
  *
@@ -36,15 +66,15 @@ import { getOrdinal } from "./ordinal.js";
36
66
  */
37
67
  const createFormatter = (fn) => (coordinates, config) => {
38
68
  const [latitude, longitude] = coordinates;
39
- const latOrdinal = `${config?.withOrdinal ? ` ${getOrdinal(latitude, true)}` : ""}`;
40
- const lonOrdinal = `${config?.withOrdinal ? ` ${getOrdinal(longitude, false)}` : ""}`;
41
- const lat = fn(latitude, config?.withOrdinal);
42
- const lon = fn(longitude, config?.withOrdinal);
69
+ const latOrdinal = config?.withOrdinal ? ` ${getHemisphere(latitude, "lat")}` : "";
70
+ const lonOrdinal = config?.withOrdinal ? ` ${getHemisphere(longitude, "lon")}` : "";
71
+ const latValue = fn(latitude, config?.withOrdinal);
72
+ const lonValue = fn(longitude, config?.withOrdinal);
43
73
  const prefix = config?.prefix ?? "";
44
74
  const suffix = config?.suffix ?? "";
45
- return `${prefix}${lat}${latOrdinal}${config?.separator ?? ", "}${lon}${lonOrdinal}${suffix}`;
75
+ return `${prefix}${latValue}${latOrdinal}${config?.separator ?? ", "}${lonValue}${lonOrdinal}${suffix}`;
46
76
  };
47
77
 
48
78
  //#endregion
49
- export { createFormatter };
79
+ export { createFormatter, formatCoordinateSystem };
50
80
  //# sourceMappingURL=format.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"format.js","names":[],"sources":["../../../../src/coordinates/latlon/internal/format.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n// __private-exports\n\nimport { getOrdinal } from './ordinal';\n\nexport interface FormatOptions {\n prefix: string;\n suffix: string;\n separator: string;\n withOrdinal?: boolean;\n}\n\n/**\n * Creates a coordinate formatter function from a coordinate conversion function.\n *\n * @param fn - Function that converts a single coordinate value to a formatted string.\n * @returns Formatter function that takes coordinate pair and optional config.\n *\n * @example\n * ```typescript\n * const formatDD = createFormatter((num) => `${num.toFixed(6)}°`);\n * formatDD([37.7749, -122.4194]);\n * // '37.774900° N, 122.419400° W'\n * ```\n *\n * @example\n * ```typescript\n * const formatDMS = createFormatter(toDegreesMinutesSeconds);\n * formatDMS([37.7749, -122.4194], { separator: ' / ', withOrdinal: true });\n * // '37° 46' 29.64″ N / 122° 25' 9.84″ W'\n * ```\n */\nexport const createFormatter =\n (fn: (coord: number, withOrdinal?: boolean) => string) =>\n (coordinates: [number, number], config?: FormatOptions): string => {\n const [latitude, longitude] = coordinates;\n const latOrdinal = `${config?.withOrdinal ? ` ${getOrdinal(latitude, true)}` : ''}`;\n const lonOrdinal = `${config?.withOrdinal ? ` ${getOrdinal(longitude, false)}` : ''}`;\n const lat = fn(latitude, config?.withOrdinal);\n const lon = fn(longitude, config?.withOrdinal);\n const prefix = config?.prefix ?? '';\n const suffix = config?.suffix ?? '';\n const separator = config?.separator ?? ', ';\n\n return `${prefix}${lat}${latOrdinal}${separator}${lon}${lonOrdinal}${suffix}`;\n };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAa,mBACV,QACA,aAA+B,WAAmC;CACjE,MAAM,CAAC,UAAU,aAAa;CAC9B,MAAM,aAAa,GAAG,QAAQ,cAAc,IAAI,WAAW,UAAU,KAAK,KAAK;CAC/E,MAAM,aAAa,GAAG,QAAQ,cAAc,IAAI,WAAW,WAAW,MAAM,KAAK;CACjF,MAAM,MAAM,GAAG,UAAU,QAAQ,YAAY;CAC7C,MAAM,MAAM,GAAG,WAAW,QAAQ,YAAY;CAC9C,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,SAAS,QAAQ,UAAU;AAGjC,QAAO,GAAG,SAAS,MAAM,aAFP,QAAQ,aAAa,OAEW,MAAM,aAAa"}
1
+ {"version":3,"file":"format.js","names":[],"sources":["../../../../src/coordinates/latlon/internal/format.ts"],"sourcesContent":["/*\n * Copyright 2026 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\n// __private-exports\n\nimport { BEARINGS, type Format, SYMBOLS } from '.';\nimport { getHemisphere } from './ordinal';\n\n/**\n * Display options for the `format*` coordinate formatters: text wrapped around\n * the whole string, the divider between axes, and whether each axis carries\n * its `N`/`S`/`E`/`W` letter.\n */\nexport type FormatOptions = {\n prefix: string;\n suffix: string;\n separator: string;\n withOrdinal?: boolean;\n};\n\n/**\n * Shared scaffold for a `CoordinateSystem`'s `toFormat` round-trip string.\n *\n * This is the loss-less internal representation `createCoordinate(...).dd()` /\n * `.ddm()` / `.dms()` returns — deliberately distinct from the display\n * formatters in each system's `formatter.ts`. The `to*Parts` / `format*`\n * functions round and carry to a fixed precision for human display; this path\n * keeps full precision and applies **no** carry so a value survives a\n * format → parse → float round-trip unchanged. The two must not be merged.\n *\n * Owns the parts every system's `toFormat` shares — the `[lat, lon]` map, the\n * ` / ` divider join, and the signed → `N`/`S`/`E`/`W` direction lookup — and\n * defers only the per-axis magnitude rendering (which is all that differs\n * between DD, DDM, and DMS) to `renderMagnitude`.\n *\n * @param format - Axis ordering (`'LATLON'` or `'LONLAT'`).\n * @param coordinates - Signed `[left, right]` values in the given ordering.\n * @param renderMagnitude - Renders the space-separated magnitude components of\n * one axis (e.g. `\"37 46.494\"` for DDM) from its absolute value.\n * @returns The joined round-trip string, e.g. `\"37 46.494 N / 122 25.164 W\"`.\n *\n * @remarks pure function\n */\nexport const formatCoordinateSystem = (\n format: Format,\n [left, right]: [number, number],\n renderMagnitude: (magnitude: number) => string,\n): string =>\n [left, right]\n .map((value, index) => {\n const direction = BEARINGS[format][index as 0 | 1][+(value < 0)];\n\n return `${renderMagnitude(Math.abs(value))} ${direction}`;\n })\n .join(` ${SYMBOLS.DIVIDER} `);\n\n/**\n * Creates a coordinate formatter function from a coordinate conversion function.\n *\n * @param fn - Function that converts a single coordinate value to a formatted string.\n * @returns Formatter function that takes coordinate pair and optional config.\n *\n * @example\n * ```typescript\n * const formatDD = createFormatter(\n * (value, withOrdinal) => `${(withOrdinal ? Math.abs(value) : value).toFixed(6)}°`,\n * );\n * formatDD([37.7749, -122.4194], { withOrdinal: true });\n * // '37.774900° N, 122.419400° W'\n * ```\n *\n * @example\n * ```typescript\n * const formatDMS = createFormatter(toDegreesMinutesSeconds);\n * formatDMS([37.7749, -122.4194], { separator: ' / ', withOrdinal: true });\n * // '37° 46' 29.64″ N / 122° 25' 9.84″ W'\n * ```\n */\nexport const createFormatter =\n (fn: (coord: number, withOrdinal?: boolean) => string) =>\n (coordinates: [number, number], config?: FormatOptions): string => {\n const [latitude, longitude] = coordinates;\n const latOrdinal = config?.withOrdinal\n ? ` ${getHemisphere(latitude, 'lat')}`\n : '';\n const lonOrdinal = config?.withOrdinal\n ? ` ${getHemisphere(longitude, 'lon')}`\n : '';\n const latValue = fn(latitude, config?.withOrdinal);\n const lonValue = fn(longitude, config?.withOrdinal);\n const prefix = config?.prefix ?? '';\n const suffix = config?.suffix ?? '';\n const separator = config?.separator ?? ', ';\n\n return `${prefix}${latValue}${latOrdinal}${separator}${lonValue}${lonOrdinal}${suffix}`;\n };\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,MAAa,0BACX,QACA,CAAC,MAAM,QACP,oBAEA,CAAC,MAAM,MAAM,CACV,KAAK,OAAO,UAAU;CACrB,MAAM,YAAY,SAAS,QAAQ,OAAgB,EAAE,QAAQ;AAE7D,QAAO,GAAG,gBAAgB,KAAK,IAAI,MAAM,CAAC,CAAC,GAAG;EAC9C,CACD,KAAK,IAAI,QAAQ,QAAQ,GAAG;;;;;;;;;;;;;;;;;;;;;;;AAwBjC,MAAa,mBACV,QACA,aAA+B,WAAmC;CACjE,MAAM,CAAC,UAAU,aAAa;CAC9B,MAAM,aAAa,QAAQ,cACvB,IAAI,cAAc,UAAU,MAAM,KAClC;CACJ,MAAM,aAAa,QAAQ,cACvB,IAAI,cAAc,WAAW,MAAM,KACnC;CACJ,MAAM,WAAW,GAAG,UAAU,QAAQ,YAAY;CAClD,MAAM,WAAW,GAAG,WAAW,QAAQ,YAAY;CACnD,MAAM,SAAS,QAAQ,UAAU;CACjC,MAAM,SAAS,QAAQ,UAAU;AAGjC,QAAO,GAAG,SAAS,WAAW,aAFZ,QAAQ,aAAa,OAEgB,WAAW,aAAa"}
@@ -11,7 +11,8 @@
11
11
  */
12
12
 
13
13
  import { CoordinateInput, CoordinateInternalValue, CoordinateObject, CoordinateTuple, LatLonTuple, LonLatTuple, isCoordinateObject, isCoordinateTuple, normalizeObjectToLatLon, tupleToLatLon } from "./normalize.js";
14
- import { isFiniteNumber, validateNumericCoordinate, validateSignedRange } from "./validate.js";
14
+ import { toPlainDecimalString } from "./plain-decimal.js";
15
+ import { isFiniteNumber, isValidNumericCoordinate, validateNumericCoordinate, validateSignedRange } from "./validate.js";
15
16
 
16
17
  //#region src/coordinates/latlon/internal/index.d.ts
17
18
  type Axes = 'LAT' | 'LON';
@@ -72,5 +73,5 @@ declare const PARTIAL_PATTERNS: {
72
73
  readonly secDec: RegExp;
73
74
  };
74
75
  //#endregion
75
- export { Axes, BEARINGS, Compass, type CoordinateInput, type CoordinateInternalValue, type CoordinateObject, type CoordinateTuple, Errors, FORMATS, FORMATS_DEFAULT, Format, LIMITS, type LatLonTuple, type LonLatTuple, PARTIAL_PATTERNS, SYMBOLS, SYMBOL_PATTERNS, isCoordinateObject, isCoordinateTuple, isFiniteNumber, normalizeObjectToLatLon, tupleToLatLon, validateNumericCoordinate, validateSignedRange };
76
+ export { Axes, BEARINGS, Compass, type CoordinateInput, type CoordinateInternalValue, type CoordinateObject, type CoordinateTuple, Errors, FORMATS, FORMATS_DEFAULT, Format, LIMITS, type LatLonTuple, type LonLatTuple, PARTIAL_PATTERNS, SYMBOLS, SYMBOL_PATTERNS, isCoordinateObject, isCoordinateTuple, isFiniteNumber, isValidNumericCoordinate, normalizeObjectToLatLon, toPlainDecimalString, tupleToLatLon, validateNumericCoordinate, validateSignedRange };
76
77
  //# sourceMappingURL=index.d.ts.map
@@ -13,7 +13,8 @@
13
13
 
14
14
  import { capture, merge, optional } from "../../../patterning.js";
15
15
  import { isCoordinateObject, isCoordinateTuple, normalizeObjectToLatLon, tupleToLatLon } from "./normalize.js";
16
- import { isFiniteNumber, validateNumericCoordinate, validateSignedRange } from "./validate.js";
16
+ import { toPlainDecimalString } from "./plain-decimal.js";
17
+ import { isFiniteNumber, isValidNumericCoordinate, validateNumericCoordinate, validateSignedRange } from "./validate.js";
17
18
 
18
19
  //#region src/coordinates/latlon/internal/index.ts
19
20
  /**
@@ -98,5 +99,5 @@ const PARTIAL_PATTERNS = {
98
99
  };
99
100
 
100
101
  //#endregion
101
- export { BEARINGS, FORMATS, FORMATS_DEFAULT, LIMITS, PARTIAL_PATTERNS, SYMBOLS, SYMBOL_PATTERNS, isCoordinateObject, isCoordinateTuple, isFiniteNumber, normalizeObjectToLatLon, tupleToLatLon, validateNumericCoordinate, validateSignedRange };
102
+ export { BEARINGS, FORMATS, FORMATS_DEFAULT, LIMITS, PARTIAL_PATTERNS, SYMBOLS, SYMBOL_PATTERNS, isCoordinateObject, isCoordinateTuple, isFiniteNumber, isValidNumericCoordinate, normalizeObjectToLatLon, toPlainDecimalString, tupleToLatLon, validateNumericCoordinate, validateSignedRange };
102
103
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["Patterning.optional","Patterning.capture","Patterning.merge"],"sources":["../../../../src/coordinates/latlon/internal/index.ts"],"sourcesContent":["// __private-exports\n/*\n * Copyright 2024 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport * as Patterning from '@/patterning';\n\nexport type Axes = 'LAT' | 'LON';\nexport type Compass = 'N' | 'S' | 'E' | 'W';\nexport type Errors = string[];\nexport type Format = (typeof FORMATS)[number];\n\n/**\n * Bearings are the consistent/explicit identifiers of directionality of a\n * coordinate component; this library has opted for these over implicit\n * indication by number sign not because there is an inherent superiority\n * but because something had to be chosen.\n *\n * NOTE: these arrays are position-important; negative values are [1] and\n * positive values are [0] so that they can be consistently indexed using\n * an `isNegative` boolean to reference the negative bearing of each axis\n */\nexport const BEARINGS = {\n LAT: ['N', 'S'],\n LON: ['E', 'W'],\n LATLON: [\n ['N', 'S'],\n ['E', 'W'],\n ],\n LONLAT: [\n ['E', 'W'],\n ['N', 'S'],\n ],\n} as const;\n\nexport const FORMATS = ['LATLON', 'LONLAT'] as const;\nexport const FORMATS_DEFAULT = FORMATS[0];\n\nexport const LIMITS = { LATLON: [90, 180], LONLAT: [180, 90] } as const;\n\nexport const SYMBOLS = {\n DEGREES: '°',\n MINUTES: \"'\",\n SECONDS: '\"',\n DIVIDER: '/',\n};\n\nexport const SYMBOL_PATTERNS = {\n LAT: new RegExp(`[${BEARINGS.LAT.join('')}]`),\n LON: new RegExp(`[${BEARINGS.LON.join('')}]`),\n NSEW: new RegExp(`[${[...BEARINGS.LAT, ...BEARINGS.LON].join('')}]`),\n NEGATIVE_BEARINGS: /[SW]/i,\n NEGATIVE_SIGN: /-/,\n\n DEGREES: new RegExp(SYMBOLS.DEGREES),\n MINUTES: new RegExp(SYMBOLS.MINUTES),\n SECONDS: new RegExp(SYMBOLS.SECONDS),\n\n DIVIDER: new RegExp(SYMBOLS.DIVIDER),\n\n DMS: new RegExp(\n `[${[SYMBOLS.DEGREES, SYMBOLS.MINUTES, SYMBOLS.SECONDS].join('')}]`,\n ),\n\n // divider: {\n // first: /(?<NAMED_SEPARATOR>:?)/,\n // follow: new RegExp(`\\\\s?\\\\k<${'NAMED_SEPARATOR'}>\\\\s?`),\n // },\n} as const;\n\n/**\n * Creates a regex pattern for matching decimal minutes or seconds values.\n *\n * Generates a pattern that matches numeric values in the range 0-59.999... with optional\n * leading zeros, decimal points, and symbol indicators, using lookbehind and lookahead\n * to prevent partial matches within larger numbers.\n *\n * @param symbol - Regular expression for the symbol (minutes ' or seconds \" indicator).\n * @returns Combined regex pattern with precise boundary matching.\n *\n * @example\n * ```typescript\n * const minutesPattern = decimalSecAndMin(SYMBOL_PATTERNS.MINUTES);\n * // Matches: \"30.5'\", \"59.999999999\", \".5'\", \"001'\", etc.\n * ```\n *\n * @example\n * ```typescript\n * const secondsPattern = decimalSecAndMin(SYMBOL_PATTERNS.SECONDS);\n * // Matches: '45.23\"', '0.5', '59.9999999999\"', etc.\n * ```\n */\nconst decimalSecAndMin = (symbol: RegExp) =>\n Patterning.optional(\n // Negative lookbehind\n // to ensure that the match is not preceded by a digit,\n // avoiding partial matches within larger numbers.\n /(?<!\\d)/,\n\n // 0-59 including 10 decimal places and leading zeros or no number before\n // acceptable values: 0, 0.1234567890, .9876543210, 001, 59.9999999999\n /([-+]?0*(?:[0-5]?\\d|\\.\\d{1,10})(?:\\.\\d{1,10})?)/,\n\n Patterning.optional(symbol),\n\n // Negative lookahead\n // to ensure that the match is not followed by a digit,\n // avoiding partial matches within larger numbers.\n /(?!\\d)/,\n );\n\nexport const PARTIAL_PATTERNS = {\n ' ': /\\s*/,\n '/': Patterning.capture(SYMBOL_PATTERNS.DIVIDER),\n NS: Patterning.optional(Patterning.capture(SYMBOL_PATTERNS.LAT)),\n EW: Patterning.optional(Patterning.capture(SYMBOL_PATTERNS.LON)),\n\n degLatDec: Patterning.merge(\n Patterning.capture(\n /0*(?:90(?:\\.0{1,10})?)/, // 90[.0]\n /|/,\n /(?:0?[0-8]?\\d(?:\\.\\d{1,10})?)/, // [0]0[.0]-89[.9]\n ),\n Patterning.optional(SYMBOL_PATTERNS.DEGREES),\n ),\n degLonDec: Patterning.merge(\n Patterning.capture(\n /(?:180(?:\\.0{1,10})?)/, // 180[.0]\n /|/,\n /(?:0*(?:\\d{1,2}|1[0-7]\\d)(?:\\.\\d{1,10})?)/, // [00]0[.0]-179[.9]\n ),\n Patterning.optional(SYMBOL_PATTERNS.DEGREES),\n ),\n degLat: Patterning.merge(\n Patterning.capture(\n /(?:0?90)/, // 90\n /|/,\n /(?:0?[0-8]?\\d)/, // [0]0-89\n ),\n Patterning.optional(SYMBOL_PATTERNS.DEGREES),\n ),\n degLon: Patterning.merge(\n Patterning.capture(\n /(?:180)/, // 180\n /|/,\n /(?:0*(?:\\d{1,2}|1[0-7]\\d))/, // [00]0-179\n ),\n Patterning.optional(SYMBOL_PATTERNS.DEGREES),\n ),\n min: Patterning.merge(\n Patterning.optional(\n Patterning.capture(\n /(?:0?[0-5]?\\d)?/, // [0]0-59\n ),\n Patterning.optional(SYMBOL_PATTERNS.MINUTES),\n ),\n ),\n minDec: decimalSecAndMin(SYMBOL_PATTERNS.MINUTES),\n secDec: decimalSecAndMin(SYMBOL_PATTERNS.SECONDS),\n} as const;\n\nexport {\n type CoordinateInput,\n type CoordinateInternalValue,\n type CoordinateObject,\n type CoordinateTuple,\n type LatLonTuple,\n type LonLatTuple,\n isCoordinateObject,\n isCoordinateTuple,\n normalizeObjectToLatLon,\n tupleToLatLon,\n} from './normalize';\nexport {\n isFiniteNumber,\n validateNumericCoordinate,\n validateSignedRange,\n} from './validate';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAa,WAAW;CACtB,KAAK,CAAC,KAAK,IAAI;CACf,KAAK,CAAC,KAAK,IAAI;CACf,QAAQ,CACN,CAAC,KAAK,IAAI,EACV,CAAC,KAAK,IAAI,CACX;CACD,QAAQ,CACN,CAAC,KAAK,IAAI,EACV,CAAC,KAAK,IAAI,CACX;CACF;AAED,MAAa,UAAU,CAAC,UAAU,SAAS;AAC3C,MAAa,kBAAkB,QAAQ;AAEvC,MAAa,SAAS;CAAE,QAAQ,CAAC,IAAI,IAAI;CAAE,QAAQ,CAAC,KAAK,GAAG;CAAE;AAE9D,MAAa,UAAU;CACrB,SAAS;CACT,SAAS;CACT,SAAS;CACT,SAAS;CACV;AAED,MAAa,kBAAkB;CAC7B,qBAAK,IAAI,OAAO,IAAI,SAAS,IAAI,KAAK,GAAG,CAAC,GAAG;CAC7C,qBAAK,IAAI,OAAO,IAAI,SAAS,IAAI,KAAK,GAAG,CAAC,GAAG;CAC7C,sBAAM,IAAI,OAAO,IAAI,CAAC,GAAG,SAAS,KAAK,GAAG,SAAS,IAAI,CAAC,KAAK,GAAG,CAAC,GAAG;CACpE,mBAAmB;CACnB,eAAe;CAEf,SAAS,IAAI,OAAO,QAAQ,QAAQ;CACpC,SAAS,IAAI,OAAO,QAAQ,QAAQ;CACpC,SAAS,IAAI,OAAO,QAAQ,QAAQ;CAEpC,SAAS,IAAI,OAAO,QAAQ,QAAQ;CAEpC,qBAAK,IAAI,OACP,IAAI;EAAC,QAAQ;EAAS,QAAQ;EAAS,QAAQ;EAAQ,CAAC,KAAK,GAAG,CAAC,GAClE;CAMF;;;;;;;;;;;;;;;;;;;;;;;AAwBD,MAAM,oBAAoB,WACxBA,SAIE,WAIA,mDAEAA,SAAoB,OAAO,EAK3B,SACD;AAEH,MAAa,mBAAmB;CAC9B,KAAK;CACL,KAAKC,QAAmB,gBAAgB,QAAQ;CAChD,IAAID,SAAoBC,QAAmB,gBAAgB,IAAI,CAAC;CAChE,IAAID,SAAoBC,QAAmB,gBAAgB,IAAI,CAAC;CAEhE,WAAWC,MACTD,QACE,0BACA,KACA,gCACD,EACDD,SAAoB,gBAAgB,QAAQ,CAC7C;CACD,WAAWE,MACTD,QACE,yBACA,KACA,4CACD,EACDD,SAAoB,gBAAgB,QAAQ,CAC7C;CACD,QAAQE,MACND,QACE,YACA,KACA,iBACD,EACDD,SAAoB,gBAAgB,QAAQ,CAC7C;CACD,QAAQE,MACND,QACE,WACA,KACA,6BACD,EACDD,SAAoB,gBAAgB,QAAQ,CAC7C;CACD,KAAKE,MACHF,SACEC,QACE,kBACD,EACDD,SAAoB,gBAAgB,QAAQ,CAC7C,CACF;CACD,QAAQ,iBAAiB,gBAAgB,QAAQ;CACjD,QAAQ,iBAAiB,gBAAgB,QAAQ;CAClD"}
1
+ {"version":3,"file":"index.js","names":["Patterning.optional","Patterning.capture","Patterning.merge"],"sources":["../../../../src/coordinates/latlon/internal/index.ts"],"sourcesContent":["// __private-exports\n/*\n * Copyright 2024 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport * as Patterning from '@/patterning';\n\nexport type Axes = 'LAT' | 'LON';\nexport type Compass = 'N' | 'S' | 'E' | 'W';\nexport type Errors = string[];\nexport type Format = (typeof FORMATS)[number];\n\n/**\n * Bearings are the consistent/explicit identifiers of directionality of a\n * coordinate component; this library has opted for these over implicit\n * indication by number sign not because there is an inherent superiority\n * but because something had to be chosen.\n *\n * NOTE: these arrays are position-important; negative values are [1] and\n * positive values are [0] so that they can be consistently indexed using\n * an `isNegative` boolean to reference the negative bearing of each axis\n */\nexport const BEARINGS = {\n LAT: ['N', 'S'],\n LON: ['E', 'W'],\n LATLON: [\n ['N', 'S'],\n ['E', 'W'],\n ],\n LONLAT: [\n ['E', 'W'],\n ['N', 'S'],\n ],\n} as const;\n\nexport const FORMATS = ['LATLON', 'LONLAT'] as const;\nexport const FORMATS_DEFAULT = FORMATS[0];\n\nexport const LIMITS = { LATLON: [90, 180], LONLAT: [180, 90] } as const;\n\nexport const SYMBOLS = {\n DEGREES: '°',\n MINUTES: \"'\",\n SECONDS: '\"',\n DIVIDER: '/',\n};\n\nexport const SYMBOL_PATTERNS = {\n LAT: new RegExp(`[${BEARINGS.LAT.join('')}]`),\n LON: new RegExp(`[${BEARINGS.LON.join('')}]`),\n NSEW: new RegExp(`[${[...BEARINGS.LAT, ...BEARINGS.LON].join('')}]`),\n NEGATIVE_BEARINGS: /[SW]/i,\n NEGATIVE_SIGN: /-/,\n\n DEGREES: new RegExp(SYMBOLS.DEGREES),\n MINUTES: new RegExp(SYMBOLS.MINUTES),\n SECONDS: new RegExp(SYMBOLS.SECONDS),\n\n DIVIDER: new RegExp(SYMBOLS.DIVIDER),\n\n DMS: new RegExp(\n `[${[SYMBOLS.DEGREES, SYMBOLS.MINUTES, SYMBOLS.SECONDS].join('')}]`,\n ),\n\n // divider: {\n // first: /(?<NAMED_SEPARATOR>:?)/,\n // follow: new RegExp(`\\\\s?\\\\k<${'NAMED_SEPARATOR'}>\\\\s?`),\n // },\n} as const;\n\n/**\n * Creates a regex pattern for matching decimal minutes or seconds values.\n *\n * Generates a pattern that matches numeric values in the range 0-59.999... with optional\n * leading zeros, decimal points, and symbol indicators, using lookbehind and lookahead\n * to prevent partial matches within larger numbers.\n *\n * @param symbol - Regular expression for the symbol (minutes ' or seconds \" indicator).\n * @returns Combined regex pattern with precise boundary matching.\n *\n * @example\n * ```typescript\n * const minutesPattern = decimalSecAndMin(SYMBOL_PATTERNS.MINUTES);\n * // Matches: \"30.5'\", \"59.999999999\", \".5'\", \"001'\", etc.\n * ```\n *\n * @example\n * ```typescript\n * const secondsPattern = decimalSecAndMin(SYMBOL_PATTERNS.SECONDS);\n * // Matches: '45.23\"', '0.5', '59.9999999999\"', etc.\n * ```\n */\nconst decimalSecAndMin = (symbol: RegExp) =>\n Patterning.optional(\n // Negative lookbehind\n // to ensure that the match is not preceded by a digit,\n // avoiding partial matches within larger numbers.\n /(?<!\\d)/,\n\n // 0-59 including 10 decimal places and leading zeros or no number before\n // acceptable values: 0, 0.1234567890, .9876543210, 001, 59.9999999999\n /([-+]?0*(?:[0-5]?\\d|\\.\\d{1,10})(?:\\.\\d{1,10})?)/,\n\n Patterning.optional(symbol),\n\n // Negative lookahead\n // to ensure that the match is not followed by a digit,\n // avoiding partial matches within larger numbers.\n /(?!\\d)/,\n );\n\nexport const PARTIAL_PATTERNS = {\n ' ': /\\s*/,\n '/': Patterning.capture(SYMBOL_PATTERNS.DIVIDER),\n NS: Patterning.optional(Patterning.capture(SYMBOL_PATTERNS.LAT)),\n EW: Patterning.optional(Patterning.capture(SYMBOL_PATTERNS.LON)),\n\n degLatDec: Patterning.merge(\n Patterning.capture(\n /0*(?:90(?:\\.0{1,10})?)/, // 90[.0]\n /|/,\n /(?:0?[0-8]?\\d(?:\\.\\d{1,10})?)/, // [0]0[.0]-89[.9]\n ),\n Patterning.optional(SYMBOL_PATTERNS.DEGREES),\n ),\n degLonDec: Patterning.merge(\n Patterning.capture(\n /(?:180(?:\\.0{1,10})?)/, // 180[.0]\n /|/,\n /(?:0*(?:\\d{1,2}|1[0-7]\\d)(?:\\.\\d{1,10})?)/, // [00]0[.0]-179[.9]\n ),\n Patterning.optional(SYMBOL_PATTERNS.DEGREES),\n ),\n degLat: Patterning.merge(\n Patterning.capture(\n /(?:0?90)/, // 90\n /|/,\n /(?:0?[0-8]?\\d)/, // [0]0-89\n ),\n Patterning.optional(SYMBOL_PATTERNS.DEGREES),\n ),\n degLon: Patterning.merge(\n Patterning.capture(\n /(?:180)/, // 180\n /|/,\n /(?:0*(?:\\d{1,2}|1[0-7]\\d))/, // [00]0-179\n ),\n Patterning.optional(SYMBOL_PATTERNS.DEGREES),\n ),\n min: Patterning.merge(\n Patterning.optional(\n Patterning.capture(\n /(?:0?[0-5]?\\d)?/, // [0]0-59\n ),\n Patterning.optional(SYMBOL_PATTERNS.MINUTES),\n ),\n ),\n minDec: decimalSecAndMin(SYMBOL_PATTERNS.MINUTES),\n secDec: decimalSecAndMin(SYMBOL_PATTERNS.SECONDS),\n} as const;\n\nexport {\n type CoordinateInput,\n type CoordinateInternalValue,\n type CoordinateObject,\n type CoordinateTuple,\n isCoordinateObject,\n isCoordinateTuple,\n type LatLonTuple,\n type LonLatTuple,\n normalizeObjectToLatLon,\n tupleToLatLon,\n} from './normalize';\nexport { toPlainDecimalString } from './plain-decimal';\nexport {\n isFiniteNumber,\n isValidNumericCoordinate,\n validateNumericCoordinate,\n validateSignedRange,\n} from './validate';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAa,WAAW;CACtB,KAAK,CAAC,KAAK,IAAI;CACf,KAAK,CAAC,KAAK,IAAI;CACf,QAAQ,CACN,CAAC,KAAK,IAAI,EACV,CAAC,KAAK,IAAI,CACX;CACD,QAAQ,CACN,CAAC,KAAK,IAAI,EACV,CAAC,KAAK,IAAI,CACX;CACF;AAED,MAAa,UAAU,CAAC,UAAU,SAAS;AAC3C,MAAa,kBAAkB,QAAQ;AAEvC,MAAa,SAAS;CAAE,QAAQ,CAAC,IAAI,IAAI;CAAE,QAAQ,CAAC,KAAK,GAAG;CAAE;AAE9D,MAAa,UAAU;CACrB,SAAS;CACT,SAAS;CACT,SAAS;CACT,SAAS;CACV;AAED,MAAa,kBAAkB;CAC7B,qBAAK,IAAI,OAAO,IAAI,SAAS,IAAI,KAAK,GAAG,CAAC,GAAG;CAC7C,qBAAK,IAAI,OAAO,IAAI,SAAS,IAAI,KAAK,GAAG,CAAC,GAAG;CAC7C,sBAAM,IAAI,OAAO,IAAI,CAAC,GAAG,SAAS,KAAK,GAAG,SAAS,IAAI,CAAC,KAAK,GAAG,CAAC,GAAG;CACpE,mBAAmB;CACnB,eAAe;CAEf,SAAS,IAAI,OAAO,QAAQ,QAAQ;CACpC,SAAS,IAAI,OAAO,QAAQ,QAAQ;CACpC,SAAS,IAAI,OAAO,QAAQ,QAAQ;CAEpC,SAAS,IAAI,OAAO,QAAQ,QAAQ;CAEpC,qBAAK,IAAI,OACP,IAAI;EAAC,QAAQ;EAAS,QAAQ;EAAS,QAAQ;EAAQ,CAAC,KAAK,GAAG,CAAC,GAClE;CAMF;;;;;;;;;;;;;;;;;;;;;;;AAwBD,MAAM,oBAAoB,WACxBA,SAIE,WAIA,mDAEAA,SAAoB,OAAO,EAK3B,SACD;AAEH,MAAa,mBAAmB;CAC9B,KAAK;CACL,KAAKC,QAAmB,gBAAgB,QAAQ;CAChD,IAAID,SAAoBC,QAAmB,gBAAgB,IAAI,CAAC;CAChE,IAAID,SAAoBC,QAAmB,gBAAgB,IAAI,CAAC;CAEhE,WAAWC,MACTD,QACE,0BACA,KACA,gCACD,EACDD,SAAoB,gBAAgB,QAAQ,CAC7C;CACD,WAAWE,MACTD,QACE,yBACA,KACA,4CACD,EACDD,SAAoB,gBAAgB,QAAQ,CAC7C;CACD,QAAQE,MACND,QACE,YACA,KACA,iBACD,EACDD,SAAoB,gBAAgB,QAAQ,CAC7C;CACD,QAAQE,MACND,QACE,WACA,KACA,6BACD,EACDD,SAAoB,gBAAgB,QAAQ,CAC7C;CACD,KAAKE,MACHF,SACEC,QACE,kBACD,EACDD,SAAoB,gBAAgB,QAAQ,CAC7C,CACF;CACD,QAAQ,iBAAiB,gBAAgB,QAAQ;CACjD,QAAQ,iBAAiB,gBAAgB,QAAQ;CAClD"}
@@ -12,6 +12,7 @@
12
12
 
13
13
 
14
14
  import { capture, group, merge, optional } from "../../../patterning.js";
15
+ import { toPlainDecimalString } from "./plain-decimal.js";
15
16
  import { SYMBOLS, SYMBOL_PATTERNS } from "./index.js";
16
17
 
17
18
  //#region src/coordinates/latlon/internal/lexer.ts
@@ -62,7 +63,7 @@ const TOKENS = new RegExp(merge(DIVIDERS, /|/, SYMBOL_PATTERNS.NSEW, /|/, group(
62
63
  */
63
64
  function fixLeadingAndTrailing(t) {
64
65
  const [sign, num, pos] = (FLOATS.exec(t) ?? []).slice(1);
65
- if (num) return `${sign}${Number.parseFloat(num)}${pos}`;
66
+ if (num) return `${sign}${toPlainDecimalString(Number.parseFloat(num))}${pos}`;
66
67
  return t;
67
68
  }
68
69
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"lexer.js","names":["Patterning.merge","Patterning.capture","Patterning.group","Patterning.optional"],"sources":["../../../../src/coordinates/latlon/internal/lexer.ts"],"sourcesContent":["// __private-exports\n/*\n * Copyright 2024 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport * as Patterning from '@/patterning';\nimport { SYMBOL_PATTERNS, SYMBOLS } from '.';\n\nexport type Tokens = ReturnType<typeof lexer>;\n\n/**\n * Separating latitude from longitude portions of a coordinate. At this level\n * of pattern matching this list can not include the \"space\" character since\n * that is valid between components of either side of a divider; higher level\n * parsers will be able to make up for this shortcoming and be more intelligent\n * about deducing where a divider could be added or would not be valid.\n */\nconst DIVIDERS = /[,/]/g;\nconst FLOATS = /^(-?)([\\d.]+)([^.\\d]?)$/;\n/** Positional indicators for: degrees, minutes, and seconds */\nconst POSITIONAL = new RegExp(\n Patterning.merge(/\\s*/, Patterning.capture(SYMBOL_PATTERNS.DMS), /\\s*/),\n 'g',\n);\nconst POSITIVE = /\\+/g;\nconst SIGNS = /([-+])\\s*/g;\n/**\n * Any recognizably significant tokens anywhere (non-positional-y) within a\n * string; because at this level (lexing) actual position is not important.\n *\n * - [Regex Vis](https://regex-vis.com/?r=%2F%5B%2C%2F%5D%7C%5BNSEW%5D%7C%28%3F%3A%5B-%2B%5D%3F%28%3F%3A%28%3F%3A%5Cd%2B%28%3F%3A%5C.%5Cd*%29%3F%29%7C%28%3F%3A%5C.%5Cd%2B%29%29%28%3F%3A%5B%C2%B0%27%22%5D%29%3F%29%2Fgi)\n * - [Nodexr](https://www.nodexr.net/?parse=%2F%5B,%2F%5D%7C%5BNSEW%5D%7C(%3F%3A%5B-%2B%5D%3F(%3F%3A(%3F%3A%5Cd%2B(%3F%3A%5C.%5Cd*)%3F)%7C(%3F%3A%5C.%5Cd%2B))%5B%C2%B0%27%22%5D%3F)%2Fgi)\n */\n// NOTE: the links (above) for \"Regex Vis\" and \"Nodexr\" would need to be updated if/when the pattern is changed.\nconst TOKENS = new RegExp(\n Patterning.merge(\n DIVIDERS,\n /|/,\n SYMBOL_PATTERNS.NSEW,\n /|/,\n Patterning.group(\n /[-+]?/,\n Patterning.group(\n // left of decimal REQUIRED, right of decimal optional\n /(?:\\d+(?:\\.\\d*)?)|/,\n // left of decimal omitted, right of decimal REQUIRED\n /(?:\\.\\d+)/,\n ),\n Patterning.optional(SYMBOL_PATTERNS.DMS),\n ),\n ),\n 'gi',\n);\n\n/**\n * Remove trailing zeros '?.0' and ensure leading zero '0.?' in numbers.\n *\n * @param t - Token string to normalize.\n * @returns Normalized token with cleaned numeric formatting.\n *\n * @example\n * ```typescript\n * fixLeadingAndTrailing('45.0°');\n * // '45°'\n * ```\n *\n * @example\n * ```typescript\n * fixLeadingAndTrailing('.5'');\n * // '0.5''\n * ```\n *\n * @example\n * ```typescript\n * fixLeadingAndTrailing('-122.00');\n * // '-122'\n * ```\n */\nfunction fixLeadingAndTrailing(t: string) {\n const [sign, num, pos] = (FLOATS.exec(t) ?? []).slice(1);\n\n if (num) {\n return `${sign}${Number.parseFloat(num)}${pos}`;\n }\n\n return t;\n}\n\n/**\n * Take an input string - possibly from user input - and clean it up enough to\n * be something to work with at a higher level of processing (with more\n * information) than is available at this level. Generating a list of \"tokens\"\n * that are potentially valid parts of a coordinate. The values being looked\n * for are: numbers (with positional indicators) and axes (NSEW).\n *\n * NOTE: No validation is done at this level to keep it simple as agnostic.\n *\n * @remarks\n * pure function\n *\n * @example\n * ```typescript\n * lexer('N 55,E 44') === ['N' '55', '/', 'E', '44']\n * lexer(` + 89 ° 59 59.999 \" N, 179° 59 59.999\" `) === ['89', '59', '59.999', 'N', '/', '179', '59', '59.999', 'E']\n * ```\n */\nexport function lexer(input: string) {\n const tokens =\n input\n .trim()\n .toUpperCase()\n .replace(POSITIVE, '') // positive signs are redundant\n .replace(POSITIONAL, '$1 ') // group positional indicators with numbers\n .replace(SIGNS, '$1') // group signs with numbers\n .replace(DIVIDERS, SYMBOLS.DIVIDER) // standardize the divider\n .match(TOKENS)\n ?.map(fixLeadingAndTrailing)\n ?.slice() ?? [];\n\n return tokens;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyBA,MAAM,WAAW;AACjB,MAAM,SAAS;;AAEf,MAAM,aAAa,IAAI,OACrBA,MAAiB,OAAOC,QAAmB,gBAAgB,IAAI,EAAE,MAAM,EACvE,IACD;AACD,MAAM,WAAW;AACjB,MAAM,QAAQ;;;;;;;;AASd,MAAM,SAAS,IAAI,OACjBD,MACE,UACA,KACA,gBAAgB,MAChB,KACAE,MACE,SACAA,MAEE,sBAEA,YACD,EACDC,SAAoB,gBAAgB,IAAI,CACzC,CACF,EACD,KACD;;;;;;;;;;;;;;;;;;;;;;;;;AA0BD,SAAS,sBAAsB,GAAW;CACxC,MAAM,CAAC,MAAM,KAAK,QAAQ,OAAO,KAAK,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE;AAExD,KAAI,IACF,QAAO,GAAG,OAAO,OAAO,WAAW,IAAI,GAAG;AAG5C,QAAO;;;;;;;;;;;;;;;;;;;;AAqBT,SAAgB,MAAM,OAAe;AAanC,QAXE,MACG,MAAM,CACN,aAAa,CACb,QAAQ,UAAU,GAAG,CACrB,QAAQ,YAAY,MAAM,CAC1B,QAAQ,OAAO,KAAK,CACpB,QAAQ,UAAU,QAAQ,QAAQ,CAClC,MAAM,OAAO,EACZ,IAAI,sBAAsB,EAC1B,OAAO,IAAI,EAAE"}
1
+ {"version":3,"file":"lexer.js","names":["Patterning.merge","Patterning.capture","Patterning.group","Patterning.optional"],"sources":["../../../../src/coordinates/latlon/internal/lexer.ts"],"sourcesContent":["// __private-exports\n/*\n * Copyright 2024 Hypergiant Galactic Systems Inc. All rights reserved.\n * This file is licensed to you under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License. You may obtain a copy\n * of the License at https://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software distributed under\n * the License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS\n * OF ANY KIND, either express or implied. See the License for the specific language\n * governing permissions and limitations under the License.\n */\n\nimport * as Patterning from '@/patterning';\nimport { SYMBOL_PATTERNS, SYMBOLS } from '.';\nimport { toPlainDecimalString } from './plain-decimal';\n\nexport type Tokens = ReturnType<typeof lexer>;\n\n/**\n * Separating latitude from longitude portions of a coordinate. At this level\n * of pattern matching this list can not include the \"space\" character since\n * that is valid between components of either side of a divider; higher level\n * parsers will be able to make up for this shortcoming and be more intelligent\n * about deducing where a divider could be added or would not be valid.\n */\nconst DIVIDERS = /[,/]/g;\nconst FLOATS = /^(-?)([\\d.]+)([^.\\d]?)$/;\n/** Positional indicators for: degrees, minutes, and seconds */\nconst POSITIONAL = new RegExp(\n Patterning.merge(/\\s*/, Patterning.capture(SYMBOL_PATTERNS.DMS), /\\s*/),\n 'g',\n);\nconst POSITIVE = /\\+/g;\nconst SIGNS = /([-+])\\s*/g;\n/**\n * Any recognizably significant tokens anywhere (non-positional-y) within a\n * string; because at this level (lexing) actual position is not important.\n *\n * - [Regex Vis](https://regex-vis.com/?r=%2F%5B%2C%2F%5D%7C%5BNSEW%5D%7C%28%3F%3A%5B-%2B%5D%3F%28%3F%3A%28%3F%3A%5Cd%2B%28%3F%3A%5C.%5Cd*%29%3F%29%7C%28%3F%3A%5C.%5Cd%2B%29%29%28%3F%3A%5B%C2%B0%27%22%5D%29%3F%29%2Fgi)\n * - [Nodexr](https://www.nodexr.net/?parse=%2F%5B,%2F%5D%7C%5BNSEW%5D%7C(%3F%3A%5B-%2B%5D%3F(%3F%3A(%3F%3A%5Cd%2B(%3F%3A%5C.%5Cd*)%3F)%7C(%3F%3A%5C.%5Cd%2B))%5B%C2%B0%27%22%5D%3F)%2Fgi)\n */\n// NOTE: the links (above) for \"Regex Vis\" and \"Nodexr\" would need to be updated if/when the pattern is changed.\nconst TOKENS = new RegExp(\n Patterning.merge(\n DIVIDERS,\n /|/,\n SYMBOL_PATTERNS.NSEW,\n /|/,\n Patterning.group(\n /[-+]?/,\n Patterning.group(\n // left of decimal REQUIRED, right of decimal optional\n /(?:\\d+(?:\\.\\d*)?)|/,\n // left of decimal omitted, right of decimal REQUIRED\n /(?:\\.\\d+)/,\n ),\n Patterning.optional(SYMBOL_PATTERNS.DMS),\n ),\n ),\n 'gi',\n);\n\n/**\n * Remove trailing zeros '?.0' and ensure leading zero '0.?' in numbers.\n *\n * @param t - Token string to normalize.\n * @returns Normalized token with cleaned numeric formatting.\n *\n * @example\n * ```typescript\n * fixLeadingAndTrailing('45.0°');\n * // '45°'\n * ```\n *\n * @example\n * ```typescript\n * fixLeadingAndTrailing('.5'');\n * // '0.5''\n * ```\n *\n * @example\n * ```typescript\n * fixLeadingAndTrailing('-122.00');\n * // '-122'\n * ```\n */\nfunction fixLeadingAndTrailing(t: string) {\n const [sign, num, pos] = (FLOATS.exec(t) ?? []).slice(1);\n\n if (num) {\n // Plain notation: `${1e-7}` would be '1e-7', whose '-7' re-lexes as a sign.\n return `${sign}${toPlainDecimalString(Number.parseFloat(num))}${pos}`;\n }\n\n return t;\n}\n\n/**\n * Take an input string - possibly from user input - and clean it up enough to\n * be something to work with at a higher level of processing (with more\n * information) than is available at this level. Generating a list of \"tokens\"\n * that are potentially valid parts of a coordinate. The values being looked\n * for are: numbers (with positional indicators) and axes (NSEW).\n *\n * NOTE: No validation is done at this level to keep it simple as agnostic.\n *\n * @remarks\n * pure function\n *\n * @example\n * ```typescript\n * lexer('N 55,E 44') === ['N' '55', '/', 'E', '44']\n * lexer(` + 89 ° 59 59.999 \" N, 179° 59 59.999\" `) === ['89', '59', '59.999', 'N', '/', '179', '59', '59.999', 'E']\n * ```\n */\nexport function lexer(input: string) {\n const tokens =\n input\n .trim()\n .toUpperCase()\n .replace(POSITIVE, '') // positive signs are redundant\n .replace(POSITIONAL, '$1 ') // group positional indicators with numbers\n .replace(SIGNS, '$1') // group signs with numbers\n .replace(DIVIDERS, SYMBOLS.DIVIDER) // standardize the divider\n .match(TOKENS)\n ?.map(fixLeadingAndTrailing)\n ?.slice() ?? [];\n\n return tokens;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AA0BA,MAAM,WAAW;AACjB,MAAM,SAAS;;AAEf,MAAM,aAAa,IAAI,OACrBA,MAAiB,OAAOC,QAAmB,gBAAgB,IAAI,EAAE,MAAM,EACvE,IACD;AACD,MAAM,WAAW;AACjB,MAAM,QAAQ;;;;;;;;AASd,MAAM,SAAS,IAAI,OACjBD,MACE,UACA,KACA,gBAAgB,MAChB,KACAE,MACE,SACAA,MAEE,sBAEA,YACD,EACDC,SAAoB,gBAAgB,IAAI,CACzC,CACF,EACD,KACD;;;;;;;;;;;;;;;;;;;;;;;;;AA0BD,SAAS,sBAAsB,GAAW;CACxC,MAAM,CAAC,MAAM,KAAK,QAAQ,OAAO,KAAK,EAAE,IAAI,EAAE,EAAE,MAAM,EAAE;AAExD,KAAI,IAEF,QAAO,GAAG,OAAO,qBAAqB,OAAO,WAAW,IAAI,CAAC,GAAG;AAGlE,QAAO;;;;;;;;;;;;;;;;;;;;AAqBT,SAAgB,MAAM,OAAe;AAanC,QAXE,MACG,MAAM,CACN,aAAa,CACb,QAAQ,UAAU,GAAG,CACrB,QAAQ,YAAY,MAAM,CAC1B,QAAQ,OAAO,KAAK,CACpB,QAAQ,UAAU,QAAQ,QAAQ,CAClC,MAAM,OAAO,EACZ,IAAI,sBAAsB,EAC1B,OAAO,IAAI,EAAE"}
@@ -1,11 +1,47 @@
1
1
  //#region src/coordinates/latlon/internal/ordinal.d.ts
2
+ /**
3
+ * Axis discriminator for a single coordinate value.
4
+ *
5
+ * `'lat'` selects the N/S hemisphere pair; `'lon'` selects E/W.
6
+ */
7
+ type Axis = 'lat' | 'lon';
8
+ /**
9
+ * Hemisphere letter for a coordinate value, following geo's `>= 0`
10
+ * convention (0 maps to `N` on the lat axis and `E` on the lon axis).
11
+ */
12
+ type Hemisphere = 'N' | 'S' | 'E' | 'W';
13
+ /**
14
+ * Gets the typed hemisphere letter for a signed coordinate value on an axis.
15
+ *
16
+ * Follows the same `>= 0` convention as {@link getOrdinal}: a value of exactly
17
+ * `0` maps to `N` on the latitude axis and `E` on the longitude axis.
18
+ *
19
+ * @param value - The signed coordinate value.
20
+ * @param axis - Whether the value is a latitude (`'lat'`) or longitude (`'lon'`).
21
+ * @returns Hemisphere letter: `'N'`, `'S'`, `'E'`, or `'W'`.
22
+ *
23
+ * @remarks pure function
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * getHemisphere(-77.0369, 'lon');
28
+ * // 'W'
29
+ * ```
30
+ */
31
+ declare const getHemisphere: (value: number, axis: Axis) => Hemisphere;
2
32
  /**
3
33
  * Gets the ordinal direction (N/S/E/W) for a coordinate value.
4
34
  *
5
- * @param num - The coordinate value (positive or negative).
35
+ * Retained as the established public API. Adapts {@link getHemisphere} — which
36
+ * owns the `>= 0` convention — to a boolean axis and the wider `string` return
37
+ * its existing callers expect.
38
+ *
39
+ * @param value - The coordinate value (positive or negative).
6
40
  * @param isLatitude - Whether this is a latitude coordinate (true) or longitude (false).
7
41
  * @returns Ordinal direction character: 'N', 'S', 'E', or 'W'.
8
42
  *
43
+ * @remarks pure function
44
+ *
9
45
  * @example
10
46
  * ```typescript
11
47
  * getOrdinal(37.7749, true);
@@ -17,14 +53,8 @@
17
53
  * getOrdinal(-122.4194, false);
18
54
  * // 'W'
19
55
  * ```
20
- *
21
- * @example
22
- * ```typescript
23
- * getOrdinal(-45, true);
24
- * // 'S'
25
- * ```
26
56
  */
27
- declare const getOrdinal: (num: number, isLatitude: boolean) => string;
57
+ declare const getOrdinal: (value: number, isLatitude: boolean) => string;
28
58
  //#endregion
29
- export { getOrdinal };
59
+ export { Axis, Hemisphere, getHemisphere, getOrdinal };
30
60
  //# sourceMappingURL=ordinal.d.ts.map
@@ -13,12 +13,40 @@
13
13
 
14
14
  //#region src/coordinates/latlon/internal/ordinal.ts
15
15
  /**
16
+ * Gets the typed hemisphere letter for a signed coordinate value on an axis.
17
+ *
18
+ * Follows the same `>= 0` convention as {@link getOrdinal}: a value of exactly
19
+ * `0` maps to `N` on the latitude axis and `E` on the longitude axis.
20
+ *
21
+ * @param value - The signed coordinate value.
22
+ * @param axis - Whether the value is a latitude (`'lat'`) or longitude (`'lon'`).
23
+ * @returns Hemisphere letter: `'N'`, `'S'`, `'E'`, or `'W'`.
24
+ *
25
+ * @remarks pure function
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * getHemisphere(-77.0369, 'lon');
30
+ * // 'W'
31
+ * ```
32
+ */
33
+ const getHemisphere = (value, axis) => {
34
+ if (axis === "lat") return value >= 0 ? "N" : "S";
35
+ return value >= 0 ? "E" : "W";
36
+ };
37
+ /**
16
38
  * Gets the ordinal direction (N/S/E/W) for a coordinate value.
17
39
  *
18
- * @param num - The coordinate value (positive or negative).
40
+ * Retained as the established public API. Adapts {@link getHemisphere} — which
41
+ * owns the `>= 0` convention — to a boolean axis and the wider `string` return
42
+ * its existing callers expect.
43
+ *
44
+ * @param value - The coordinate value (positive or negative).
19
45
  * @param isLatitude - Whether this is a latitude coordinate (true) or longitude (false).
20
46
  * @returns Ordinal direction character: 'N', 'S', 'E', or 'W'.
21
47
  *
48
+ * @remarks pure function
49
+ *
22
50
  * @example
23
51
  * ```typescript
24
52
  * getOrdinal(37.7749, true);
@@ -30,18 +58,9 @@
30
58
  * getOrdinal(-122.4194, false);
31
59
  * // 'W'
32
60
  * ```
33
- *
34
- * @example
35
- * ```typescript
36
- * getOrdinal(-45, true);
37
- * // 'S'
38
- * ```
39
61
  */
40
- const getOrdinal = (num, isLatitude) => {
41
- if (isLatitude) return num >= 0 ? "N" : "S";
42
- return num >= 0 ? "E" : "W";
43
- };
62
+ const getOrdinal = (value, isLatitude) => getHemisphere(value, isLatitude ? "lat" : "lon");
44
63
 
45
64
  //#endregion
46
- export { getOrdinal };
65
+ export { getHemisphere, getOrdinal };
47
66
  //# sourceMappingURL=ordinal.js.map