@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # @accelint/geo
2
2
 
3
+ ## 0.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - dcaba41: Add a coordinate "parts" API that returns the structured pieces of a formatted coordinate instead of only a finished string. `toDecimalDegreesParts(value, axis, precision?)`, `toDdmParts(value, axis, precision?)`, and `toDmsParts(value, axis, precision?)` take a signed number plus its axis and return the non-negative `degrees`/`minutes`/`seconds` numbers and the `hemisphere` letter (`'N' | 'S' | 'E' | 'W'`), with the `60″ → +1′ → +1°` and `60′ → +1°` carry already applied. `toMgrsParts([lat, lon])` and `toUtmParts([lat, lon])` read the grid fields directly and return a discriminated result — `{ ok: true, value: … }` for in-band coordinates or `{ ok: false, reason: 'out-of-range' }` for latitudes outside the inclusive `80°S`–`84°N` band, and for a longitude of exactly `+180°` (the antimeridian, where the UTM zone is undefined) — so callers can branch on validity without matching thrown error text, and never have to guard against a thrown exception. The result stays total even when `geodesy` itself rejects a coordinate (for example an unpatched `geodesy@2.4.0` at the 84°N edge): that also maps to `{ ok: false }`. The supporting types (`DecimalDegreesParts`, `DdmParts`, `DmsParts`, `MgrsParts`, `UtmParts`, `GridPartsResult`) are exported alongside them.
8
+
9
+ `formatMgrsParts(MgrsParts)` and `formatUtmParts(UtmParts)` render grid parts back into their canonical strings, so callers that already hold parts (or want to consume `toMgrsParts`/`toUtmParts` and render without re-deriving the pad/floor/join logic) share one renderer instead of duplicating it. The boolean `isValidNumericCoordinate(lat, lon)` predicate is also exported for callers that need a finite-and-in-range check without the error-message array `validateNumericCoordinate` builds.
10
+
11
+ The existing `format*`/`parse*` functions and `createCoordinate(...).mgrs()`/`.utm()` now compose over this parts layer internally; their string output is byte-identical to before.
12
+
13
+ Also newly exported, since the parts layer is built from them: `getHemisphere(value, axis)` with the `Axis` and `Hemisphere` types (the typed core behind the existing `getOrdinal`); the grid-band helpers `isWithinGridBand(lat)`, `isOnEasternAntimeridian(lon)`, and `isGridProjectable([lat, lon])` with the `GRID_LATITUDE_MIN` / `GRID_LATITUDE_MAX` bounds; the display-precision defaults `DECIMAL_DEGREES_PRECISION`, `DDM_PRECISION`, and `DMS_PRECISION`; and `formatCoordinateSystem`, the lossless round-trip scaffold the `CoordinateSystem` `toFormat` implementations share (distinct from the display formatters — it applies no rounding or carry).
14
+
15
+ ### Patch Changes
16
+
17
+ - 747ea86: Export `formatCoordinate` and `normalizeLongitude` from `@accelint/map-toolkit/cursor-coordinates`. `formatCoordinate(lonLat, format)` is the pure formatter behind `useCursorCoordinates` — reach for it to render a DD/DDM/DMS/MGRS/UTM string outside the hook instead of re-deriving the grid-conversion logic.
18
+
19
+ Also stop a `RangeError` escaping `formatCoordinate` at the UTM/MGRS latitude boundaries (84°N and 80°S). Both are valid in those systems, but `geodesy@2.4.0` rejected them — 84°N via too-strict northing bounds (widest in the extended Svalbard zones), 80°S via a floating-point error in the latitude-band lookup. Two layers: this monorepo patches `geodesy` (`patches/geodesy@2.4.0.patch`, a workspace-only pnpm patch that does not ship in the published package), and `@accelint/geo`'s grid-parts functions now return `{ ok: false }` instead of throwing whenever geodesy rejects a coordinate — so `formatCoordinate` shows the `--- -- ---- ----` placeholder rather than crashing, even against an unpatched `geodesy`. Coordinates outside the valid band still report `valid: false` as before.
20
+
21
+ - 9345871: Fix parsing and formatting of coordinate magnitudes below `1e-6°` (within about 11 cm of the equator or prime meridian). JavaScript renders such numbers in exponential notation (`String(0.0000001)` is `'1e-7'`), which the lexer mis-tokenized — the `-7` read as a sign, so `'0.0000001 N / 0 E'` failed with "Bearing (N) conflicts with negative number" — and which `createCoordinate(...).dd()`/`.ddm()`/`.dms()` emitted verbatim. Both paths now use the new `toPlainDecimalString(value)` helper, which renders any number in plain decimal notation; ordinary coordinates are unaffected.
22
+
3
23
  ## 0.6.1
4
24
 
5
25
  ### Patch Changes
package/README.md CHANGED
@@ -12,6 +12,8 @@ Geographic coordinate parsing, conversion, and formatting for multiple coordinat
12
12
  - Universal Transverse Mercator (UTM): `18N 585628 4511644`
13
13
 
14
14
  - **Flexible Format Ordering**: Convert between LATLON and LONLAT formats
15
+ - **Structured Parts API**: Get the numeric pieces of a coordinate (`{ degrees, minutes, seconds, hemisphere }`, or grid components) instead of a formatted string — useful for segmented inputs and custom renderers
16
+ - **Standalone Functions**: Per-system `format*`/`parse*` helpers and validation predicates alongside the `createCoordinate` façade
15
17
  - **Input Validation**: Detailed error messages for invalid coordinates
16
18
  - **Performance Optimized**: Immutable coordinate objects with intelligent caching
17
19
  - **Type Safe**: Full TypeScript support with complete type definitions
@@ -19,7 +21,7 @@ Geographic coordinate parsing, conversion, and formatting for multiple coordinat
19
21
  ## Installation
20
22
 
21
23
  ```sh
22
- npm install @accelint/geo
24
+ pnpm add @accelint/geo
23
25
  ```
24
26
 
25
27
  ## Quick Start
@@ -207,6 +209,101 @@ const latlon: LatLonTuple = [40.7128, -74.0060]; // [latitude, longitude]
207
209
  const lonlat: LonLatTuple = [-74.0060, 40.7128]; // [longitude, latitude]
