mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-12-29 03:20:46 +08:00
refactor: dont allow nullable numbers (@miodec) (#6564)
Enables strict boolean expressions rule for nullable numbers
This commit is contained in:
parent
cde852cf2a
commit
ea90e0a99e
17 changed files with 156 additions and 42 deletions
|
|
@ -52,7 +52,12 @@ import {
|
|||
XpBreakdown,
|
||||
} from "@monkeytype/contracts/schemas/results";
|
||||
import { Mode } from "@monkeytype/contracts/schemas/shared";
|
||||
import { mapRange, roundTo2, stdDev } from "@monkeytype/util/numbers";
|
||||
import {
|
||||
isSafeNumber,
|
||||
mapRange,
|
||||
roundTo2,
|
||||
stdDev,
|
||||
} from "@monkeytype/util/numbers";
|
||||
import {
|
||||
getCurrentDayTimestamp,
|
||||
getStartOfDayTimestamp,
|
||||
|
|
@ -321,7 +326,10 @@ export async function addResult(
|
|||
const earliestPossible =
|
||||
(lastResult?.timestamp ?? 0) + testDurationMilis + incompleteTestsMilis;
|
||||
const nowNoMilis = Math.floor(Date.now() / 1000) * 1000;
|
||||
if (lastResult?.timestamp && nowNoMilis < earliestPossible - 1000) {
|
||||
if (
|
||||
isSafeNumber(lastResult?.timestamp) &&
|
||||
nowNoMilis < earliestPossible - 1000
|
||||
) {
|
||||
void addLog(
|
||||
"invalid_result_spacing",
|
||||
{
|
||||
|
|
@ -777,7 +785,7 @@ async function calculateXp(
|
|||
Logger.error(`Could not fetch last result: ${getLastResultError}`);
|
||||
}
|
||||
|
||||
if (lastResult?.timestamp) {
|
||||
if (isSafeNumber(lastResult?.timestamp)) {
|
||||
const lastResultDay = getStartOfDayTimestamp(lastResult.timestamp);
|
||||
const today = getCurrentDayTimestamp();
|
||||
if (lastResultDay !== today) {
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import LaterQueue, {
|
|||
import { recordTimeToCompleteJob } from "../utils/prometheus";
|
||||
import { WeeklyXpLeaderboard } from "../services/weekly-xp-leaderboard";
|
||||
import { MonkeyMail } from "@monkeytype/contracts/schemas/users";
|
||||
import { mapRange } from "@monkeytype/util/numbers";
|
||||
import { isSafeNumber, mapRange } from "@monkeytype/util/numbers";
|
||||
|
||||
async function handleDailyLeaderboardResults(
|
||||
ctx: LaterTaskContexts["daily-leaderboard-results"]
|
||||
|
|
@ -74,7 +74,7 @@ async function handleDailyLeaderboardResults(
|
|||
)
|
||||
.max();
|
||||
|
||||
if (!xpReward) return;
|
||||
if (!isSafeNumber(xpReward)) return;
|
||||
|
||||
const rewardMail = buildMonkeyMail({
|
||||
subject: "Daily leaderboard placement",
|
||||
|
|
@ -164,7 +164,7 @@ async function handleWeeklyXpLeaderboardResults(
|
|||
)
|
||||
.max();
|
||||
|
||||
if (!xpReward) return;
|
||||
if (!isSafeNumber(xpReward)) return;
|
||||
|
||||
const rewardMail = buildMonkeyMail({
|
||||
subject: "Weekly XP Leaderboard placement",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as ThemeColors from "./theme-colors";
|
||||
import * as SlowTimer from "../states/slow-timer";
|
||||
import Config from "../config";
|
||||
import { isSafeNumber } from "@monkeytype/util/numbers";
|
||||
|
||||
type Particle = {
|
||||
x: number;
|
||||
|
|
@ -85,7 +86,7 @@ function createParticle(x: number, y: number, color: string): Particle {
|
|||
* @param {Particle} particle
|
||||
*/
|
||||
function updateParticle(particle: Particle): void {
|
||||
if (!ctx.canvas || !ctx.deltaTime) return;
|
||||
if (!ctx.canvas || !isSafeNumber(ctx.deltaTime)) return;
|
||||
|
||||
particle.prev.x = particle.x;
|
||||
particle.prev.y = particle.y;
|
||||
|
|
@ -123,7 +124,7 @@ export function init(): void {
|
|||
}
|
||||
|
||||
function render(): void {
|
||||
if (!ctx.lastFrame || !ctx.context2d || !ctx.canvas) return;
|
||||
if (!isSafeNumber(ctx.lastFrame) || !ctx.context2d || !ctx.canvas) return;
|
||||
ctx.rendering = true;
|
||||
const time = Date.now();
|
||||
ctx.deltaTime = (time - ctx.lastFrame) / 1000;
|
||||
|
|
@ -163,7 +164,7 @@ function render(): void {
|
|||
}
|
||||
|
||||
export function reset(immediate = false): void {
|
||||
if (!ctx.resetTimeOut) return;
|
||||
if (!isSafeNumber(ctx.resetTimeOut)) return;
|
||||
delete ctx.resetTimeOut;
|
||||
|
||||
clearTimeout(ctx.resetTimeOut);
|
||||
|
|
@ -213,7 +214,7 @@ export async function addPower(good = true, extra = false): Promise<void> {
|
|||
"transform",
|
||||
`translate(${shake[0]}px, ${shake[1]}px)`
|
||||
);
|
||||
if (ctx.resetTimeOut) clearTimeout(ctx.resetTimeOut);
|
||||
if (isSafeNumber(ctx.resetTimeOut)) clearTimeout(ctx.resetTimeOut);
|
||||
ctx.resetTimeOut = setTimeout(reset, 2000) as unknown as number;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ export async function update(
|
|||
console.debug("isToday", isToday);
|
||||
console.debug("isYesterday", isYesterday);
|
||||
|
||||
const offsetString = streakOffset
|
||||
const offsetString = Numbers.isSafeNumber(streakOffset)
|
||||
? `(${streakOffset > 0 ? "+" : ""}${streakOffset} offset)`
|
||||
: "";
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { z } from "zod";
|
|||
import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
|
||||
import { IdSchema } from "@monkeytype/contracts/schemas/util";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
import { isSafeNumber } from "@monkeytype/util/numbers";
|
||||
|
||||
const confirmedPSAs = new LocalStorageWithSchema({
|
||||
key: "confirmedPSAs",
|
||||
|
|
@ -136,7 +137,7 @@ export async function show(): Promise<void> {
|
|||
}
|
||||
const localmemory = getMemory();
|
||||
latest.forEach((psa) => {
|
||||
if (psa.date) {
|
||||
if (isSafeNumber(psa.date)) {
|
||||
const dateObj = new Date(psa.date);
|
||||
const diff = psa.date - Date.now();
|
||||
const string = secondsToString(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { UTCDateMini } from "@date-fns/utc/date/mini";
|
||||
import { safeNumber } from "@monkeytype/util/numbers";
|
||||
import {
|
||||
format,
|
||||
endOfMonth,
|
||||
|
|
@ -219,7 +220,7 @@ export class ModifiableTestActivityCalendar
|
|||
const lastDay = new UTCDateMini(this.lastDay);
|
||||
if (isSameDay(date, lastDay)) {
|
||||
const last = this.data.length - 1;
|
||||
this.data[last] = (this.data[last] || 0) + 1;
|
||||
this.data[last] = (safeNumber(this.data[last]) ?? 0) + 1;
|
||||
} else if (isBefore(date, lastDay)) {
|
||||
throw new Error("cannot alter data in the past.");
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {
|
|||
TestActivityCalendar,
|
||||
TestActivityMonth,
|
||||
} from "./test-activity-calendar";
|
||||
import { safeNumber } from "@monkeytype/util/numbers";
|
||||
|
||||
let yearSelector: SlimSelect | undefined = undefined;
|
||||
|
||||
|
|
@ -21,7 +22,10 @@ export function init(
|
|||
$("#testActivity").removeClass("hidden");
|
||||
|
||||
yearSelector = getYearSelector();
|
||||
initYearSelector("current", userSignUpDate?.getFullYear() || 2022);
|
||||
initYearSelector(
|
||||
"current",
|
||||
safeNumber(userSignUpDate?.getFullYear()) ?? 2022
|
||||
);
|
||||
updateLabels(calendar.firstDayOfWeek);
|
||||
update(calendar);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import * as Levels from "../utils/levels";
|
|||
import { getAll } from "./theme-colors";
|
||||
import * as SlowTimer from "../states/slow-timer";
|
||||
import { XpBreakdown } from "@monkeytype/contracts/schemas/results";
|
||||
import { mapRange } from "@monkeytype/util/numbers";
|
||||
import { isSafeNumber, mapRange } from "@monkeytype/util/numbers";
|
||||
|
||||
let breakdownVisible = false;
|
||||
let skip = false;
|
||||
|
|
@ -268,12 +268,12 @@ async function animateXpBreakdown(
|
|||
|
||||
await Misc.sleep(delay);
|
||||
|
||||
if (breakdown.fullAccuracy) {
|
||||
if (isSafeNumber(breakdown.fullAccuracy)) {
|
||||
await Misc.sleep(delay);
|
||||
total += breakdown.fullAccuracy;
|
||||
void flashTotalXp(total);
|
||||
await addBreakdownListItem("perfect", breakdown.fullAccuracy);
|
||||
} else if (breakdown.corrected) {
|
||||
} else if (isSafeNumber(breakdown.corrected)) {
|
||||
await Misc.sleep(delay);
|
||||
total += breakdown.corrected;
|
||||
void flashTotalXp(total);
|
||||
|
|
@ -282,19 +282,19 @@ async function animateXpBreakdown(
|
|||
|
||||
if (skip) return;
|
||||
|
||||
if (breakdown.quote) {
|
||||
if (isSafeNumber(breakdown.quote)) {
|
||||
await Misc.sleep(delay);
|
||||
total += breakdown.quote;
|
||||
void flashTotalXp(total);
|
||||
await addBreakdownListItem("quote", breakdown.quote);
|
||||
} else {
|
||||
if (breakdown.punctuation) {
|
||||
if (isSafeNumber(breakdown.punctuation)) {
|
||||
await Misc.sleep(delay);
|
||||
total += breakdown.punctuation;
|
||||
void flashTotalXp(total);
|
||||
await addBreakdownListItem("punctuation", breakdown.punctuation);
|
||||
}
|
||||
if (breakdown.numbers) {
|
||||
if (isSafeNumber(breakdown.numbers)) {
|
||||
await Misc.sleep(delay);
|
||||
total += breakdown.numbers;
|
||||
void flashTotalXp(total);
|
||||
|
|
@ -304,7 +304,7 @@ async function animateXpBreakdown(
|
|||
|
||||
if (skip) return;
|
||||
|
||||
if (breakdown.funbox) {
|
||||
if (isSafeNumber(breakdown.funbox)) {
|
||||
await Misc.sleep(delay);
|
||||
total += breakdown.funbox;
|
||||
void flashTotalXp(total);
|
||||
|
|
@ -313,7 +313,7 @@ async function animateXpBreakdown(
|
|||
|
||||
if (skip) return;
|
||||
|
||||
if (breakdown.streak) {
|
||||
if (isSafeNumber(breakdown.streak)) {
|
||||
await Misc.sleep(delay);
|
||||
total += breakdown.streak;
|
||||
void flashTotalXp(total);
|
||||
|
|
@ -322,7 +322,7 @@ async function animateXpBreakdown(
|
|||
|
||||
if (skip) return;
|
||||
|
||||
if (breakdown.accPenalty) {
|
||||
if (isSafeNumber(breakdown.accPenalty)) {
|
||||
await Misc.sleep(delay);
|
||||
total -= breakdown.accPenalty;
|
||||
void flashTotalXp(total);
|
||||
|
|
@ -331,7 +331,7 @@ async function animateXpBreakdown(
|
|||
|
||||
if (skip) return;
|
||||
|
||||
if (breakdown.incomplete) {
|
||||
if (isSafeNumber(breakdown.incomplete)) {
|
||||
await Misc.sleep(delay);
|
||||
total += breakdown.incomplete;
|
||||
void flashTotalXp(total);
|
||||
|
|
@ -340,7 +340,7 @@ async function animateXpBreakdown(
|
|||
|
||||
if (skip) return;
|
||||
|
||||
if (breakdown.configMultiplier) {
|
||||
if (isSafeNumber(breakdown.configMultiplier)) {
|
||||
await Misc.sleep(delay);
|
||||
total *= breakdown.configMultiplier;
|
||||
void flashTotalXp(total);
|
||||
|
|
@ -352,7 +352,7 @@ async function animateXpBreakdown(
|
|||
|
||||
if (skip) return;
|
||||
|
||||
if (breakdown.daily) {
|
||||
if (isSafeNumber(breakdown.daily)) {
|
||||
await Misc.sleep(delay);
|
||||
total += breakdown.daily;
|
||||
void flashTotalXp(total);
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import * as DB from "../db";
|
|||
import * as Loader from "../elements/loader";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import AnimatedModal, { ShowOptions } from "../utils/animated-modal";
|
||||
import { isSafeNumber } from "@monkeytype/util/numbers";
|
||||
|
||||
let rating = 0;
|
||||
|
||||
|
|
@ -33,11 +34,16 @@ function reset(): void {
|
|||
}
|
||||
|
||||
function getRatingAverage(quoteStats: QuoteStats): number {
|
||||
if (!quoteStats.totalRating || !quoteStats.ratings) {
|
||||
return 0;
|
||||
if (
|
||||
isSafeNumber(quoteStats.ratings) &&
|
||||
isSafeNumber(quoteStats.totalRating) &&
|
||||
quoteStats.ratings > 0 &&
|
||||
quoteStats.totalRating > 0
|
||||
) {
|
||||
return Math.round((quoteStats.totalRating / quoteStats.ratings) * 10) / 10;
|
||||
}
|
||||
|
||||
return Math.round((quoteStats.totalRating / quoteStats.ratings) * 10) / 10;
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function getQuoteStats(
|
||||
|
|
@ -66,7 +72,7 @@ export async function getQuoteStats(
|
|||
}
|
||||
|
||||
quoteStats = response.body.data as QuoteStats;
|
||||
if (quoteStats !== undefined && !quoteStats.average) {
|
||||
if (quoteStats !== undefined && quoteStats.average === undefined) {
|
||||
quoteStats.average = getRatingAverage(quoteStats);
|
||||
}
|
||||
|
||||
|
|
@ -118,7 +124,7 @@ export function show(quote: Quote, showOptions?: ShowOptions): void {
|
|||
const snapshot = DB.getSnapshot();
|
||||
const alreadyRated =
|
||||
snapshot?.quoteRatings?.[currentQuote.language]?.[currentQuote.id];
|
||||
if (alreadyRated) {
|
||||
if (isSafeNumber(alreadyRated)) {
|
||||
rating = alreadyRated;
|
||||
}
|
||||
refreshStars();
|
||||
|
|
@ -163,7 +169,7 @@ async function submit(): Promise<void> {
|
|||
|
||||
const languageRatings = quoteRatings?.[currentQuote.language] ?? {};
|
||||
|
||||
if (languageRatings?.[currentQuote.id]) {
|
||||
if (isSafeNumber(languageRatings?.[currentQuote.id])) {
|
||||
const oldRating = quoteRatings[currentQuote.language]?.[
|
||||
currentQuote.id
|
||||
] as number;
|
||||
|
|
@ -180,7 +186,10 @@ async function submit(): Promise<void> {
|
|||
Notifications.add("Rating updated", 1);
|
||||
} else {
|
||||
languageRatings[currentQuote.id] = rating;
|
||||
if (quoteStats?.ratings && quoteStats.totalRating) {
|
||||
if (
|
||||
isSafeNumber(quoteStats?.ratings) &&
|
||||
isSafeNumber(quoteStats.totalRating)
|
||||
) {
|
||||
quoteStats.ratings++;
|
||||
quoteStats.totalRating += rating;
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ let visibleTableLines = 0;
|
|||
function loadMoreLines(lineIndex?: number): void {
|
||||
if (filteredResults === undefined || filteredResults.length === 0) return;
|
||||
let newVisibleLines;
|
||||
if (lineIndex && lineIndex > visibleTableLines) {
|
||||
if (Numbers.isSafeNumber(lineIndex) && lineIndex > visibleTableLines) {
|
||||
newVisibleLines = Math.ceil(lineIndex / 10) * 10;
|
||||
} else {
|
||||
newVisibleLines = visibleTableLines + 10;
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import {
|
|||
Language,
|
||||
LanguageSchema,
|
||||
} from "@monkeytype/contracts/schemas/languages";
|
||||
import { isSafeNumber } from "@monkeytype/util/numbers";
|
||||
|
||||
const LeaderboardTypeSchema = z.enum(["allTime", "weekly", "daily"]);
|
||||
type LeaderboardType = z.infer<typeof LeaderboardTypeSchema>;
|
||||
|
|
@ -479,7 +480,9 @@ function buildTableRow(entry: LeaderboardEntry, me = false): string {
|
|||
}?isUid" class="entryName" uid=${entry.uid} router-link>${entry.name}</a>
|
||||
<div class="flagsAndBadge">
|
||||
${getHtmlByUserFlags(entry)}
|
||||
${entry.badgeId ? getBadgeHTMLbyId(entry.badgeId) : ""}
|
||||
${
|
||||
isSafeNumber(entry.badgeId) ? getBadgeHTMLbyId(entry.badgeId) : ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -530,7 +533,9 @@ function buildWeeklyTableRow(entry: XpLeaderboardEntry, me = false): string {
|
|||
}?isUid" class="entryName" uid=${entry.uid} router-link>${entry.name}</a>
|
||||
<div class="flagsAndBadge">
|
||||
${getHtmlByUserFlags(entry)}
|
||||
${entry.badgeId ? getBadgeHTMLbyId(entry.badgeId) : ""}
|
||||
${
|
||||
isSafeNumber(entry.badgeId) ? getBadgeHTMLbyId(entry.badgeId) : ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
|
@ -1071,7 +1076,7 @@ function handleJumpButton(action: Action, page?: number): void {
|
|||
const user = Auth?.currentUser;
|
||||
if (user) {
|
||||
const rank = state.userData?.rank;
|
||||
if (rank) {
|
||||
if (isSafeNumber(rank)) {
|
||||
// - 1 to make sure position 50 with page size 50 is on the first page (page 0)
|
||||
const page = Math.floor((rank - 1) / state.pageSize);
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import * as TestWords from "./test-words";
|
|||
import { prefersReducedMotion } from "../utils/misc";
|
||||
import { convertRemToPixels } from "../utils/numbers";
|
||||
import { splitIntoCharacters } from "../utils/strings";
|
||||
import { safeNumber } from "@monkeytype/util/numbers";
|
||||
|
||||
export let caretAnimating = true;
|
||||
const caret = document.querySelector("#caret") as HTMLElement;
|
||||
|
|
@ -163,8 +164,8 @@ export async function updatePosition(noAnim = false): Promise<void> {
|
|||
// offsetHeight is the same for all visible letters
|
||||
// so is offsetTop (for same line letters)
|
||||
const letterHeight =
|
||||
currentLetter?.offsetHeight ||
|
||||
lastWordLetter?.offsetHeight ||
|
||||
(safeNumber(currentLetter?.offsetHeight) ?? 0) ||
|
||||
(safeNumber(lastWordLetter?.offsetHeight) ?? 0) ||
|
||||
Config.fontSize * convertRemToPixels(1);
|
||||
|
||||
const letterPosTop =
|
||||
|
|
|
|||
|
|
@ -780,7 +780,7 @@ export function updateRateQuote(randomQuote: Quote | null): void {
|
|||
|
||||
const userqr =
|
||||
DB.getSnapshot()?.quoteRatings?.[randomQuote.language]?.[randomQuote.id];
|
||||
if (userqr) {
|
||||
if (Numbers.isSafeNumber(userqr)) {
|
||||
$(".pageTest #result #rateQuoteButton .icon")
|
||||
.removeClass("far")
|
||||
.addClass("fas");
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ module.exports = {
|
|||
"@typescript-eslint/no-floating-promises": "error",
|
||||
"@typescript-eslint/strict-boolean-expressions": [
|
||||
"error",
|
||||
{ allowNullableBoolean: true, allowNullableNumber: true },
|
||||
{ allowNullableBoolean: true },
|
||||
],
|
||||
"@typescript-eslint/non-nullable-type-assertion-style": "off",
|
||||
"@typescript-eslint/no-unnecessary-condition": "off",
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { intersect } from "@monkeytype/util/arrays";
|
|||
import { FunboxForcedConfig } from "./types";
|
||||
import { getFunbox } from "./list";
|
||||
import { FunboxName } from "@monkeytype/contracts/schemas/configs";
|
||||
import { safeNumber } from "@monkeytype/util/numbers";
|
||||
|
||||
export function checkCompatibility(
|
||||
funboxNames: FunboxName[],
|
||||
|
|
@ -110,7 +111,8 @@ export function checkCompatibility(
|
|||
.filter((f) => f !== undefined)
|
||||
.flat()
|
||||
.reduce<Record<string, number>>((counts, cssModification) => {
|
||||
counts[cssModification] = (counts[cssModification] || 0) + 1;
|
||||
counts[cssModification] =
|
||||
(safeNumber(counts[cssModification]) ?? 0) + 1;
|
||||
return counts;
|
||||
}, {})
|
||||
).every((c) => c <= 1);
|
||||
|
|
|
|||
|
|
@ -97,4 +97,60 @@ describe("numbers", () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
describe("isSafeNumber", () => {
|
||||
describe("should correctly identify safe numbers", () => {
|
||||
const testCases = [
|
||||
//safe
|
||||
{ input: 0, expected: true },
|
||||
{ input: 1, expected: true },
|
||||
{ input: -1, expected: true },
|
||||
{ input: 0.5, expected: true },
|
||||
{ input: -0.5, expected: true },
|
||||
//not safe
|
||||
{ input: NaN, expected: false },
|
||||
{ input: Infinity, expected: false },
|
||||
{ input: -Infinity, expected: false },
|
||||
{ input: "string", expected: false },
|
||||
{ input: null, expected: false },
|
||||
{ input: undefined, expected: false },
|
||||
{ input: true, expected: false },
|
||||
{ input: false, expected: false },
|
||||
];
|
||||
|
||||
it.for(testCases)(
|
||||
"should return $expected for $input",
|
||||
({ input, expected }) => {
|
||||
expect(Numbers.isSafeNumber(input)).toEqual(expected);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("safeNumber", () => {
|
||||
describe("should correctly identify safe numbers", () => {
|
||||
const testCases = [
|
||||
//safe
|
||||
{ input: 0, expected: 0 },
|
||||
{ input: 1, expected: 1 },
|
||||
{ input: -1, expected: -1 },
|
||||
{ input: 0.5, expected: 0.5 },
|
||||
{ input: -0.5, expected: -0.5 },
|
||||
//not safe
|
||||
{ input: NaN, expected: undefined },
|
||||
{ input: Infinity, expected: undefined },
|
||||
{ input: -Infinity, expected: undefined },
|
||||
{ input: "string", expected: undefined },
|
||||
{ input: null, expected: undefined },
|
||||
{ input: undefined, expected: undefined },
|
||||
{ input: true, expected: undefined },
|
||||
{ input: false, expected: undefined },
|
||||
];
|
||||
|
||||
it.for(testCases)(
|
||||
"should return $expected for $input",
|
||||
({ input, expected }) => {
|
||||
expect(Numbers.safeNumber(input as number)).toEqual(expected);
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -125,3 +125,29 @@ export function mapRange(
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a value is a safe number. Safe numbers are finite and not NaN.
|
||||
* @param value The value to check.
|
||||
* @returns True if the value is a safe number, false otherwise.
|
||||
*/
|
||||
export function isSafeNumber(value: unknown): value is number {
|
||||
if (typeof value === "number") {
|
||||
return !isNaN(value) && isFinite(value);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a number to a safe number or undefined. NaN, Infinity, and -Infinity are converted to undefined.
|
||||
* @param value The value to convert.
|
||||
* @returns The input number if it is safe, undefined otherwise.
|
||||
*/
|
||||
export function safeNumber(
|
||||
value: number | undefined | null
|
||||
): number | undefined {
|
||||
if (isSafeNumber(value)) {
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue