diff --git a/public/js/script.js b/public/js/script.js index bd8eded..74dd19b 100644 --- a/public/js/script.js +++ b/public/js/script.js @@ -17,11 +17,13 @@ const operations = [ { value: "isBoolean", label: "Is Boolean" }, { value: "isCountry", label: "Is Country" }, { value: "isValidStateCode", label: "Is Valid State Code" }, + { value: "isLatLong", label: "Is Latitude/Longitude" }, ]; async function getResponse() { const inputString = document.querySelector("#inputString")?.value; const endpoint = document.querySelector("#selectedOperation")?.value; + const checkDMS = document.querySelector("#checkDMS")?.checked; if (!endpoint) { alert("Please select an operation first"); @@ -31,12 +33,16 @@ async function getResponse() { // Use window.location.origin to get the base URL const baseUrl = window.location.origin; + let requestBody = { inputString }; + + if (endpoint === "isLatLong") { + requestBody.checkDMS = checkDMS; + } + try { const response = await fetch(`${baseUrl}/api/${endpoint}`, { method: "POST", - body: JSON.stringify({ - inputString: inputString, - }), + body: JSON.stringify(requestBody), headers: { "Content-Type": "application/json", }, @@ -119,6 +125,8 @@ function clearSelection() { const selectedOperation = document.querySelector("#selectedOperation"); const clearIcon = document.querySelector("#clearSearch"); const dropdownIcon = document.querySelector("#dropdownToggle"); + const checkDMSContainer = document.querySelector("#checkDMSContainer"); + const checkDMS = document.querySelector("#checkDMS"); searchInput.value = ""; renderOperations(operations); @@ -126,6 +134,8 @@ function clearSelection() { searchResults.style.display = "block"; clearIcon.style.display = "none"; dropdownIcon.textContent = "▲"; + checkDMSContainer.style.display = "none"; + checkDMS.checked = false; } function selectOperation(operation) { @@ -140,6 +150,13 @@ function selectOperation(operation) { searchResults.style.display = "none"; clearIcon.style.display = "block"; dropdownIcon.textContent = "▼"; + + if (operation.value === "isLatLong") { + checkDMSContainer.style.display = "block"; + } else { + checkDMSContainer.style.display = "none"; + document.querySelector("#checkDMS").checked = false; + } } document.addEventListener("click", (e) => { diff --git a/server.js b/server.js index 627a3a6..137dd90 100644 --- a/server.js +++ b/server.js @@ -184,6 +184,12 @@ app.use((err, req, res, next) => { * @property {boolean} [caseSensitive=true] - Whether the comparison should be case-sensitive (default: true) */ +/** + * A LatLongRequest + * @typedef {object} LatLongRequest + * @property {string} inputString.required - The latitude and longitude to validate (supports decimal degrees or DMS format) + * @property {boolean} [checkDMS=false] - Optionally check if the input is in DMS (Degrees, Minutes, Seconds) format + */ /** * POST /api/isField @@ -1086,4 +1092,44 @@ app.post('/api/isValidStateCode', (req, res) => { res.json({ result }); }); -module.exports = app; +/** + * POST /api/isLatLong + * @summary Returns true if valid latitude and longitude, otherwise false + * @description + * Supports two formats: + * 1. Decimal degrees: e.g. "37.7749,-122.4194" or "37.7749, -122.4194" + * 2. DMS (degrees, minutes, seconds): e.g. "37°46'30\"N 122°25'10\"W" (if checkDMS: true) + * @param {LatLongRequest} request.body.required - The input string and optional checkDMS flag + * @return {BasicResponse} 200 - Success response + * @return {BadRequestResponse} 400 - Bad request response + * @example request - decimal degrees + * { + * "inputString": "34.052235,-118.243683" + * } + * @example request - DMS + * { + * "inputString": "34°3'8.1\"N 118°14'37.2\"W", + * "checkDMS": true + * } + * @example response - 200 - example payload + * { + * "result": true + * } + * @example response - 400 - example + * { + * "error": "Input string required as a parameter." + * } + */ +app.post('/api/isLatLong', (req, res) => { + const { inputString, checkDMS = false } = req.body; + + if (!inputString) { + return res.status(400).json({ error: requiredParameterResponse }); + } + + const result = ValidationFunctions.isLatLong(inputString, { checkDMS }); + + res.json({ result }); +}); + +module.exports = app; \ No newline at end of file diff --git a/test/integration/isLatLong.test.js b/test/integration/isLatLong.test.js new file mode 100644 index 0000000..87ce254 --- /dev/null +++ b/test/integration/isLatLong.test.js @@ -0,0 +1,59 @@ +const request = require('supertest'); +const app = require('../../server.js'); + +describe('POST /api/isLatLong', () => { + it('should return true for a valid latitude and longitude in decimal degrees format', async () => { + const response = await request(app) + .post('/api/isLatLong') + .send({ inputString: '34.052235,-118.243683' }) + .expect(200); + + expect(response.body).toHaveProperty('result', true); + }); + + it('should return true for a valid latitude and longitude in degrees, minutes, seconds format', async () => { + const response = await request(app) + .post('/api/isLatLong') + .send({ inputString: "34°3'8.1\"N 118°14'37.2\"W", checkDMS: true }) + .expect(200); + + expect(response.body).toHaveProperty('result', true); + }); + + it('should return false for an invalid latitude and longitude in decimal degrees format', async () => { + const response = await request(app) + .post('/api/isLatLong') + .send({ inputString: '34.052235,-118.243683,extra' }) + .expect(200); + + expect(response.body).toHaveProperty('result', false); + }); + + it('should return false for an invalid latitude and longitude in degrees, minutes, seconds format', async () => { + const response = await request(app) + .post('/api/isLatLong') + .send({ inputString: "34°3'8.1'N 118°14'37.2'W extra", checkDMS: true }) + .expect(200); + + expect(response.body).toHaveProperty('result', false); + }); + + it('should return false if inputString is not a string', async () => { + const response = await request(app) + .post('/api/isLatLong') + .send({ inputString: 12345 }) + .expect(200); + + expect(response.body).toHaveProperty('result', false); + }); + + it('should return 400 if inputString is missing', async () => { + const response = await request(app) + .post('/api/isLatLong') + .send({}) + .expect(400); + + expect(response.body).toHaveProperty('error'); + expect(response.body.error).toBeDefined(); + }); +}); diff --git a/test/unit/isLatLong.test.js b/test/unit/isLatLong.test.js new file mode 100644 index 0000000..474667d --- /dev/null +++ b/test/unit/isLatLong.test.js @@ -0,0 +1,31 @@ +const { isLatLong } = require("../../validationFunctions"); + +describe("isLatLong", () => { + it("should return true for valid latitude and longitude in decimal degrees format", () => { + expect(isLatLong("34.052235,-118.243683")).toBe(true); + }); + + it("should return true for valid latitude and longitude in degrees, minutes, seconds format", () => { + expect(isLatLong("34°3'8.1\"N 118°14'37.2\"W", { checkDMS: true })).toBe( + true + ); + }); + + it("should return false for invalid latitude and longitude in decimal degrees format", () => { + expect(isLatLong("34.052235,-118.243683,extra")).toBe(false); + }); + + it("should return false for invalid latitude and longitude in degrees, minutes, seconds format", () => { + expect( + isLatLong("34°3'8.1'N 118°14'37.2'W extra", { checkDMS: true }) + ).toBe(false); + }); + + it("should return false if inputString is not a string", () => { + expect(isLatLong(12345)).toBe(false); + }); + + it("should return false if inputString is an empty string", () => { + expect(isLatLong("")).toBe(false); + }); +}); diff --git a/validationFunctions.js b/validationFunctions.js index 2e1f577..94157ad 100644 --- a/validationFunctions.js +++ b/validationFunctions.js @@ -450,6 +450,32 @@ module.exports = class ValidationFunctions { return validStateCodes.includes(inputString); } + + /** + * Checks if the given string is a valid latitude-longitude coordinate. + * + * * Supports two formats: + * 1. Decimal degrees: e.g. "37.7749,-122.4194" or "37.7749, -122.4194" + * 2. DMS (degrees, minutes, seconds): e.g. "37°46'30\"N 122°25'10\"W" (if checkDMS: true) + * + * @param {string} inputString - The coordinate to validate. + * @param {Object} [options={ checkDMS: false }] - Options for validation. + * @param {boolean} [options.checkDMS=false] - If true, checks for DMS format. + * @returns {boolean} - Returns `true` if `inputString` is a valid latitude-longitude coordinate, otherwise `false`. + */ + static isLatLong(inputString, options = { checkDMS: false }) { + if (!inputString || typeof inputString !== "string") return false; + + const trimmedInput = inputString.trim(); + + if (options.checkDMS) { + 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]$/; + return dmsRegex.test(trimmedInput); + } + + const decimalDegreesRegex = /^-?\d{1,3}(?:\.\d+)?,\s*-?\d{1,3}(?:\.\d+)?$/; + return decimalDegreesRegex.test(trimmedInput); + } } const handleAxiosError = (error) => { diff --git a/views/pages/index.pug b/views/pages/index.pug index 064e9f3..4b9a346 100644 --- a/views/pages/index.pug +++ b/views/pages/index.pug @@ -17,6 +17,10 @@ block content span(class="dropdown-icon" id="dropdownToggle" onclick="toggleDropdown()") ▼ div(id="searchResults" class="search-results") input(type="hidden" id="selectedOperation") + div#checkDMSContainer(style='display: none; margin-top: 8px;') + label(for='checkDMS' style='display: flex; align-items: center; gap: 8px;') + input#checkDMS(type='checkbox' style='margin: 0;') + | Check DMS format (e.g. 34°3'8.1"N 118°14'37.2"W) br br button(onclick='getResponse()' id='getResponseButton' disabled='true') Get Response