Skip to content

Commit 9cfa517

Browse files
Merge pull request #119 from cbanlawi/feat/add-isLatLong-route-and-function
(feat): Add isLatLong API route and function
2 parents 068c2a7 + 861282b commit 9cfa517

6 files changed

Lines changed: 187 additions & 4 deletions

File tree

public/js/script.js

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ const operations = [
1717
{ value: "isBoolean", label: "Is Boolean" },
1818
{ value: "isCountry", label: "Is Country" },
1919
{ value: "isValidStateCode", label: "Is Valid State Code" },
20+
{ value: "isLatLong", label: "Is Latitude/Longitude" },
2021
];
2122

2223
async function getResponse() {
2324
const inputString = document.querySelector("#inputString")?.value;
2425
const endpoint = document.querySelector("#selectedOperation")?.value;
26+
const checkDMS = document.querySelector("#checkDMS")?.checked;
2527

2628
if (!endpoint) {
2729
alert("Please select an operation first");
@@ -31,12 +33,16 @@ async function getResponse() {
3133
// Use window.location.origin to get the base URL
3234
const baseUrl = window.location.origin;
3335

36+
let requestBody = { inputString };
37+
38+
if (endpoint === "isLatLong") {
39+
requestBody.checkDMS = checkDMS;
40+
}
41+
3442
try {
3543
const response = await fetch(`${baseUrl}/api/${endpoint}`, {
3644
method: "POST",
37-
body: JSON.stringify({
38-
inputString: inputString,
39-
}),
45+
body: JSON.stringify(requestBody),
4046
headers: {
4147
"Content-Type": "application/json",
4248
},
@@ -119,13 +125,17 @@ function clearSelection() {
119125
const selectedOperation = document.querySelector("#selectedOperation");
120126
const clearIcon = document.querySelector("#clearSearch");
121127
const dropdownIcon = document.querySelector("#dropdownToggle");
128+
const checkDMSContainer = document.querySelector("#checkDMSContainer");
129+
const checkDMS = document.querySelector("#checkDMS");
122130

123131
searchInput.value = "";
124132
renderOperations(operations);
125133
selectedOperation.value = "";
126134
searchResults.style.display = "block";
127135
clearIcon.style.display = "none";
128136
dropdownIcon.textContent = "▲";
137+
checkDMSContainer.style.display = "none";
138+
checkDMS.checked = false;
129139
}
130140

131141
function selectOperation(operation) {
@@ -140,6 +150,13 @@ function selectOperation(operation) {
140150
searchResults.style.display = "none";
141151
clearIcon.style.display = "block";
142152
dropdownIcon.textContent = "▼";
153+
154+
if (operation.value === "isLatLong") {
155+
checkDMSContainer.style.display = "block";
156+
} else {
157+
checkDMSContainer.style.display = "none";
158+
document.querySelector("#checkDMS").checked = false;
159+
}
143160
}
144161

145162
document.addEventListener("click", (e) => {

server.js

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@ app.use((err, req, res, next) => {
184184
* @property {boolean} [caseSensitive=true] - Whether the comparison should be case-sensitive (default: true)
185185
*/
186186

187+
/**
188+
* A LatLongRequest
189+
* @typedef {object} LatLongRequest
190+
* @property {string} inputString.required - The latitude and longitude to validate (supports decimal degrees or DMS format)
191+
* @property {boolean} [checkDMS=false] - Optionally check if the input is in DMS (Degrees, Minutes, Seconds) format
192+
*/
187193

188194
/**
189195
* POST /api/isField
@@ -1086,4 +1092,44 @@ app.post('/api/isValidStateCode', (req, res) => {
10861092
res.json({ result });
10871093
});
10881094

1089-
module.exports = app;
1095+
/**
1096+
* POST /api/isLatLong
1097+
* @summary Returns true if valid latitude and longitude, otherwise false
1098+
* @description
1099+
* Supports two formats:
1100+
* 1. Decimal degrees: e.g. "37.7749,-122.4194" or "37.7749, -122.4194"
1101+
* 2. DMS (degrees, minutes, seconds): e.g. "37°46'30\"N 122°25'10\"W" (if checkDMS: true)
1102+
* @param {LatLongRequest} request.body.required - The input string and optional checkDMS flag
1103+
* @return {BasicResponse} 200 - Success response
1104+
* @return {BadRequestResponse} 400 - Bad request response
1105+
* @example request - decimal degrees
1106+
* {
1107+
* "inputString": "34.052235,-118.243683"
1108+
* }
1109+
* @example request - DMS
1110+
* {
1111+
* "inputString": "34°3'8.1\"N 118°14'37.2\"W",
1112+
* "checkDMS": true
1113+
* }
1114+
* @example response - 200 - example payload
1115+
* {
1116+
* "result": true
1117+
* }
1118+
* @example response - 400 - example
1119+
* {
1120+
* "error": "Input string required as a parameter."
1121+
* }
1122+
*/
1123+
app.post('/api/isLatLong', (req, res) => {
1124+
const { inputString, checkDMS = false } = req.body;
1125+
1126+
if (!inputString) {
1127+
return res.status(400).json({ error: requiredParameterResponse });
1128+
}
1129+
1130+
const result = ValidationFunctions.isLatLong(inputString, { checkDMS });
1131+
1132+
res.json({ result });
1133+
});
1134+
1135+
module.exports = app;

test/integration/isLatLong.test.js

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const request = require('supertest');
2+
const app = require('../../server.js');
3+
4+
describe('POST /api/isLatLong', () => {
5+
it('should return true for a valid latitude and longitude in decimal degrees format', async () => {
6+
const response = await request(app)
7+
.post('/api/isLatLong')
8+
.send({ inputString: '34.052235,-118.243683' })
9+
.expect(200);
10+
11+
expect(response.body).toHaveProperty('result', true);
12+
});
13+
14+
it('should return true for a valid latitude and longitude in degrees, minutes, seconds format', async () => {
15+
const response = await request(app)
16+
.post('/api/isLatLong')
17+
.send({ inputString: "34°3'8.1\"N 118°14'37.2\"W", checkDMS: true })
18+
.expect(200);
19+
20+
expect(response.body).toHaveProperty('result', true);
21+
});
22+
23+
it('should return false for an invalid latitude and longitude in decimal degrees format', async () => {
24+
const response = await request(app)
25+
.post('/api/isLatLong')
26+
.send({ inputString: '34.052235,-118.243683,extra' })
27+
.expect(200);
28+
29+
expect(response.body).toHaveProperty('result', false);
30+
});
31+
32+
it('should return false for an invalid latitude and longitude in degrees, minutes, seconds format', async () => {
33+
const response = await request(app)
34+
.post('/api/isLatLong')
35+
.send({ inputString: "34°3'8.1'N 118°14'37.2'W extra", checkDMS: true })
36+
.expect(200);
37+
38+
expect(response.body).toHaveProperty('result', false);
39+
});
40+
41+
it('should return false if inputString is not a string', async () => {
42+
const response = await request(app)
43+
.post('/api/isLatLong')
44+
.send({ inputString: 12345 })
45+
.expect(200);
46+
47+
expect(response.body).toHaveProperty('result', false);
48+
});
49+
50+
it('should return 400 if inputString is missing', async () => {
51+
const response = await request(app)
52+
.post('/api/isLatLong')
53+
.send({})
54+
.expect(400);
55+
56+
expect(response.body).toHaveProperty('error');
57+
expect(response.body.error).toBeDefined();
58+
});
59+
});

test/unit/isLatLong.test.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const { isLatLong } = require("../../validationFunctions");
2+
3+
describe("isLatLong", () => {
4+
it("should return true for valid latitude and longitude in decimal degrees format", () => {
5+
expect(isLatLong("34.052235,-118.243683")).toBe(true);
6+
});
7+
8+
it("should return true for valid latitude and longitude in degrees, minutes, seconds format", () => {
9+
expect(isLatLong("34°3'8.1\"N 118°14'37.2\"W", { checkDMS: true })).toBe(
10+
true
11+
);
12+
});
13+
14+
it("should return false for invalid latitude and longitude in decimal degrees format", () => {
15+
expect(isLatLong("34.052235,-118.243683,extra")).toBe(false);
16+
});
17+
18+
it("should return false for invalid latitude and longitude in degrees, minutes, seconds format", () => {
19+
expect(
20+
isLatLong("34°3'8.1'N 118°14'37.2'W extra", { checkDMS: true })
21+
).toBe(false);
22+
});
23+
24+
it("should return false if inputString is not a string", () => {
25+
expect(isLatLong(12345)).toBe(false);
26+
});
27+
28+
it("should return false if inputString is an empty string", () => {
29+
expect(isLatLong("")).toBe(false);
30+
});
31+
});

validationFunctions.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,32 @@ module.exports = class ValidationFunctions {
450450

451451
return validStateCodes.includes(inputString);
452452
}
453+
454+
/**
455+
* Checks if the given string is a valid latitude-longitude coordinate.
456+
*
457+
* * Supports two formats:
458+
* 1. Decimal degrees: e.g. "37.7749,-122.4194" or "37.7749, -122.4194"
459+
* 2. DMS (degrees, minutes, seconds): e.g. "37°46'30\"N 122°25'10\"W" (if checkDMS: true)
460+
*
461+
* @param {string} inputString - The coordinate to validate.
462+
* @param {Object} [options={ checkDMS: false }] - Options for validation.
463+
* @param {boolean} [options.checkDMS=false] - If true, checks for DMS format.
464+
* @returns {boolean} - Returns `true` if `inputString` is a valid latitude-longitude coordinate, otherwise `false`.
465+
*/
466+
static isLatLong(inputString, options = { checkDMS: false }) {
467+
if (!inputString || typeof inputString !== "string") return false;
468+
469+
const trimmedInput = inputString.trim();
470+
471+
if (options.checkDMS) {
472+
const dmsRegex = /^(\d{1,3})°\d{1,2}'\d{1,2}(\.\d+)?"[NS]\s+(\d{1,3})°\d{1,2}'\d{1,2}(\.\d+)?"[EW]$/;
473+
return dmsRegex.test(trimmedInput);
474+
}
475+
476+
const decimalDegreesRegex = /^-?\d{1,3}(?:\.\d+)?,\s*-?\d{1,3}(?:\.\d+)?$/;
477+
return decimalDegreesRegex.test(trimmedInput);
478+
}
453479
}
454480

455481
const handleAxiosError = (error) => {

views/pages/index.pug

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ block content
1717
span(class="dropdown-icon" id="dropdownToggle" onclick="toggleDropdown()")
1818
div(id="searchResults" class="search-results")
1919
input(type="hidden" id="selectedOperation")
20+
div#checkDMSContainer(style='display: none; margin-top: 8px;')
21+
label(for='checkDMS' style='display: flex; align-items: center; gap: 8px;')
22+
input#checkDMS(type='checkbox' style='margin: 0;')
23+
| Check DMS format (e.g. 34°3'8.1"N 118°14'37.2"W)
2024
br
2125
br
2226
button(onclick='getResponse()' id='getResponseButton' disabled='true') Get Response

0 commit comments

Comments
 (0)