Remove duplicate code for parsing numbers.
This commit is contained in:
parent
15d3f45bae
commit
b1075aa2ec
6 changed files with 181 additions and 35 deletions
|
@ -3,11 +3,12 @@ import ErrorTypes from "../utils/errorTypes";
|
|||
import * as MailService from "../services/mailService";
|
||||
import * as Resources from "../utils/resources";
|
||||
import { handleJSONWithData, RequestData } from "../utils/resources";
|
||||
import { normalizeString, parseInteger } from "../shared/utils/strings";
|
||||
import { normalizeString } from "../shared/utils/strings";
|
||||
import { forConstraint } from "../shared/validation/validator";
|
||||
import type { Request, Response } from "express";
|
||||
import { isString, Mail, MailId } from "../types";
|
||||
import { HttpHeader } from "../shared/utils/http";
|
||||
import { parseToInteger } from "../shared/utils/numbers";
|
||||
|
||||
const isValidId = forConstraint(CONSTRAINTS.id, false);
|
||||
|
||||
|
@ -22,7 +23,7 @@ async function withValidMailId(data: RequestData): Promise<MailId> {
|
|||
throw { data: "Invalid mail id.", type: ErrorTypes.badRequest };
|
||||
}
|
||||
|
||||
return parseInteger(id) as MailId;
|
||||
return parseToInteger(id) as MailId;
|
||||
}
|
||||
|
||||
export const get = handleJSONWithData(async (data) => {
|
||||
|
|
|
@ -11,7 +11,8 @@ import * as MailService from "../services/mailService";
|
|||
import * as NodeService from "../services/nodeService";
|
||||
import * as Resources from "../utils/resources";
|
||||
import type { RestParams } from "../utils/resources";
|
||||
import { normalizeMac, parseInteger } from "../shared/utils/strings";
|
||||
import { normalizeMac } from "../shared/utils/strings";
|
||||
import { parseToInteger } from "../shared/utils/numbers";
|
||||
import { monitoringDisableUrl } from "../utils/urlBuilder";
|
||||
import CONSTRAINTS from "../shared/validation/constraints";
|
||||
import { forConstraint } from "../shared/validation/validator";
|
||||
|
@ -525,7 +526,7 @@ async function sendOfflineMails(
|
|||
startTime: UnixTimestampSeconds,
|
||||
mailType: MailType
|
||||
): Promise<void> {
|
||||
const mailNumber = parseInteger(mailType.split("-")[2]);
|
||||
const mailNumber = parseToInteger(mailType.split("-")[2]);
|
||||
await sendMonitoringMailsBatched(
|
||||
"offline " + mailNumber,
|
||||
mailType,
|
||||
|
|
|
@ -24,6 +24,15 @@ export function isInteger(arg: unknown): arg is number {
|
|||
return isNumber(arg) && Number.isInteger(arg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard checking the given value is a floating point `number`.
|
||||
*
|
||||
* @param arg - Value to check.
|
||||
*/
|
||||
export function isFloat(arg: unknown): arg is number {
|
||||
return isNumber(arg) && Number.isFinite(arg);
|
||||
}
|
||||
|
||||
// =====================================================================================================================
|
||||
// Strings
|
||||
// =====================================================================================================================
|
||||
|
|
163
server/shared/utils/numbers.ts
Normal file
163
server/shared/utils/numbers.ts
Normal file
|
@ -0,0 +1,163 @@
|
|||
/**
|
||||
* Utility functions for numbers.
|
||||
*/
|
||||
import { isFloat, isInteger } from "../types";
|
||||
|
||||
// TODO: Write tests!
|
||||
|
||||
/**
|
||||
* Normalizes the given `string` before parsing as a `number` by:
|
||||
*
|
||||
* * Removing whitespace from beginning and end of the `string`.
|
||||
* * Removing one `+` at the beginning of the number.
|
||||
*
|
||||
* @param arg - String to normalize.
|
||||
* @returns The normalized string.
|
||||
*/
|
||||
function normalizeStringToParse(arg: string): string {
|
||||
const str = (arg as string).trim();
|
||||
return str.startsWith("+") ? str.slice(1) : str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given value and converts it into an integer.
|
||||
*
|
||||
* For a `string` to be considered a valid representation of an integer `number` it has to satisfy the
|
||||
* following criteria:
|
||||
*
|
||||
* * The integer must be base 10.
|
||||
* * The `string` starts with an optional `+` or `-` sign followed by one or more digits.
|
||||
* * The first digit must not be `0`.
|
||||
* * Surrounding whitespaces will be ignored.
|
||||
*
|
||||
* @param arg - Value to parse.
|
||||
* @returns The parsed integer `number`.
|
||||
* @throws {@link SyntaxError} - If the given value does not represent a valid integer.
|
||||
* @throws {@link RangeError} - If the given value is a non-integer number.
|
||||
* @throws {@link TypeError} - If the given value is neither an integer `number` nor a `string`.
|
||||
*/
|
||||
export function parseToInteger(arg: unknown): number;
|
||||
|
||||
/**
|
||||
* Parses the given value and converts it into an integer.
|
||||
*
|
||||
* For a `string` to be considered a valid representation of an integer `number` it has to satisfy the
|
||||
* following criteria:
|
||||
*
|
||||
* * The integer base matches the given radix.
|
||||
* * The `string` starts with an optional `+` or `-` sign followed by one or more digits.
|
||||
* * The first digit must not be `0`.
|
||||
* * Surrounding whitespaces will be ignored.
|
||||
*
|
||||
* @param arg - Value to parse.
|
||||
* @param radix - Integer base to parse against. Must be an integer in range `2 <= radix <= 36`.
|
||||
* @returns The parsed integer `number`.
|
||||
* @throws {@link SyntaxError} - If the given value does not represent a valid integer.
|
||||
* @throws {@link RangeError} - If the given value is a non-integer number or the given radix is out of range.
|
||||
* @throws {@link TypeError} - If the given value is neither an integer `number` nor a `string`.
|
||||
*/
|
||||
export function parseToInteger(arg: unknown, radix: number): number;
|
||||
|
||||
/**
|
||||
* Parses the given value and converts it into an integer.
|
||||
*
|
||||
* For a `string` to be considered a valid representation of an integer `number` it has to satisfy the
|
||||
* following criteria:
|
||||
*
|
||||
* * The integer base matches the given or default radix.
|
||||
* * The `string` starts with an optional `+` or `-` sign followed by one or more digits.
|
||||
* * The first digit must not be `0`.
|
||||
* * Surrounding whitespaces will be ignored.
|
||||
*
|
||||
* @param arg - Value to parse.
|
||||
* @param radix - Optional: Integer base to parse against. Must be an integer in range `2 <= radix <= 36`.
|
||||
* @returns The parsed integer `number`.
|
||||
* @throws {@link SyntaxError} - If the given value does not represent a valid integer.
|
||||
* @throws {@link RangeError} - If the given value is a non-integer number or the given radix is out of range.
|
||||
* @throws {@link TypeError} - If the given value is neither an integer `number` nor a `string`.
|
||||
*/
|
||||
export function parseToInteger(arg: unknown, radix?: number): number {
|
||||
if (!radix) {
|
||||
radix = 10;
|
||||
}
|
||||
if (radix < 2 || radix > 36 || isNaN(radix)) {
|
||||
throw new RangeError(`Radix out of range: ${radix}`);
|
||||
}
|
||||
|
||||
if (isInteger(arg)) {
|
||||
return arg;
|
||||
}
|
||||
|
||||
switch (typeof arg) {
|
||||
case "number":
|
||||
throw new RangeError(`Not an integer: ${arg}`);
|
||||
case "string": {
|
||||
const str = normalizeStringToParse(arg as string);
|
||||
const num = parseInt(str, radix);
|
||||
if (isNaN(num)) {
|
||||
throw new SyntaxError(
|
||||
`Not a valid number (radix: ${radix}): ${str}`
|
||||
);
|
||||
}
|
||||
if (num.toString(radix).toLowerCase() !== str.toLowerCase()) {
|
||||
throw new SyntaxError(
|
||||
`Parsed integer does not match given string (radix: ${radix}): ${str}`
|
||||
);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
default:
|
||||
throw new TypeError(
|
||||
`Cannot parse number (radix: ${radix}): ${arg} of type ${typeof arg}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given value and converts it into a finite floating point `number`.
|
||||
*
|
||||
* For a `string` to be considered a valid representation of a floating point `number` it has to satisfy the
|
||||
* following criteria:
|
||||
*
|
||||
* * The number is base 10.
|
||||
* * The `string` starts with an optional `+` or `-` sign followed by one or more digits and at most one decimal point.
|
||||
* * It starts with at most one leading `0` (after the optional sign).
|
||||
* * Surrounding whitespaces will be ignored.
|
||||
*
|
||||
* @param arg - Value to parse.
|
||||
* @returns The parsed floating point `number`.
|
||||
* @throws {@link SyntaxError} - If the given value does not represent a finite floating point number.
|
||||
* @throws {@link RangeError} - If the given value is number that is either not finite or is `NaN`.
|
||||
* @throws {@link TypeError} - If the given value is neither a floating point `number` nor a `string`.
|
||||
*/
|
||||
export function parseToFloat(arg: unknown): number {
|
||||
if (isFloat(arg)) {
|
||||
return arg;
|
||||
}
|
||||
switch (typeof arg) {
|
||||
case "number":
|
||||
throw new Error(`Not a finite number: ${arg}`);
|
||||
case "string": {
|
||||
let str = (arg as string).trim();
|
||||
const num = parseFloat(str);
|
||||
if (isNaN(num)) {
|
||||
throw new Error(`Not a valid number: ${str}`);
|
||||
}
|
||||
|
||||
if (Number.isInteger(num)) {
|
||||
str = str.replace(/\.0+$/, "");
|
||||
}
|
||||
|
||||
if (num.toString(10) !== str) {
|
||||
throw new Error(
|
||||
`Parsed float does not match given string: ${num.toString(
|
||||
10
|
||||
)} !== ${str}`
|
||||
);
|
||||
}
|
||||
return num;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Cannot parse number: ${arg}`);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
/**
|
||||
* Utility functions all around strings.
|
||||
*/
|
||||
import { isInteger, type MAC } from "../types";
|
||||
import type { MAC } from "../types";
|
||||
|
||||
/**
|
||||
* Trims the given `string` and replaces multiple whitespaces by one space each.
|
||||
|
@ -39,31 +39,3 @@ export function normalizeMac(mac: MAC): MAC {
|
|||
|
||||
return macParts.join(":") as MAC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the given `string` and converts it into an integer.
|
||||
*
|
||||
* For a `string` to be considered a valid representation of an integer `number` it has to satisfy the
|
||||
* following criteria:
|
||||
*
|
||||
* * The integer is base `10`.
|
||||
* * The `string` starts with an optional `+` or `-` sign followed by one or more digits.
|
||||
* * The first digit must not be `0`.
|
||||
* * The `string` does not contain any other characters.
|
||||
*
|
||||
* @param str - `string` to parse.
|
||||
* @returns The parsed integer `number`.
|
||||
* @throws {@link SyntaxError} - If the given `string` does not represent a valid integer.
|
||||
*/
|
||||
export function parseInteger(str: string): number {
|
||||
const parsed = parseInt(str, 10);
|
||||
const original = str.startsWith("+") ? str.slice(1) : str;
|
||||
|
||||
if (isInteger(parsed) && parsed.toString() === original) {
|
||||
return parsed;
|
||||
} else {
|
||||
throw new SyntaxError(
|
||||
`String does not represent a valid integer: "${str}"`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { parseInteger } from "../utils/strings";
|
||||
import { parseToInteger } from "../utils/numbers";
|
||||
import {
|
||||
isBoolean,
|
||||
isNumber,
|
||||
|
@ -67,7 +67,7 @@ function isValidBoolean(value: unknown): boolean {
|
|||
|
||||
function isValidNumber(constraint: Constraint, value: unknown): boolean {
|
||||
if (isString(value)) {
|
||||
value = parseInteger(value);
|
||||
value = parseToInteger(value);
|
||||
}
|
||||
|
||||
if (!isNumber(value)) {
|
||||
|
|
Loading…
Reference in a new issue