208
210
  ```
209
211
 
212
+ ## Structured Parts API
213
+
214
+ When you need the numeric components of a coordinate rather than a formatted string — driving segmented inputs, building a custom renderer, or feeding another calculation — reach for the parts functions instead of parsing a formatted string back apart.
215
+
216
+ ### Lat/lon parts
217
+
218
+ `toDecimalDegreesParts`, `toDdmParts`, and `toDmsParts` each take a single signed value plus an axis (`'lat'` or `'lon'`) and return the non-negative magnitude components plus a `hemisphere` letter. Carry is applied so `minutes` and `seconds` never reach `60`.
219
+
220
+ ```typescript
221
+ import { toDecimalDegreesParts, toDdmParts, toDmsParts } from '@accelint/geo';
222
+
223
+ toDecimalDegreesParts(-122.4194, 'lon');
224
+ // { degrees: 122.4194, hemisphere: 'W' }
225
+
226
+ toDdmParts(12.576, 'lat');
227
+ // { degrees: 12, minutes: 34.56, hemisphere: 'N' }
228
+
229
+ toDmsParts(-77.0369, 'lon');
230
+ // { degrees: 77, minutes: 2, seconds: 12.84, hemisphere: 'W' }
231
+ ```
232
+
233
+ Each accepts an optional trailing `precision` argument (defaults: DD `6`, DDM `4`, DMS `2`).
234
+
235
+ ### Grid parts
236
+
237
+ `toUtmParts` and `toMgrsParts` take a signed `[latitude, longitude]` tuple and read the grid components directly. They return a discriminated `GridPartsResult`: `{ ok: true, value }` on success, or `{ ok: false, reason: 'out-of-range' }` when the coordinate is non-finite, outside the inclusive `80°S`–`84°N` grid band, or exactly `+180°` longitude (where the UTM zone is undefined). They never throw, so callers branch on `ok` instead of catching an error.
238
+
239
+ ```typescript
240
+ import { toUtmParts, toMgrsParts, formatUtmParts, formatMgrsParts } from '@accelint/geo';
241
+
242
+ toUtmParts([38.8977, -77.0365]);
243
+ // { ok: true, value: { zone: 18, hemisphere: 'N', easting: 323394, northing: 4307396 } }
244
+
245
+ toUtmParts([85, 0]);
246
+ // { ok: false, reason: 'out-of-range' }
247
+
248
+ toMgrsParts([-81, 0]);
249
+ // { ok: false, reason: 'out-of-range' }
250
+ ```
251
+
252
+ `formatUtmParts` / `formatMgrsParts` render grid parts back into the canonical string, so a caller that already holds parts (or wants to consume `toUtmParts`/`toMgrsParts` and render) shares one renderer:
253
+
254
+ ```typescript
255
+ formatUtmParts({ zone: 18, hemisphere: 'N', easting: 323394, northing: 4307396 });
256
+ // '18N 323394 4307396'
257
+ ```
258
+
259
+ ### Helpers
260
+
261
+ The pieces the parts layer is built from are exported too:
262
+
263
+ - `getHemisphere(value, axis)` returns the typed `'N' | 'S' | 'E' | 'W'` letter for a signed value on `'lat'` or `'lon'` (`0` maps to `N`/`E`). `Axis` and `Hemisphere` are the matching types; `getOrdinal` is the older boolean-axis form of the same function.
264
+ - `isWithinGridBand(lat)`, `isOnEasternAntimeridian(lon)`, and `isGridProjectable([lat, lon])` are the checks behind `toUtmParts`/`toMgrsParts`, with the band edges exported as `GRID_LATITUDE_MIN` (`-80`) and `GRID_LATITUDE_MAX` (`84`).
265
+ - `DECIMAL_DEGREES_PRECISION` (`6`), `DDM_PRECISION` (`4`), and `DMS_PRECISION` (`2`) are the parts functions' default precisions.
266
+ - `toPlainDecimalString(value)` renders a number in plain decimal notation. JavaScript switches to exponential notation below `1e-6` (`String(0.0000001)` is `'1e-7'`); use this when building a coordinate string by hand from very small magnitudes.
267
+
268
+ ```typescript
269
+ import { getHemisphere, isGridProjectable, toPlainDecimalString } from '@accelint/geo';
270
+
271
+ getHemisphere(-77.0369, 'lon'); // 'W'
272
+ isGridProjectable([85, 0]); // false
273
+ toPlainDecimalString(0.0000001); // '0.0000001'
274
+ ```
275
+
276
+ ## Standalone Functions
277
+
278
+ The per-system `format*` and `parse*` functions are exported directly, so you can format a `[latitude, longitude]` tuple (or parse a single system's string) without building a `createCoordinate` object.
279
+
280
+ ```typescript
281
+ import { formatDecimalDegrees, formatDegreesMinutesSeconds } from '@accelint/geo';
282
+
283
+ formatDecimalDegrees([37.7749, -122.4194], { separator: ' / ', withOrdinal: true });
284
+ // '37.774900° N / 122.419400° W'
285
+
286
+ formatDegreesMinutesSeconds([37.7749, -122.4194], { separator: ' / ' });
287
+ // '37° 46' 29.64″ / 122° 25' 9.84″'
288
+ ```
289
+
290
+ Parsers: `parseDecimalDegrees`, `parseDegreesDecimalMinutes`, `parseDegreesMinutesSeconds`, `parseMGRS`, and `parseUTM` parse a single system's string.
291
+
292
+ ### Validation
293
+
294
+ `isValidNumericCoordinate` is a boolean check (finite and in range); `validateNumericCoordinate` returns an array of error messages (empty when valid).
295
+
296
+ ```typescript
297
+ import { isValidNumericCoordinate, validateNumericCoordinate } from '@accelint/geo';
298
+
299
+ isValidNumericCoordinate(45.5, -122.6); // true
300
+ isValidNumericCoordinate(91, -122.6); // false
301
+
302
+ validateNumericCoordinate(45.5, -122.6); // []
303
+ validateNumericCoordinate(91, -122.6);
304
+ // ['[ERROR] Latitude value (91) is outside valid range (-90 to 90).']
305
+ ```
306
+
210
307
  ## Coordinate System Formats
211
308
 
212
309
  ### Decimal Degrees (DD)
package/catalog-info.yaml CHANGED
@@ -15,7 +15,7 @@ metadata:
15
15
  annotations:
16
16
  backstage.io/edit-url: https://github.com/gohypergiant/standard-toolkit/blob/main/packages/geo/catalog-info.yaml
17
17
  backstage.io/techdocs-ref: dir:.
18
- package/version: 0.6.1
18
+ package/version: 0.7.0
19
19
  github.com/project-slug: gohypergiant/standard-toolkit
20
20
  links:
21
21
  - url: https://github.com/gohypergiant/standard-toolkit/tree/main/packages/geo
@@ -11,9 +11,42 @@
11
11
  */
12
12
 
13
13
  import { FormatOptions } from "../internal/format.js";
14
+ import { Axis, Hemisphere } from "../internal/ordinal.js";
14
15
 
15
16
  //#region src/coordinates/latlon/decimal-degrees/formatter.d.ts
16
-
17
+ /** Default number of decimal places for decimal-degrees formatting. */
18
+ declare const DECIMAL_DEGREES_PRECISION = 6;
19
+ /**
20
+ * Structured decimal-degrees parts for a single signed coordinate value.
21
+ *
22
+ * `degrees` is the non-negative magnitude rounded to the requested precision;
23
+ * the signed value is recoverable from the axis and `hemisphere`.
24
+ */
25
+ type DecimalDegreesParts = {
26
+ degrees: number;
27
+ hemisphere: Hemisphere;
28
+ };
29
+ /**
30
+ * Converts a single signed coordinate value into decimal-degrees parts.
31
+ *
32
+ * Returns the non-negative magnitude plus the hemisphere letter for the axis
33
+ * (following geo's `>= 0` convention). Decimal degrees has no minutes/seconds
34
+ * carry; the magnitude is simply rounded to the requested precision.
35
+ *
36
+ * @param value - The signed coordinate value.
37
+ * @param axis - Whether the value is a latitude (`'lat'`) or longitude (`'lon'`).
38
+ * @param precision - Decimal places for the magnitude (default `6`).
39
+ * @returns The `{ degrees, hemisphere }` parts object.
40
+ *
41
+ * @remarks pure function
42
+ *
43
+ * @example
44
+ * ```typescript
45
+ * toDecimalDegreesParts(-122.4194, 'lon');
46
+ * // { degrees: 122.4194, hemisphere: 'W' }
47
+ * ```
48
+ */
49
+ declare const toDecimalDegreesParts: (value: number, axis: Axis, precision?: number) => DecimalDegreesParts;
17
50
  /**
18
51
  * Formats latitude/longitude coordinates in decimal degrees notation.
19
52
  *
@@ -35,5 +68,5 @@ import { FormatOptions } from "../internal/format.js";
35
68
  */
36
69
  declare const formatDecimalDegrees: (coordinates: [number, number], config?: FormatOptions) => string;
37
70
  //#endregion
38
- export { formatDecimalDegrees };
71
+ export { DECIMAL_DEGREES_PRECISION, DecimalDegreesParts, formatDecimalDegrees, toDecimalDegreesParts };
39
72
  //# sourceMappingURL=formatter.d.ts.map
@@ -11,13 +11,42 @@
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/decimal-degrees/formatter.ts
18
+ /** Default number of decimal places for decimal-degrees formatting. */
19
+ const DECIMAL_DEGREES_PRECISION = 6;
20
+ /**
21
+ * Converts a single signed coordinate value into decimal-degrees parts.
22
+ *
23
+ * Returns the non-negative magnitude plus the hemisphere letter for the axis
24
+ * (following geo's `>= 0` convention). Decimal degrees has no minutes/seconds
25
+ * carry; the magnitude is simply rounded to the requested precision.
26
+ *
27
+ * @param value - The signed coordinate value.
28
+ * @param axis - Whether the value is a latitude (`'lat'`) or longitude (`'lon'`).
29
+ * @param precision - Decimal places for the magnitude (default `6`).
30
+ * @returns The `{ degrees, hemisphere }` parts object.
31
+ *
32
+ * @remarks pure function
33
+ *
34
+ * @example
35
+ * ```typescript
36
+ * toDecimalDegreesParts(-122.4194, 'lon');
37
+ * // { degrees: 122.4194, hemisphere: 'W' }
38
+ * ```
39
+ */
40
+ const toDecimalDegreesParts = (value, axis, precision = DECIMAL_DEGREES_PRECISION) => {
41
+ return {
42
+ degrees: Number(Math.abs(value).toFixed(precision)),
43
+ hemisphere: getHemisphere(value, axis)
44
+ };
45
+ };
17
46
  /**
18
47
  * Converts a coordinate value to decimal degrees format.
19
48
  *
20
- * @param num - The coordinate value to format.
49
+ * @param value - The coordinate value to format.
21
50
  * @param withOrdinal - Whether to use absolute value (when ordinal directions are shown separately).
22
51
  * @returns Formatted coordinate string with degree symbol and 6 decimal places.
23
52
  *
@@ -33,8 +62,8 @@ import { createFormatter } from "../internal/format.js";
33
62
  * // '122.419400°'
34
63
  * ```
35
64
  */
36
- const toDecimalDegrees = (num, withOrdinal) => {
37
- return `${(withOrdinal ? Math.abs(num) : num).toFixed(6)}°`;
65
+ const toDecimalDegrees = (value, withOrdinal) => {
66
+ return `${(withOrdinal ? Math.abs(value) : value).toFixed(DECIMAL_DEGREES_PRECISION)}°`;
38
67
  };
39
68
  /**
40
69
  * Formats latitude/longitude coordinates in decimal degrees notation.
@@ -58,5 +87,5 @@ const toDecimalDegrees = (num, withOrdinal) => {
58
87
  const formatDecimalDegrees = createFormatter(toDecimalDegrees);
59
88
 
60
89
  //#endregion
61
- export { formatDecimalDegrees };
90
+ export { DECIMAL_DEGREES_PRECISION, formatDecimalDegrees, toDecimalDegreesParts };
62
91
  //# sourceMappingURL=formatter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"formatter.js","names":[],"sources":["../../../../src/coordinates/latlon/decimal-degrees/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 decimal degrees format.\n *\n * @param num - The coordinate value to format.\n * @param withOrdinal - Whether to use absolute value (when ordinal directions are shown separately).\n * @returns Formatted coordinate string with degree symbol and 6 decimal places.\n *\n * @example\n * ```typescript\n * toDecimalDegrees(45.123456);\n * // '45.123456°'\n * ```\n *\n * @example\n * ```typescript\n * toDecimalDegrees(-122.4194, true);\n * // '122.419400°'\n * ```\n */\nconst toDecimalDegrees = (num: number, withOrdinal?: boolean): string => {\n const value = withOrdinal ? Math.abs(num) : num;\n return `${value.toFixed(6)}°`;\n};\n\n/**\n * Formats latitude/longitude coordinates in decimal degrees notation.\n *\n * @param coordinates - Tuple of [latitude, longitude] values.\n * @param config - Optional formatting configuration.\n * @returns Formatted coordinate string in decimal degrees format.\n *\n * @example\n * ```typescript\n * formatDecimalDegrees([37.7749, -122.4194]);\n * // '37.774900° N, 122.419400° W'\n * ```\n *\n * @example\n * ```typescript\n * formatDecimalDegrees([37.7749, -122.4194], { separator: ' / ' });\n * // '37.774900° N / 122.419400° W'\n * ```\n */\nexport const formatDecimalDegrees = createFormatter(toDecimalDegrees);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiCA,MAAM,oBAAoB,KAAa,gBAAkC;AAEvE,QAAO,IADO,cAAc,KAAK,IAAI,IAAI,GAAG,KAC5B,QAAQ,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;AAsB7B,MAAa,uBAAuB,gBAAgB,iBAAiB"}
1
+ {"version":3,"file":"formatter.js","names":[],"sources":["../../../../src/coordinates/latlon/decimal-degrees/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 decimal-degrees formatting. */\nexport const DECIMAL_DEGREES_PRECISION = 6;\n\n/**\n * Structured decimal-degrees parts for a single signed coordinate value.\n *\n * `degrees` is the non-negative magnitude rounded to the requested precision;\n * the signed value is recoverable from the axis and `hemisphere`.\n */\nexport type DecimalDegreesParts = {\n degrees: number;\n hemisphere: Hemisphere;\n};\n\n/**\n * Converts a single signed coordinate value into decimal-degrees parts.\n *\n * Returns the non-negative magnitude plus the hemisphere letter for the axis\n * (following geo's `>= 0` convention). Decimal degrees has no minutes/seconds\n * carry; the magnitude is simply rounded to the requested precision.\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 magnitude (default `6`).\n * @returns The `{ degrees, hemisphere }` parts object.\n *\n * @remarks pure function\n *\n * @example\n * ```typescript\n * toDecimalDegreesParts(-122.4194, 'lon');\n * // { degrees: 122.4194, hemisphere: 'W' }\n * ```\n */\nexport const toDecimalDegreesParts = (\n value: number,\n axis: Axis,\n precision: number = DECIMAL_DEGREES_PRECISION,\n): DecimalDegreesParts => {\n const degrees = Number(Math.abs(value).toFixed(precision));\n\n return {\n degrees,\n hemisphere: getHemisphere(value, axis),\n };\n};\n\n/**\n * Converts a coordinate value to decimal degrees format.\n *\n * @param value - The coordinate value to format.\n * @param withOrdinal - Whether to use absolute value (when ordinal directions are shown separately).\n * @returns Formatted coordinate string with degree symbol and 6 decimal places.\n *\n * @example\n * ```typescript\n * toDecimalDegrees(45.123456);\n * // '45.123456°'\n * ```\n *\n * @example\n * ```typescript\n * toDecimalDegrees(-122.4194, true);\n * // '122.419400°'\n * ```\n */\nconst toDecimalDegrees = (value: number, withOrdinal?: boolean): string => {\n const rendered = withOrdinal ? Math.abs(value) : value;\n\n return `${rendered.toFixed(DECIMAL_DEGREES_PRECISION)}°`;\n};\n\n/**\n * Formats latitude/longitude coordinates in decimal degrees notation.\n *\n * @param coordinates - Tuple of [latitude, longitude] values.\n * @param config - Optional formatting configuration.\n * @returns Formatted coordinate string in decimal degrees format.\n *\n * @example\n * ```typescript\n * formatDecimalDegrees([37.7749, -122.4194]);\n * // '37.774900° N, 122.419400° W'\n * ```\n *\n * @example\n * ```typescript\n * formatDecimalDegrees([37.7749, -122.4194], { separator: ' / ' });\n * // '37.774900° N / 122.419400° W'\n * ```\n */\nexport const formatDecimalDegrees = createFormatter(toDecimalDegrees);\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgBA,MAAa,4BAA4B;;;;;;;;;;;;;;;;;;;;;AAiCzC,MAAa,yBACX,OACA,MACA,YAAoB,8BACI;AAGxB,QAAO;EACL,SAHc,OAAO,KAAK,IAAI,MAAM,CAAC,QAAQ,UAAU,CAAC;EAIxD,YAAY,cAAc,OAAO,KAAK;EACvC;;;;;;;;;;;;;;;;;;;;;AAsBH,MAAM,oBAAoB,OAAe,gBAAkC;AAGzE,QAAO,IAFU,cAAc,KAAK,IAAI,MAAM,GAAG,OAE9B,QAAQ,0BAA0B,CAAC;;;;;;;;;;;;;;;;;;;;;AAsBxD,MAAa,uBAAuB,gBAAgB,iBAAiB"}
@@ -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 { parseDecimalDegrees } from "./parser.js";
16
18
 
17
19
  //#region src/coordinates/latlon/decimal-degrees/system.ts
@@ -53,8 +55,8 @@ const systemDecimalDegrees = {
53
55
  const [num, bear] = arg;
54
56
  return Number.parseFloat(num) * (SYMBOL_PATTERNS.NEGATIVE_BEARINGS.test(bear) ? -1 : 1);
55
57
  },
56
- toFormat(format, [left, right]) {
57
- return [left, right].map((num, index) => `${Math.abs(num)} ${BEARINGS[format][index][+(num < 0)]}`).join(` ${SYMBOLS.DIVIDER} `);
58
+ toFormat(format, coordinates) {
59
+ return formatCoordinateSystem(format, coordinates, toPlainDecimalString);
58
60
  }
59
61
  };
60
62
 
@@ -1 +1 @@
1
- {"version":3,"file":"system.js","names":["systemDecimalDegrees: CoordinateSystem"],"sources":["../../../../src/coordinates/latlon/decimal-degrees/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 { parseDecimalDegrees } from './parser';\nimport type { CoordinateSystem } from '../internal/coordinate-system';\n\n/**\n * Decimal Degrees coordinate system implementation.\n *\n * Provides parsing, conversion, and formatting for coordinates in decimal degrees notation.\n * Coordinates are expressed as decimal numbers with degree symbols (e.g., 37.7749° N).\n *\n * @property name - Human-readable name of the coordinate system.\n * @property parse - Parses decimal degrees coordinate strings.\n * @property toFloat - Converts parsed coordinate components to floating point numbers.\n * @property toFormat - Formats numeric coordinates back to decimal degrees string.\n *\n * @example\n * ```ts\n * // Parse a coordinate string\n * const [coords, errors] = systemDecimalDegrees.parse('37.7749° N / 122.4194° W', 'LATLON');\n * ```\n *\n * @example\n * ```ts\n * // Convert to float\n * const lat = systemDecimalDegrees.toFloat(['37.7749', 'N']);\n * // 37.7749\n * ```\n *\n * @example\n * ```ts\n * // Format to string\n * const formatted = systemDecimalDegrees.toFormat('LATLON', [37.7749, -122.4194]);\n * // '37.7749 N / 122.4194 W'\n * ```\n */\nexport const systemDecimalDegrees: CoordinateSystem = {\n name: 'Decimal Degrees',\n\n parse: parseDecimalDegrees,\n\n toFloat(arg) {\n const [num, bear] = arg as [string, Compass];\n\n return (\n Number.parseFloat(num) *\n (SYMBOL_PATTERNS.NEGATIVE_BEARINGS.test(bear) ? -1 : 1)\n );\n },\n\n toFormat(format: Format, [left, right]: [number, number]) {\n return [left, right]\n .map(\n (num, index) =>\n `${Math.abs(num)} ${BEARINGS[format][index as 0 | 1][+(num < 0)]}`,\n )\n .join(` ${SYMBOLS.DIVIDER} `);\n },\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDA,MAAaA,uBAAyC;CACpD,MAAM;CAEN,OAAO;CAEP,QAAQ,KAAK;EACX,MAAM,CAAC,KAAK,QAAQ;AAEpB,SACE,OAAO,WAAW,IAAI,IACrB,gBAAgB,kBAAkB,KAAK,KAAK,GAAG,KAAK;;CAIzD,SAAS,QAAgB,CAAC,MAAM,QAA0B;AACxD,SAAO,CAAC,MAAM,MAAM,CACjB,KACE,KAAK,UACJ,GAAG,KAAK,IAAI,IAAI,CAAC,GAAG,SAAS,QAAQ,OAAgB,EAAE,MAAM,MAChE,CACA,KAAK,IAAI,QAAQ,QAAQ,GAAG;;CAElC"}
1
+ {"version":3,"file":"system.js","names":["systemDecimalDegrees: CoordinateSystem"],"sources":["../../../../src/coordinates/latlon/decimal-degrees/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 { parseDecimalDegrees } from './parser';\nimport type { CoordinateSystem } from '../internal/coordinate-system';\n\n/**\n * Decimal Degrees coordinate system implementation.\n *\n * Provides parsing, conversion, and formatting for coordinates in decimal degrees notation.\n * Coordinates are expressed as decimal numbers with degree symbols (e.g., 37.7749° N).\n *\n * @property name - Human-readable name of the coordinate system.\n * @property parse - Parses decimal degrees coordinate strings.\n * @property toFloat - Converts parsed coordinate components to floating point numbers.\n * @property toFormat - Formats numeric coordinates back to decimal degrees string.\n *\n * @example\n * ```ts\n * // Parse a coordinate string\n * const [coords, errors] = systemDecimalDegrees.parse('37.7749° N / 122.4194° W', 'LATLON');\n * ```\n *\n * @example\n * ```ts\n * // Convert to float\n * const lat = systemDecimalDegrees.toFloat(['37.7749', 'N']);\n * // 37.7749\n * ```\n *\n * @example\n * ```ts\n * // Format to string\n * const formatted = systemDecimalDegrees.toFormat('LATLON', [37.7749, -122.4194]);\n * // '37.7749 N / 122.4194 W'\n * ```\n */\nexport const systemDecimalDegrees: CoordinateSystem = {\n name: 'Decimal Degrees',\n\n parse: parseDecimalDegrees,\n\n toFloat(arg) {\n const [num, bear] = arg as [string, Compass];\n\n return (\n Number.parseFloat(num) *\n (SYMBOL_PATTERNS.NEGATIVE_BEARINGS.test(bear) ? -1 : 1)\n );\n },\n\n toFormat(format: Format, coordinates: [number, number]) {\n return formatCoordinateSystem(format, coordinates, toPlainDecimalString);\n },\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,MAAaA,uBAAyC;CACpD,MAAM;CAEN,OAAO;CAEP,QAAQ,KAAK;EACX,MAAM,CAAC,KAAK,QAAQ;AAEpB,SACE,OAAO,WAAW,IAAI,IACrB,gBAAgB,kBAAkB,KAAK,KAAK,GAAG,KAAK;;CAIzD,SAAS,QAAgB,aAA+B;AACtD,SAAO,uBAAuB,QAAQ,aAAa,qBAAqB;;CAE3E"}
@@ -11,9 +11,49 @@
11
11
  */
12
12
 
13
13
  import { FormatOptions } from "../internal/format.js";
14
+ import { Axis, Hemisphere } from "../internal/ordinal.js";
14
15
 
15
16
  //#region src/coordinates/latlon/degrees-decimal-minutes/formatter.d.ts
16
-
17
+ /** Default number of decimal places for degrees-decimal-minutes formatting. */
18
+ declare const DDM_PRECISION = 4;
19
+ /**
20
+ * Structured degrees-decimal-minutes parts for a single signed coordinate value.
21
+ *
22
+ * `degrees` and `minutes` are non-negative; carry keeps `minutes` below `60`.
23
+ * The signed value is recoverable from the axis and `hemisphere`.
24
+ */
25
+ type DdmParts = {
26
+ degrees: number;
27
+ minutes: number;
28
+ hemisphere: Hemisphere;
29
+ };
30
+ /**
31
+ * Converts a single signed coordinate value into degrees-decimal-minutes parts.
32
+ *
33
+ * Applies the minutes carry (`60′ → +1°`) after rounding so `minutes` never
34
+ * reaches `60`, then attaches the hemisphere letter for the axis.
35
+ *
36
+ * This is the display path: it rounds to `precision` and carries. The
37
+ * lossless round-trip representation returned by `createCoordinate(...).ddm()`
38
+ * lives separately in `degrees-decimal-minutes/system.ts` (`toFormat`, via the
39
+ * shared `formatCoordinateSystem`), which keeps full precision and applies no
40
+ * carry so a value survives format → parse unchanged. The two are
41
+ * intentionally not shared — do not route one through the other.
42
+ *
43
+ * @param value - The signed coordinate value.
44
+ * @param axis - Whether the value is a latitude (`'lat'`) or longitude (`'lon'`).
45
+ * @param precision - Decimal places for the minutes (default `4`).
46
+ * @returns The `{ degrees, minutes, hemisphere }` parts object.
47
+ *
48
+ * @remarks pure function
49
+ *
50
+ * @example
51
+ * ```typescript
52
+ * toDdmParts(12.576, 'lat');
53
+ * // { degrees: 12, minutes: 34.56, hemisphere: 'N' }
54
+ * ```
55
+ */
56
+ declare const toDdmParts: (value: number, axis: Axis, precision?: number) => DdmParts;
17
57
  /**
18
58
  * Formats latitude/longitude coordinates in degrees decimal minutes notation.
19
59
  *
@@ -35,5 +75,5 @@ import { FormatOptions } from "../internal/format.js";
35
75
  */
36
76
  declare const formatDegreesDecimalMinutes: (coordinates: [number, number], config?: FormatOptions) => string;
37
77
  //#endregion
38
- export { formatDegreesDecimalMinutes };
78
+ export { DDM_PRECISION, DdmParts, formatDegreesDecimalMinutes, toDdmParts };
39
79
  //# sourceMappingURL=formatter.d.ts.map
@@ -11,13 +11,68 @@
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-decimal-minutes/formatter.ts
18
+ /** Default number of decimal places for degrees-decimal-minutes formatting. */
19
+ const DDM_PRECISION = 4;
20
+ /**
21
+ * Splits a non-negative magnitude into whole degrees and decimal minutes,
22
+ * rounding the minutes to `precision` and carrying `60′ → +1°` so the output
23
+ * stays a valid coordinate. Shared core of {@link toDdmParts} and the display
24
+ * string formatter.
25
+ *
26
+ * @param magnitude - Non-negative coordinate magnitude in degrees.
27
+ * @param precision - Decimal places for the minutes.
28
+ * @returns The `{ degrees, minutes }` pair.
29
+ *
30
+ * @remarks pure function
31
+ */
32
+ const toDdmMagnitude = (magnitude, precision) => {
33
+ let degrees = Math.floor(magnitude);
34
+ let minutes = Number(((magnitude - degrees) * 60).toFixed(precision));
35
+ degrees += Math.floor(minutes / 60);
36
+ minutes %= 60;
37
+ return {
38
+ degrees,
39
+ minutes
40
+ };
41
+ };
42
+ /**
43
+ * Converts a single signed coordinate value into degrees-decimal-minutes parts.
44
+ *
45
+ * Applies the minutes carry (`60′ → +1°`) after rounding so `minutes` never
46
+ * reaches `60`, then attaches the hemisphere letter for the axis.
47
+ *
48
+ * This is the display path: it rounds to `precision` and carries. The
49
+ * lossless round-trip representation returned by `createCoordinate(...).ddm()`
50
+ * lives separately in `degrees-decimal-minutes/system.ts` (`toFormat`, via the
51
+ * shared `formatCoordinateSystem`), which keeps full precision and applies no
52
+ * carry so a value survives format → parse unchanged. The two are
53
+ * intentionally not shared — do not route one through the other.
54
+ *
55
+ * @param value - The signed coordinate value.
56
+ * @param axis - Whether the value is a latitude (`'lat'`) or longitude (`'lon'`).
57
+ * @param precision - Decimal places for the minutes (default `4`).
58
+ * @returns The `{ degrees, minutes, hemisphere }` parts object.
59
+ *
60
+ * @remarks pure function
61
+ *
62
+ * @example
63
+ * ```typescript
64
+ * toDdmParts(12.576, 'lat');
65
+ * // { degrees: 12, minutes: 34.56, hemisphere: 'N' }
66
+ * ```
67
+ */
68
+ const toDdmParts = (value, axis, precision = DDM_PRECISION) => ({
69
+ ...toDdmMagnitude(Math.abs(value), precision),
70
+ hemisphere: getHemisphere(value, axis)
71
+ });
17
72
  /**
18
73
  * Converts a coordinate value to degrees decimal minutes format.
19
74
  *
20
- * @param num - The coordinate value to format.
75
+ * @param value - The coordinate value to format.
21
76
  * @returns Formatted coordinate string with degrees and decimal minutes (e.g., "45° 30.1234'").
22
77
  *
23
78
  * @example
@@ -32,12 +87,9 @@ import { createFormatter } from "../internal/format.js";
32
87
  * // '122° 25.1640''
33
88
  * ```
34
89
  */
35
- const toDegreesDecimalMinutes = (num) => {
36
- let degrees = Math.floor(Math.abs(num));
37
- let minutes = Number(((Math.abs(num) - degrees) * 60).toFixed(4));
38
- degrees += Math.floor(minutes / 60);
39
- minutes %= 60;
40
- return `${degrees}° ${minutes.toFixed(4)}'`;
90
+ const toDegreesDecimalMinutes = (value) => {
91
+ const { degrees, minutes } = toDdmMagnitude(Math.abs(value), DDM_PRECISION);
92
+ return `${degrees ${minutes.toFixed(DDM_PRECISION)}'`;
41
93
  };
42
94
  /**
43
95
  * Formats latitude/longitude coordinates in degrees decimal minutes notation.
@@ -61,5 +113,5 @@ const toDegreesDecimalMinutes = (num) => {
61
113
  const formatDegreesDecimalMinutes = createFormatter(toDegreesDecimalMinutes);
62
114
 
63
115
  //#endregion
64
- export { formatDegreesDecimalMinutes };
116
+ export { DDM_PRECISION, formatDegreesDecimalMinutes, toDdmParts };
65
117
  //# sourceMappingURL=formatter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"formatter.js","names":[],"sources":["../../../../src/coordinates/latlon/degrees-decimal-minutes/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 decimal minutes format.\n *\n * @param num - The coordinate value to format.\n * @returns Formatted coordinate string with degrees and decimal minutes (e.g., \"45° 30.1234'\").\n *\n * @example\n * ```typescript\n * toDegreesDecimalMinutes(45.5);\n * // '45° 30.0000''\n * ```\n *\n * @example\n * ```typescript\n * toDegreesDecimalMinutes(-122.4194);\n * // '122° 25.1640''\n * ```\n */\nconst toDegreesDecimalMinutes = (num: number): string => {\n let degrees = Math.floor(Math.abs(num));\n let minutes = Number(((Math.abs(num) - degrees) * 60).toFixed(4));\n\n // Rounding can produce 60 minutes (e.g. 40.9999995 -> 40° 60.0000');\n // carry into degrees so the output stays a valid coordinate.\n degrees += Math.floor(minutes / 60);\n minutes %= 60;\n\n return `${degrees}° ${minutes.toFixed(4)}'`;\n};\n\n/**\n * Formats latitude/longitude coordinates in degrees decimal minutes notation.\n *\n * @param coordinates - Tuple of [latitude, longitude] values.\n * @param config - Optional formatting configuration.\n * @returns Formatted coordinate string in degrees decimal minutes format.\n *\n * @example\n * ```typescript\n * formatDegreesDecimalMinutes([37.7749, -122.4194]);\n * // '37° 46.4940' N, 122° 25.1640' W'\n * ```\n *\n * @example\n * ```typescript\n * formatDegreesDecimalMinutes([37.7749, -122.4194], { separator: ' / ' });\n * // '37° 46.4940' N / 122° 25.1640' W'\n * ```\n */\nexport const formatDegreesDecimalMinutes = createFormatter(\n toDegreesDecimalMinutes,\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,MAAM,2BAA2B,QAAwB;CACvD,IAAI,UAAU,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC;CACvC,IAAI,UAAU,SAAS,KAAK,IAAI,IAAI,GAAG,WAAW,IAAI,QAAQ,EAAE,CAAC;AAIjE,YAAW,KAAK,MAAM,UAAU,GAAG;AACnC,YAAW;AAEX,QAAO,GAAG,QAAQ,IAAI,QAAQ,QAAQ,EAAE,CAAC;;;;;;;;;;;;;;;;;;;;;AAsB3C,MAAa,8BAA8B,gBACzC,wBACD"}
1
+ {"version":3,"file":"formatter.js","names":[],"sources":["../../../../src/coordinates/latlon/degrees-decimal-minutes/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-decimal-minutes formatting. */\nexport const DDM_PRECISION = 4;\n\n/**\n * Structured degrees-decimal-minutes parts for a single signed coordinate value.\n *\n * `degrees` and `minutes` are non-negative; carry keeps `minutes` below `60`.\n * The signed value is recoverable from the axis and `hemisphere`.\n */\nexport type DdmParts = {\n degrees: number;\n minutes: number;\n hemisphere: Hemisphere;\n};\n\n/**\n * Splits a non-negative magnitude into whole degrees and decimal minutes,\n * rounding the minutes to `precision` and carrying `60′ → +1°` so the output\n * stays a valid coordinate. Shared core of {@link toDdmParts} and the display\n * string formatter.\n *\n * @param magnitude - Non-negative coordinate magnitude in degrees.\n * @param precision - Decimal places for the minutes.\n * @returns The `{ degrees, minutes }` pair.\n *\n * @remarks pure function\n */\nconst toDdmMagnitude = (\n magnitude: number,\n precision: number,\n): Pick<DdmParts, 'degrees' | 'minutes'> => {\n let degrees = Math.floor(magnitude);\n let minutes = Number(((magnitude - degrees) * 60).toFixed(precision));\n\n // Rounding can produce 60 minutes (e.g. 40.9999995 -> 40° 60.0000');\n // carry into degrees so the output stays a valid coordinate.\n degrees += Math.floor(minutes / 60);\n minutes %= 60;\n\n return { degrees, minutes };\n};\n\n/**\n * Converts a single signed coordinate value into degrees-decimal-minutes parts.\n *\n * Applies the minutes carry (`60′ → +1°`) after rounding so `minutes` never\n * reaches `60`, then attaches the hemisphere 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(...).ddm()`\n * lives separately in `degrees-decimal-minutes/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 minutes (default `4`).\n * @returns The `{ degrees, minutes, hemisphere }` parts object.\n *\n * @remarks pure function\n *\n * @example\n * ```typescript\n * toDdmParts(12.576, 'lat');\n * // { degrees: 12, minutes: 34.56, hemisphere: 'N' }\n * ```\n */\nexport const toDdmParts = (\n value: number,\n axis: Axis,\n precision: number = DDM_PRECISION,\n): DdmParts => ({\n ...toDdmMagnitude(Math.abs(value), precision),\n hemisphere: getHemisphere(value, axis),\n});\n\n/**\n * Converts a coordinate value to degrees decimal minutes format.\n *\n * @param value - The coordinate value to format.\n * @returns Formatted coordinate string with degrees and decimal minutes (e.g., \"45° 30.1234'\").\n *\n * @example\n * ```typescript\n * toDegreesDecimalMinutes(45.5);\n * // '45° 30.0000''\n * ```\n *\n * @example\n * ```typescript\n * toDegreesDecimalMinutes(-122.4194);\n * // '122° 25.1640''\n * ```\n */\nconst toDegreesDecimalMinutes = (value: number): string => {\n const { degrees, minutes } = toDdmMagnitude(Math.abs(value), DDM_PRECISION);\n\n return `${degrees}° ${minutes.toFixed(DDM_PRECISION)}'`;\n};\n\n/**\n * Formats latitude/longitude coordinates in degrees decimal minutes notation.\n *\n * @param coordinates - Tuple of [latitude, longitude] values.\n * @param config - Optional formatting configuration.\n * @returns Formatted coordinate string in degrees decimal minutes format.\n *\n * @example\n * ```typescript\n * formatDegreesDecimalMinutes([37.7749, -122.4194]);\n * // '37° 46.4940' N, 122° 25.1640' W'\n * ```\n *\n * @example\n * ```typescript\n * formatDegreesDecimalMinutes([37.7749, -122.4194], { separator: ' / ' });\n * // '37° 46.4940' N / 122° 25.1640' W'\n * ```\n */\nexport const formatDegreesDecimalMinutes = createFormatter(\n toDegreesDecimalMinutes,\n);\n"],"mappings":";;;;;;;;;;;;;;;;;;AAgBA,MAAa,gBAAgB;;;;;;;;;;;;;AA0B7B,MAAM,kBACJ,WACA,cAC0C;CAC1C,IAAI,UAAU,KAAK,MAAM,UAAU;CACnC,IAAI,UAAU,SAAS,YAAY,WAAW,IAAI,QAAQ,UAAU,CAAC;AAIrE,YAAW,KAAK,MAAM,UAAU,GAAG;AACnC,YAAW;AAEX,QAAO;EAAE;EAAS;EAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6B7B,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,YAAY,eAAe,KAAK,IAAI,MAAM,EAAE,cAAc;AAE3E,QAAO,GAAG,QAAQ,IAAI,QAAQ,QAAQ,cAAc,CAAC;;;;;;;;;;;;;;;;;;;;;AAsBvD,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 { parseDegreesDecimalMinutes } from "./parser.js";
16
18
 
17
19
  //#region src/coordinates/latlon/degrees-decimal-minutes/system.ts
@@ -53,12 +55,11 @@ const systemDegreesDecimalMinutes = {
53
55
  const [degrees, minutes, bear] = arg;
54
56
  return Number.parseFloat(((Number.parseFloat(degrees) + Number.parseFloat(minutes) / 60) * (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
- return `${deg} ${Number.parseFloat(((abs - deg) * 60).toFixed(10))} ${BEARINGS[format][index][+(num < 0)]}`;
61
- }).join(` ${SYMBOLS.DIVIDER} `);
61
+ return `${deg} ${toPlainDecimalString(Number.parseFloat(((abs - deg) * 60).toFixed(10)))}`;
62
+ });
62
63
  }
63
64
  };
64
65
 
@@ -1 +1 @@
1
- {"version":3,"file":"system.js","names":["systemDegreesDecimalMinutes: CoordinateSystem"],"sources":["../../../../src/coordinates/latlon/degrees-decimal-minutes/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 { parseDegreesDecimalMinutes } from './parser';\nimport type { CoordinateSystem } from '../internal/coordinate-system';\n\n/**\n * Degrees Decimal Minutes coordinate system implementation.\n *\n * Provides parsing, conversion, and formatting for coordinates in degrees decimal minutes notation.\n * Coordinates are expressed as integer degrees and decimal minutes (e.g., 37° 46.4940' N).\n *\n * @property name - Human-readable name of the coordinate system.\n * @property parse - Parses degrees decimal minutes coordinate strings.\n * @property toFloat - Converts parsed coordinate components to floating point numbers.\n * @property toFormat - Formats numeric coordinates back to degrees decimal minutes string.\n *\n * @example\n * ```typescript\n * // Parse a coordinate string\n * const [coords, errors] = systemDegreesDecimalMinutes.parse('37° 46.4940' N / 122° 25.1640' W', 'LATLON');\n * ```\n *\n * @example\n * ```typescript\n * // Convert to float\n * const lat = systemDegreesDecimalMinutes.toFloat(['37', '46.4940', 'N']);\n * // 37.7749\n * ```\n *\n * @example\n * ```typescript\n * // Format to string\n * const formatted = systemDegreesDecimalMinutes.toFormat('LATLON', [37.7749, -122.4194]);\n * // '37 46.494 N / 122 25.164 W'\n * ```\n */\nexport const systemDegreesDecimalMinutes: CoordinateSystem = {\n name: 'Degrees Decimal Minutes',\n\n parse: parseDegreesDecimalMinutes,\n\n toFloat(arg) {\n const [degrees, minutes, bear] = arg as [string, string, Compass];\n\n return Number.parseFloat(\n (\n (Number.parseFloat(degrees) + Number.parseFloat(minutes) / 60) *\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 min = Number.parseFloat(((abs - deg) * 60).toFixed(10));\n\n return `${deg} ${min} ${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,QAAQ;AAEjC,SAAO,OAAO,aAET,OAAO,WAAW,QAAQ,GAAG,OAAO,WAAW,QAAQ,GAAG,OAC1D,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;AAG3B,UAAO,GAAG,IAAI,GAFF,OAAO,aAAa,MAAM,OAAO,IAAI,QAAQ,GAAG,CAAC,CAExC,GAAG,SAAS,QAAQ,OAAgB,EAAE,MAAM;IACjE,CACD,KAAK,IAAI,QAAQ,QAAQ,GAAG;;CAElC"}
1
+ {"version":3,"file":"system.js","names":["systemDegreesDecimalMinutes: CoordinateSystem"],"sources":["../../../../src/coordinates/latlon/degrees-decimal-minutes/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 { parseDegreesDecimalMinutes } from './parser';\nimport type { CoordinateSystem } from '../internal/coordinate-system';\n\n/**\n * Degrees Decimal Minutes coordinate system implementation.\n *\n * Provides parsing, conversion, and formatting for coordinates in degrees decimal minutes notation.\n * Coordinates are expressed as integer degrees and decimal minutes (e.g., 37° 46.4940' N).\n *\n * @property name - Human-readable name of the coordinate system.\n * @property parse - Parses degrees decimal minutes coordinate strings.\n * @property toFloat - Converts parsed coordinate components to floating point numbers.\n * @property toFormat - Formats numeric coordinates back to degrees decimal minutes string.\n *\n * @example\n * ```typescript\n * // Parse a coordinate string\n * const [coords, errors] = systemDegreesDecimalMinutes.parse('37° 46.4940' N / 122° 25.1640' W', 'LATLON');\n * ```\n *\n * @example\n * ```typescript\n * // Convert to float\n * const lat = systemDegreesDecimalMinutes.toFloat(['37', '46.4940', 'N']);\n * // 37.7749\n * ```\n *\n * @example\n * ```typescript\n * // Format to string\n * const formatted = systemDegreesDecimalMinutes.toFormat('LATLON', [37.7749, -122.4194]);\n * // '37 46.494 N / 122 25.164 W'\n * ```\n */\nexport const systemDegreesDecimalMinutes: CoordinateSystem = {\n name: 'Degrees Decimal Minutes',\n\n parse: parseDegreesDecimalMinutes,\n\n toFloat(arg) {\n const [degrees, minutes, bear] = arg as [string, string, Compass];\n\n return Number.parseFloat(\n (\n (Number.parseFloat(degrees) + Number.parseFloat(minutes) / 60) *\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 min = Number.parseFloat(((abs - deg) * 60).toFixed(10));\n\n return `${deg} ${toPlainDecimalString(min)}`;\n });\n },\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDA,MAAaA,8BAAgD;CAC3D,MAAM;CAEN,OAAO;CAEP,QAAQ,KAAK;EACX,MAAM,CAAC,SAAS,SAAS,QAAQ;AAEjC,SAAO,OAAO,aAET,OAAO,WAAW,QAAQ,GAAG,OAAO,WAAW,QAAQ,GAAG,OAC1D,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;AAG3B,UAAO,GAAG,IAAI,GAAG,qBAFL,OAAO,aAAa,MAAM,OAAO,IAAI,QAAQ,GAAG,CAAC,CAEnB;IAC1C;;CAEL"}
@@ -11,9 +11,52 @@
11
11
  */
12
12
 
13
13
  import { FormatOptions } from "../internal/format.js";
14
+ import { Axis, Hemisphere } from "../internal/ordinal.js";
14
15
 
15
16
  //#region src/coordinates/latlon/degrees-minutes-seconds/formatter.d.ts
16
-
17
+ /** Default number of decimal places for degrees-minutes-seconds formatting. */
18
+ declare const DMS_PRECISION = 2;
19
+ /**
20
+ * Structured degrees-minutes-seconds parts for a single signed coordinate value.
21
+ *
22
+ * `degrees`, `minutes`, and `seconds` are non-negative; carry keeps `seconds`
23
+ * and `minutes` below `60`. The signed value is recoverable from the axis and
24
+ * `hemisphere`.
25
+ */
26
+ type DmsParts = {
27
+ degrees: number;
28
+ minutes: number;
29
+ seconds: number;
30
+ hemisphere: Hemisphere;
31
+ };
32
+ /**
33
+ * Converts a single signed coordinate value into degrees-minutes-seconds parts.
34
+ *
35
+ * Applies the seconds/minutes carry (`60″ → +1′`, `60′ → +1°`) after rounding
36
+ * so `seconds` and `minutes` never reach `60`, then attaches the hemisphere
37
+ * letter for the axis.
38
+ *
39
+ * This is the display path: it rounds to `precision` and carries. The
40
+ * lossless round-trip representation returned by `createCoordinate(...).dms()`
41
+ * lives separately in `degrees-minutes-seconds/system.ts` (`toFormat`, via the
42
+ * shared `formatCoordinateSystem`), which keeps full precision and applies no
43
+ * carry so a value survives format → parse unchanged. The two are
44
+ * intentionally not shared — do not route one through the other.
45
+ *
46
+ * @param value - The signed coordinate value.
47
+ * @param axis - Whether the value is a latitude (`'lat'`) or longitude (`'lon'`).
48
+ * @param precision - Decimal places for the seconds (default `2`).
49
+ * @returns The `{ degrees, minutes, seconds, hemisphere }` parts object.
50
+ *
51
+ * @remarks pure function
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * toDmsParts(-77.0369, 'lon');
56
+ * // { degrees: 77, minutes: 2, seconds: 12.84, hemisphere: 'W' }
57
+ * ```
58
+ */
59
+ declare const toDmsParts: (value: number, axis: Axis, precision?: number) => DmsParts;
17
60
  /**
18
61
  * Formats latitude/longitude coordinates in degrees minutes seconds notation.
19
62
  *
@@ -35,5 +78,5 @@ import { FormatOptions } from "../internal/format.js";
35
78
  */
36
79
  declare const formatDegreesMinutesSeconds: (coordinates: [number, number], config?: FormatOptions) => string;
37
80
  //#endregion
38
- export { formatDegreesMinutesSeconds };
81
+ export { DMS_PRECISION, DmsParts, formatDegreesMinutesSeconds, toDmsParts };
39
82
  //# sourceMappingURL=formatter.d.ts.map