ConvertCoordinates.mjs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655
  1. /**
  2. * Co-ordinate conversion resources.
  3. *
  4. * @author j433866 [j433866@gmail.com]
  5. * @copyright Crown Copyright 2019
  6. * @license Apache-2.0
  7. */
  8. import geohash from "ngeohash";
  9. import geodesy from "geodesy";
  10. import OperationError from "../errors/OperationError";
  11. /**
  12. * Co-ordinate formats
  13. */
  14. export const FORMATS = [
  15. "Degrees Minutes Seconds",
  16. "Degrees Decimal Minutes",
  17. "Decimal Degrees",
  18. "Geohash",
  19. "Military Grid Reference System",
  20. "Ordnance Survey National Grid",
  21. "Universal Transverse Mercator"
  22. ];
  23. /**
  24. * Formats that should be passed to the conversion module as-is
  25. */
  26. const NO_CHANGE = [
  27. "Geohash",
  28. "Military Grid Reference System",
  29. "Ordnance Survey National Grid",
  30. "Universal Transverse Mercator",
  31. ];
  32. /**
  33. * Convert a given latitude and longitude into a different format.
  34. *
  35. * @param {string} input - Input string to be converted
  36. * @param {string} inFormat - Format of the input coordinates
  37. * @param {string} inDelim - The delimiter splitting the lat/long of the input
  38. * @param {string} outFormat - Format to convert to
  39. * @param {string} outDelim - The delimiter to separate the output with
  40. * @param {string} includeDir - Whether or not to include the compass direction in the output
  41. * @param {number} precision - Precision of the result
  42. * @returns {string} A formatted string of the converted co-ordinates
  43. */
  44. export function convertCoordinates (input, inFormat, inDelim, outFormat, outDelim, includeDir, precision) {
  45. let isPair = false,
  46. split,
  47. latlon,
  48. convLat,
  49. convLon,
  50. conv,
  51. hash,
  52. utm,
  53. mgrs,
  54. osng,
  55. splitLat,
  56. splitLong,
  57. lat,
  58. lon;
  59. // Can't have a precision less than 0!
  60. if (precision < 0) {
  61. precision = 0;
  62. }
  63. if (inDelim === "Auto") {
  64. // Try to detect a delimiter in the input.
  65. inDelim = findDelim(input);
  66. if (inDelim === null) {
  67. throw new OperationError("Unable to detect the input delimiter automatically.");
  68. }
  69. } else if (!inDelim.includes("Direction")) {
  70. // Convert the delimiter argument value to the actual character
  71. inDelim = realDelim(inDelim);
  72. }
  73. if (inFormat === "Auto") {
  74. // Try to detect the format of the input data
  75. inFormat = findFormat(input, inDelim);
  76. if (inFormat === null) {
  77. throw new OperationError("Unable to detect the input format automatically.");
  78. }
  79. }
  80. // Convert the output delimiter argument to the real character
  81. outDelim = realDelim(outDelim);
  82. if (!NO_CHANGE.includes(inFormat)) {
  83. if (inDelim.includes("Direction")) {
  84. // Split on directions
  85. split = input.split(/[NnEeSsWw]/g);
  86. if (split[0] === "") {
  87. // Remove first element if direction preceding
  88. split = split.slice(1);
  89. }
  90. } else {
  91. split = input.split(inDelim);
  92. }
  93. // Replace any co-ordinate symbols with spaces so we can split on them later
  94. for (let i = 0; i < split.length; i++) {
  95. split[i] = split[i].replace(/[°˝´'"]/g, " ");
  96. }
  97. if (split.length > 1) {
  98. isPair = true;
  99. }
  100. } else {
  101. // Remove any delimiters from the input
  102. input = input.replace(inDelim, "");
  103. isPair = true;
  104. }
  105. // Conversions from the input format into a geodesy latlon object
  106. switch (inFormat) {
  107. case "Geohash":
  108. hash = geohash.decode(input.replace(/[^A-Za-z0-9]/g, ""));
  109. latlon = new geodesy.LatLonEllipsoidal(hash.latitude, hash.longitude);
  110. break;
  111. case "Military Grid Reference System":
  112. utm = geodesy.Mgrs.parse(input.replace(/[^A-Za-z0-9]/g, "")).toUtm();
  113. latlon = utm.toLatLonE();
  114. break;
  115. case "Ordnance Survey National Grid":
  116. osng = geodesy.OsGridRef.parse(input.replace(/[^A-Za-z0-9]/g, ""));
  117. latlon = geodesy.OsGridRef.osGridToLatLon(osng);
  118. break;
  119. case "Universal Transverse Mercator":
  120. // Geodesy needs a space between the first 2 digits and the next letter
  121. if (/^[\d]{2}[A-Za-z]/.test(input)) {
  122. input = input.slice(0, 2) + " " + input.slice(2);
  123. }
  124. utm = geodesy.Utm.parse(input);
  125. latlon = utm.toLatLonE();
  126. break;
  127. case "Degrees Minutes Seconds":
  128. if (isPair) {
  129. // Split up the lat/long into degrees / minutes / seconds values
  130. splitLat = splitInput(split[0]);
  131. splitLong = splitInput(split[1]);
  132. if (splitLat.length >= 3 && splitLong.length >= 3) {
  133. lat = convDMSToDD(splitLat[0], splitLat[1], splitLat[2], 10);
  134. lon = convDMSToDD(splitLong[0], splitLong[1], splitLong[2], 10);
  135. latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lon.degrees);
  136. } else {
  137. throw new OperationError("Invalid co-ordinate format for Degrees Minutes Seconds");
  138. }
  139. } else {
  140. // Not a pair, so only try to convert one set of co-ordinates
  141. splitLat = splitInput(split[0]);
  142. if (splitLat.length >= 3) {
  143. lat = convDMSToDD(splitLat[0], splitLat[1], splitLat[2]);
  144. latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lat.degrees);
  145. } else {
  146. throw new OperationError("Invalid co-ordinate format for Degrees Minutes Seconds");
  147. }
  148. }
  149. break;
  150. case "Degrees Decimal Minutes":
  151. if (isPair) {
  152. splitLat = splitInput(split[0]);
  153. splitLong = splitInput(split[1]);
  154. if (splitLat.length !== 2 || splitLong.length !== 2) {
  155. throw new OperationError("Invalid co-ordinate format for Degrees Decimal Minutes.");
  156. }
  157. // Convert to decimal degrees, and then convert to a geodesy object
  158. lat = convDDMToDD(splitLat[0], splitLat[1], 10);
  159. lon = convDDMToDD(splitLong[0], splitLong[1], 10);
  160. latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lon.degrees);
  161. } else {
  162. // Not a pair, so only try to convert one set of co-ordinates
  163. splitLat = splitInput(input);
  164. if (splitLat.length !== 2) {
  165. throw new OperationError("Invalid co-ordinate format for Degrees Decimal Minutes.");
  166. }
  167. lat = convDDMToDD(splitLat[0], splitLat[1], 10);
  168. latlon = new geodesy.LatLonEllipsoidal(lat.degrees, lat.degrees);
  169. }
  170. break;
  171. case "Decimal Degrees":
  172. if (isPair) {
  173. splitLat = splitInput(split[0]);
  174. splitLong = splitInput(split[1]);
  175. if (splitLat.length !== 1 || splitLong.length !== 1) {
  176. throw new OperationError("Invalid co-ordinate format for Decimal Degrees.");
  177. }
  178. latlon = new geodesy.LatLonEllipsoidal(splitLat[0], splitLong[0]);
  179. } else {
  180. // Not a pair, so only try to convert one set of co-ordinates
  181. splitLat = splitInput(split[0]);
  182. if (splitLat.length !== 1) {
  183. throw new OperationError("Invalid co-ordinate format for Decimal Degrees.");
  184. }
  185. latlon = new geodesy.LatLonEllipsoidal(splitLat[0], splitLat[0]);
  186. }
  187. break;
  188. default:
  189. throw new OperationError(`Unknown input format '${inFormat}'`);
  190. }
  191. // Everything is now a geodesy latlon object
  192. // These store the latitude and longitude as decimal
  193. if (inFormat.includes("Degrees")) {
  194. // If the input string contains directions, we need to check if they're S or W.
  195. // If either of the directions are, we should make the decimal value negative
  196. const dirs = input.toUpperCase().match(/[NESW]/g);
  197. if (dirs && dirs.length >= 1) {
  198. // Make positive lat/lon values with S/W directions into negative values
  199. if (dirs[0] === "S" || dirs[0] === "W" && latlon.lat > 0) {
  200. latlon.lat = -latlon.lat;
  201. }
  202. if (dirs.length >= 2) {
  203. if (dirs[1] === "S" || dirs[1] === "W" && latlon.lon > 0) {
  204. latlon.lon = -latlon.lon;
  205. }
  206. }
  207. }
  208. }
  209. // Try to find the compass directions of the lat and long
  210. const [latDir, longDir] = findDirs(latlon.lat + "," + latlon.lon, ",");
  211. // Output conversions for each output format
  212. switch (outFormat) {
  213. case "Decimal Degrees":
  214. // We could use the built in latlon.toString(),
  215. // but this makes adjusting the output harder
  216. lat = convDDToDD(latlon.lat, precision);
  217. lon = convDDToDD(latlon.lon, precision);
  218. convLat = lat.string;
  219. convLon = lon.string;
  220. break;
  221. case "Degrees Decimal Minutes":
  222. lat = convDDToDDM(latlon.lat, precision);
  223. lon = convDDToDDM(latlon.lon, precision);
  224. convLat = lat.string;
  225. convLon = lon.string;
  226. break;
  227. case "Degrees Minutes Seconds":
  228. lat = convDDToDMS(latlon.lat, precision);
  229. lon = convDDToDMS(latlon.lon, precision);
  230. convLat = lat.string;
  231. convLon = lon.string;
  232. break;
  233. case "Geohash":
  234. convLat = geohash.encode(latlon.lat, latlon.lon, precision);
  235. break;
  236. case "Military Grid Reference System":
  237. utm = latlon.toUtm();
  238. mgrs = utm.toMgrs();
  239. // MGRS wants a precision that's an even number between 2 and 10
  240. if (precision % 2 !== 0) {
  241. precision = precision + 1;
  242. }
  243. if (precision > 10) {
  244. precision = 10;
  245. }
  246. convLat = mgrs.toString(precision);
  247. break;
  248. case "Ordnance Survey National Grid":
  249. osng = geodesy.OsGridRef.latLonToOsGrid(latlon);
  250. if (osng.toString() === "") {
  251. throw new OperationError("Could not convert co-ordinates to OS National Grid. Are the co-ordinates in range?");
  252. }
  253. // OSNG wants a precision that's an even number between 2 and 10
  254. if (precision % 2 !== 0) {
  255. precision = precision + 1;
  256. }
  257. if (precision > 10) {
  258. precision = 10;
  259. }
  260. convLat = osng.toString(precision);
  261. break;
  262. case "Universal Transverse Mercator":
  263. utm = latlon.toUtm();
  264. convLat = utm.toString(precision);
  265. break;
  266. }
  267. if (convLat === undefined) {
  268. throw new OperationError("Error converting co-ordinates.");
  269. }
  270. if (outFormat.includes("Degrees")) {
  271. // Format DD/DDM/DMS for output
  272. // If we're outputting a compass direction, remove the negative sign
  273. if (latDir === "S" && includeDir !== "None") {
  274. convLat = convLat.replace("-", "");
  275. }
  276. if (longDir === "W" && includeDir !== "None") {
  277. convLon = convLon.replace("-", "");
  278. }
  279. let outConv = "";
  280. if (includeDir === "Before") {
  281. outConv += latDir + " ";
  282. }
  283. outConv += convLat;
  284. if (includeDir === "After") {
  285. outConv += " " + latDir;
  286. }
  287. outConv += outDelim;
  288. if (isPair) {
  289. if (includeDir === "Before") {
  290. outConv += longDir + " ";
  291. }
  292. outConv += convLon;
  293. if (includeDir === "After") {
  294. outConv += " " + longDir;
  295. }
  296. outConv += outDelim;
  297. }
  298. conv = outConv;
  299. } else {
  300. conv = convLat + outDelim;
  301. }
  302. return conv;
  303. }
  304. /**
  305. * Split up the input using a space or degrees signs, and sanitise the result
  306. *
  307. * @param {string} input - The input data to be split
  308. * @returns {number[]} An array of the different items in the string, stored as floats
  309. */
  310. function splitInput (input){
  311. const split = [];
  312. input.split(/\s+/).forEach(item => {
  313. // Remove any character that isn't a digit, decimal point or negative sign
  314. item = item.replace(/[^0-9.-]/g, "");
  315. if (item.length > 0){
  316. // Turn the item into a float
  317. split.push(parseFloat(item));
  318. }
  319. });
  320. return split;
  321. }
  322. /**
  323. * Convert Degrees Minutes Seconds to Decimal Degrees
  324. *
  325. * @param {number} degrees - The degrees of the input co-ordinates
  326. * @param {number} minutes - The minutes of the input co-ordinates
  327. * @param {number} seconds - The seconds of the input co-ordinates
  328. * @param {number} precision - The precision the result should be rounded to
  329. * @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string)
  330. */
  331. function convDMSToDD (degrees, minutes, seconds, precision){
  332. const absDegrees = Math.abs(degrees);
  333. let conv = absDegrees + (minutes / 60) + (seconds / 3600);
  334. let outString = round(conv, precision) + "°";
  335. if (isNegativeZero(degrees) || degrees < 0) {
  336. conv = -conv;
  337. outString = "-" + outString;
  338. }
  339. return {
  340. "degrees": conv,
  341. "string": outString
  342. };
  343. }
  344. /**
  345. * Convert Decimal Degrees Minutes to Decimal Degrees
  346. *
  347. * @param {number} degrees - The input degrees to be converted
  348. * @param {number} minutes - The input minutes to be converted
  349. * @param {number} precision - The precision which the result should be rounded to
  350. * @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string)
  351. */
  352. function convDDMToDD (degrees, minutes, precision) {
  353. const absDegrees = Math.abs(degrees);
  354. let conv = absDegrees + minutes / 60;
  355. let outString = round(conv, precision) + "°";
  356. if (isNegativeZero(degrees) || degrees < 0) {
  357. conv = -conv;
  358. outString = "-" + outString;
  359. }
  360. return {
  361. "degrees": conv,
  362. "string": outString
  363. };
  364. }
  365. /**
  366. * Convert Decimal Degrees to Decimal Degrees
  367. *
  368. * Doesn't affect the input, just puts it into an object
  369. * @param {number} degrees - The input degrees to be converted
  370. * @param {number} precision - The precision which the result should be rounded to
  371. * @returns {{string: string, degrees: number}} An object containing the raw converted value (obj.degrees), and a formatted string version (obj.string)
  372. */
  373. function convDDToDD (degrees, precision) {
  374. return {
  375. "degrees": degrees,
  376. "string": round(degrees, precision) + "°"
  377. };
  378. }
  379. /**
  380. * Convert Decimal Degrees to Degrees Minutes Seconds
  381. *
  382. * @param {number} decDegrees - The input data to be converted
  383. * @param {number} precision - The precision which the result should be rounded to
  384. * @returns {{string: string, degrees: number, minutes: number, seconds: number}} An object containing the raw converted value as separate numbers (.degrees, .minutes, .seconds), and a formatted string version (obj.string)
  385. */
  386. function convDDToDMS (decDegrees, precision) {
  387. const absDegrees = Math.abs(decDegrees);
  388. let degrees = Math.floor(absDegrees);
  389. const minutes = Math.floor(60 * (absDegrees - degrees)),
  390. seconds = round(3600 * (absDegrees - degrees) - 60 * minutes, precision);
  391. let outString = degrees + "° " + minutes + "' " + seconds + "\"";
  392. if (isNegativeZero(decDegrees) || decDegrees < 0) {
  393. degrees = -degrees;
  394. outString = "-" + outString;
  395. }
  396. return {
  397. "degrees": degrees,
  398. "minutes": minutes,
  399. "seconds": seconds,
  400. "string": outString
  401. };
  402. }
  403. /**
  404. * Convert Decimal Degrees to Degrees Decimal Minutes
  405. *
  406. * @param {number} decDegrees - The input degrees to be converted
  407. * @param {number} precision - The precision the input data should be rounded to
  408. * @returns {{string: string, degrees: number, minutes: number}} An object containing the raw converted value as separate numbers (.degrees, .minutes), and a formatted string version (obj.string)
  409. */
  410. function convDDToDDM (decDegrees, precision) {
  411. const absDegrees = Math.abs(decDegrees);
  412. let degrees = Math.floor(absDegrees);
  413. const minutes = absDegrees - degrees,
  414. decMinutes = round(minutes * 60, precision);
  415. let outString = degrees + "° " + decMinutes + "'";
  416. if (decDegrees < 0 || isNegativeZero(decDegrees)) {
  417. degrees = -degrees;
  418. outString = "-" + outString;
  419. }
  420. return {
  421. "degrees": degrees,
  422. "minutes": decMinutes,
  423. "string": outString,
  424. };
  425. }
  426. /**
  427. * Finds and returns the compass directions in an input string
  428. *
  429. * @param {string} input - The input co-ordinates containing the direction
  430. * @param {string} delim - The delimiter separating latitide and longitude
  431. * @returns {string[]} String array containing the latitude and longitude directions
  432. */
  433. export function findDirs(input, delim) {
  434. const upperInput = input.toUpperCase();
  435. const dirExp = new RegExp(/[NESW]/g);
  436. const dirs = upperInput.match(dirExp);
  437. if (dirs) {
  438. // If there's actually compass directions
  439. // in the input, use these to work out the direction
  440. if (dirs.length <= 2 && dirs.length >= 1) {
  441. return dirs.length === 2 ? [dirs[0], dirs[1]] : [dirs[0], ""];
  442. }
  443. }
  444. // Nothing was returned, so guess the directions
  445. let lat = upperInput,
  446. long,
  447. latDir = "",
  448. longDir = "";
  449. if (!delim.includes("Direction")) {
  450. if (upperInput.includes(delim)) {
  451. const split = upperInput.split(delim);
  452. if (split.length >= 1) {
  453. if (split[0] !== "") {
  454. lat = split[0];
  455. }
  456. if (split.length >= 2 && split[1] !== "") {
  457. long = split[1];
  458. }
  459. }
  460. }
  461. } else {
  462. const split = upperInput.split(dirExp);
  463. if (split.length > 1) {
  464. lat = split[0] === "" ? split[1] : split[0];
  465. if (split.length > 2 && split[2] !== "") {
  466. long = split[2];
  467. }
  468. }
  469. }
  470. if (lat) {
  471. lat = parseFloat(lat);
  472. latDir = lat < 0 ? "S" : "N";
  473. }
  474. if (long) {
  475. long = parseFloat(long);
  476. longDir = long < 0 ? "W" : "E";
  477. }
  478. return [latDir, longDir];
  479. }
  480. /**
  481. * Detects the co-ordinate format of the input data
  482. *
  483. * @param {string} input - The input data whose format we need to detect
  484. * @param {string} delim - The delimiter separating the data in input
  485. * @returns {string} The input format
  486. */
  487. export function findFormat (input, delim) {
  488. let testData;
  489. const mgrsPattern = new RegExp(/^[0-9]{2}\s?[C-HJ-NP-X]{1}\s?[A-HJ-NP-Z][A-HJ-NP-V]\s?[0-9\s]+/),
  490. osngPattern = new RegExp(/^[A-HJ-Z]{2}\s+[0-9\s]+$/),
  491. geohashPattern = new RegExp(/^[0123456789BCDEFGHJKMNPQRSTUVWXYZ]+$/),
  492. utmPattern = new RegExp(/^[0-9]{2}\s?[C-HJ-NP-X]\s[0-9.]+\s?[0-9.]+$/),
  493. degPattern = new RegExp(/[°'"]/g);
  494. input = input.trim();
  495. if (delim !== null && delim.includes("Direction")) {
  496. const split = input.split(/[NnEeSsWw]/);
  497. if (split.length > 1) {
  498. testData = split[0] === "" ? split[1] : split[0];
  499. }
  500. } else if (delim !== null && delim !== "") {
  501. if (input.includes(delim)) {
  502. const split = input.split(delim);
  503. if (split.length > 1) {
  504. testData = split[0] === "" ? split[1] : split[0];
  505. }
  506. } else {
  507. testData = input;
  508. }
  509. }
  510. // Test non-degrees formats
  511. if (!degPattern.test(input)) {
  512. const filteredInput = input.toUpperCase().replace(delim, "");
  513. if (utmPattern.test(filteredInput)) {
  514. return "Universal Transverse Mercator";
  515. }
  516. if (mgrsPattern.test(filteredInput)) {
  517. return "Military Grid Reference System";
  518. }
  519. if (osngPattern.test(filteredInput)) {
  520. return "Ordnance Survey National Grid";
  521. }
  522. if (geohashPattern.test(filteredInput)) {
  523. return "Geohash";
  524. }
  525. }
  526. // Test DMS/DDM/DD formats
  527. if (testData !== undefined) {
  528. const split = splitInput(testData);
  529. switch (split.length){
  530. case 3:
  531. return "Degrees Minutes Seconds";
  532. case 2:
  533. return "Degrees Decimal Minutes";
  534. case 1:
  535. return "Decimal Degrees";
  536. }
  537. }
  538. return null;
  539. }
  540. /**
  541. * Automatically find the delimeter type from the given input
  542. *
  543. * @param {string} input
  544. * @returns {string} Delimiter type
  545. */
  546. export function findDelim (input) {
  547. input = input.trim();
  548. const delims = [",", ";", ":"];
  549. const testDir = input.match(/[NnEeSsWw]/g);
  550. if (testDir !== null && testDir.length > 0 && testDir.length < 3) {
  551. // Possibly contains a direction
  552. const splitInput = input.split(/[NnEeSsWw]/);
  553. if (splitInput.length <= 3 && splitInput.length > 0) {
  554. // If there's 3 splits (one should be empty), then assume we have directions
  555. if (splitInput[0] === "") {
  556. return "Direction Preceding";
  557. } else if (splitInput[splitInput.length - 1] === "") {
  558. return "Direction Following";
  559. }
  560. }
  561. }
  562. // Loop through the standard delimiters, and try to find them in the input
  563. for (let i = 0; i < delims.length; i++) {
  564. const delim = delims[i];
  565. if (input.includes(delim)) {
  566. const splitInput = input.split(delim);
  567. if (splitInput.length <= 3 && splitInput.length > 0) {
  568. // Don't want to try and convert more than 2 co-ordinates
  569. return delim;
  570. }
  571. }
  572. }
  573. return null;
  574. }
  575. /**
  576. * Gets the real string for a delimiter name.
  577. *
  578. * @param {string} delim The delimiter to be matched
  579. * @returns {string}
  580. */
  581. export function realDelim (delim) {
  582. return {
  583. "Auto": "Auto",
  584. "Space": " ",
  585. "\\n": "\n",
  586. "Comma": ",",
  587. "Semi-colon": ";",
  588. "Colon": ":"
  589. }[delim];
  590. }
  591. /**
  592. * Returns true if a zero is negative
  593. *
  594. * @param {number} zero
  595. * @returns {boolean}
  596. */
  597. function isNegativeZero(zero) {
  598. return zero === 0 && (1/zero < 0);
  599. }
  600. /**
  601. * Rounds a number to a specified number of decimal places
  602. *
  603. * @param {number} input - The number to be rounded
  604. * @param {precision} precision - The number of decimal places the number should be rounded to
  605. * @returns {number}
  606. */
  607. function round(input, precision) {
  608. precision = Math.pow(10, precision);
  609. return Math.round(input * precision) / precision;
  610. }