Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions public/js/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand All @@ -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",
},
Expand Down Expand Up @@ -119,13 +125,17 @@ 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);
selectedOperation.value = "";
searchResults.style.display = "block";
clearIcon.style.display = "none";
dropdownIcon.textContent = "▲";
checkDMSContainer.style.display = "none";
checkDMS.checked = false;
}

function selectOperation(operation) {
Expand All @@ -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) => {
Expand Down
48 changes: 47 additions & 1 deletion server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
59 changes: 59 additions & 0 deletions test/integration/isLatLong.test.js
Original file line number Diff line number Diff line change
@@ -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();
});
});
31 changes: 31 additions & 0 deletions test/unit/isLatLong.test.js
Original file line number Diff line number Diff line change
@@ -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);
});
});
26 changes: 26 additions & 0 deletions validationFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
4 changes: 4 additions & 0 deletions views/pages/index.pug
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down