diff --git a/src/typescript/frontend/src/components/inputs/input-numeric/index.tsx b/src/typescript/frontend/src/components/inputs/input-numeric/index.tsx index db0f627c2..9b862db8a 100644 --- a/src/typescript/frontend/src/components/inputs/input-numeric/index.tsx +++ b/src/typescript/frontend/src/components/inputs/input-numeric/index.tsx @@ -1,44 +1,72 @@ -import React from "react"; -import { Input } from "components/inputs/input"; -import { type InputProps } from "components/inputs/input/types"; +import Big from "big.js"; +import React, { useEffect, useState } from "react"; +import { isNumberInConstruction, countDigitsAfterDecimal, sanitizeNumber } from "@sdk/utils"; -const NUMBERS = new Set("0123456789"); +const intToStr = (value: bigint, decimals?: number) => + (Number(value) / 10 ** (decimals ?? 0)).toString(); -export const InputNumeric = ({ +const strToInt = (value: string, decimals?: number) => { + if (isNaN(parseFloat(value))) { + return 0n; + } + const res = Big(value.toString()).mul(Big(10 ** (decimals ?? 0))); + if (res < Big(1)) { + return 0n; + } + return BigInt(res.toString()); +}; + +export const InputNumeric = ({ onUserInput, + decimals, + value, + onSubmit, ...props -}: InputProps & { onUserInput: (value: string) => void }): JSX.Element => { - const [input, setInput] = React.useState(""); - - const onChangeText = (event: React.ChangeEvent) => { - const value = event.target.value.replace(/,/g, ".").replace(/^0+/, "0"); - - let hasDecimal = false; - let s = ""; - for (const char of value) { - if (char === ".") { - if (!hasDecimal) { - s += char; - } else { - hasDecimal = true; - } - } else if (NUMBERS.has(char)) { - s += char; - } +}: { + className?: string; + onUserInput?: (value: bigint) => void; + onSubmit?: (value: bigint) => void; + decimals?: number; + disabled?: boolean; + value: bigint; +}) => { + const [input, setInput] = useState(intToStr(value, decimals)); + + useEffect(() => { + if (strToInt(input, decimals) != value) { + setInput(intToStr(value, decimals)); + } + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [value, decimals]); + + const onChangeText = (e: React.ChangeEvent) => { + const value = sanitizeNumber(e.target.value); + + if (!isNumberInConstruction(value)) { + return; } - if (s === "" || !isNaN(Number(s))) { - setInput(s); - onUserInput(s); + const decimalsInValue = countDigitsAfterDecimal(value); + if (typeof decimals === "number" && decimalsInValue > decimals) { + return; + } + + setInput(value); + if (onUserInput) { + onUserInput(strToInt(value, decimals)); } }; return ( - onChangeText(event)} + onChangeText(e)} value={input} + onKeyDown={(e) => { + if (e.key === "Enter" && onSubmit) { + onSubmit(strToInt(input, decimals)); + } + }} {...props} /> ); diff --git a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx index 65482e972..66d42b186 100644 --- a/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx +++ b/src/typescript/frontend/src/components/pages/emojicoin/components/trade-emojicoin/SwapComponent.tsx @@ -5,7 +5,6 @@ import { type PropsWithChildren, useEffect, useState, - useCallback, useMemo, type MouseEventHandler, } from "react"; @@ -26,6 +25,7 @@ import { toCoinTypes } from "@sdk/markets/utils"; import { Flex, FlexGap } from "@containers"; import Popup from "components/popup"; import { Text } from "components/text"; +import { InputNumeric } from "components/inputs"; const SmallButton = ({ emoji, @@ -79,8 +79,7 @@ const inputAndOutputStyles = ` border-transparent !p-0 text-white `; -const APT_DISPLAY_DECIMALS = 4; -const EMOJICOIN_DISPLAY_DECIMALS = 1; +const OUTPUT_DISPLAY_DECIMALS = 4; const SWAP_GAS_COST = 52500n; export default function SwapComponent({ @@ -103,7 +102,7 @@ export default function SwapComponent({ const [inputAmount, setInputAmount] = useState( toActualCoinDecimals({ num: presetInputAmountIsValid ? presetInputAmount! : "1" }) ); - const [outputAmount, setOutputAmount] = useState("0"); + const [outputAmount, setOutputAmount] = useState(0n); const [previous, setPrevious] = useState(inputAmount); const [isLoading, setIsLoading] = useState(false); const [isSell, setIsSell] = useState(!(searchParams.get("sell") === null)); @@ -125,15 +124,18 @@ export default function SwapComponent({ const swapResult = useSimulateSwap({ marketAddress, - inputAmount: inputAmount === "" ? "0" : inputAmount, + inputAmount: inputAmount.toString(), isSell, numSwaps, }); + const outputAmountString = toDisplayCoinDecimals({ + num: isLoading ? previous : outputAmount, + decimals: OUTPUT_DISPLAY_DECIMALS, + }); + const { ref, replay } = useScramble({ - text: Number(isLoading ? previous : outputAmount).toFixed( - isSell ? APT_DISPLAY_DECIMALS : EMOJICOIN_DISPLAY_DECIMALS - ), + text: new Intl.NumberFormat().format(Number(outputAmountString)), overdrive: false, overflow: true, speed: isLoading ? 0.4 : 1000, @@ -151,52 +153,19 @@ export default function SwapComponent({ setIsLoading(true); return; } - const swapResultDisplay = toDisplayNumber(swapResult, isSell ? "apt" : "emoji"); - setPrevious(swapResultDisplay); - setOutputAmount(swapResultDisplay); + setPrevious(swapResult); + setOutputAmount(swapResult); setIsLoading(false); replay(); }, [swapResult, replay, isSell]); - const toDisplayNumber = (value: bigint | number | string, type: "apt" | "emoji" = "apt") => { - const badString = typeof value === "string" && (value === "" || isNaN(parseInt(value))); - if (!value || badString) { - return "0"; - } - // We use the APT display decimal amount here to avoid early truncation. - return toDisplayCoinDecimals({ - num: value, - decimals: type === "apt" ? APT_DISPLAY_DECIMALS : EMOJICOIN_DISPLAY_DECIMALS, - }).toString(); - }; - - const handleInput = (e: React.ChangeEvent) => { - if (e.target.value === "") { - setInputAmount(""); - } - if (isNaN(parseFloat(e.target.value))) { - e.stopPropagation(); - return; - } - setInputAmount(toActualCoinDecimals({ num: e.target.value })); - }; - - const handleKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (e.key === "Enter" && submit) { - submit(); - } - }, - [submit] - ); - const sufficientBalance = useMemo(() => { if (!account || (isSell && !emojicoinBalance) || (!isSell && !aptBalance)) return false; if (account) { if (isSell) { - return emojicoinBalance >= BigInt(inputAmount); + return emojicoinBalance >= inputAmount; } - return aptBalance >= BigInt(inputAmount); + return aptBalance >= inputAmount; } }, [account, aptBalance, emojicoinBalance, isSell, inputAmount]); @@ -205,7 +174,7 @@ export default function SwapComponent({ const coinBalance = isSell ? emojicoinBalance : aptBalance; const balance = toDisplayCoinDecimals({ num: coinBalance, - decimals: !isSell ? APT_DISPLAY_DECIMALS : EMOJICOIN_DISPLAY_DECIMALS, + decimals: 4, }); return ( @@ -235,12 +204,16 @@ export default function SwapComponent({ setInputAmount(String(emojicoinBalance / 2n))} + onClick={() => { + setInputAmount(emojicoinBalance / 2n); + }} /> setInputAmount(String(emojicoinBalance))} + onClick={() => { + setInputAmount(emojicoinBalance); + }} /> ) : ( @@ -248,17 +221,23 @@ export default function SwapComponent({ setInputAmount(String(availableAptBalance / 4n))} + onClick={() => { + setInputAmount(availableAptBalance / 4n); + }} /> setInputAmount(String(availableAptBalance / 2n))} + onClick={() => { + setInputAmount(availableAptBalance / 2n); + }} /> setInputAmount(String(availableAptBalance))} + onClick={() => { + setInputAmount(availableAptBalance); + }} /> )} @@ -272,25 +251,23 @@ export default function SwapComponent({ {isSell ? t("You sell") : t("You pay")} {balanceLabel} - + value={inputAmount} + onUserInput={(v) => setInputAmount(v)} + onSubmit={() => (submit ? submit() : {})} + decimals={8} + /> {isSell ? : } { - setInputAmount(toActualCoinDecimals({ num: outputAmount })); + setInputAmount(outputAmount); + // This is done as to not display an old value if the swap simulation fails. + setOutputAmount(0n); + setPrevious(0n); setIsSell((v) => !v); }} /> diff --git a/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx b/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx index 39655b42c..84ef50b4b 100644 --- a/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx +++ b/src/typescript/frontend/src/components/pages/pools/components/liquidity/index.tsx @@ -4,7 +4,7 @@ import React, { type PropsWithChildren, useEffect, useMemo, useState } from "rea import { useThemeContext } from "context"; import { translationFunction } from "context/language-context"; import { Flex, Column, FlexGap } from "@containers"; -import { Text, Button } from "components"; +import { Text, Button, InputNumeric } from "components"; import { StyledAddLiquidityWrapper } from "./styled"; import { ProvideLiquidity, RemoveLiquidity } from "@sdk/emojicoin_dot_fun/emojicoin-dot-fun"; import { toCoinDecimalString } from "lib/utils/decimals"; @@ -41,15 +41,6 @@ const fmtCoin = (n: AnyNumberString | undefined) => { return new Intl.NumberFormat().format(Number(toCoinDecimalString(n, 8))); }; -const unfmtCoin = (n: AnyNumberString) => { - return BigInt( - toActualCoinDecimals({ - num: typeof n === "bigint" ? n : Number(n), - decimals: 0, - }) - ); -}; - const InnerWrapper = ({ children, id, @@ -82,18 +73,27 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { const { theme } = useThemeContext(); const searchParams = useSearchParams(); + const presetInputAmount = searchParams.get("add") !== null ? searchParams.get("add") : searchParams.get("remove"); const presetInputAmountIsValid = presetInputAmount !== null && presetInputAmount !== "" && !Number.isNaN(Number(presetInputAmount)); - const [liquidity, setLiquidity] = useState( - searchParams.get("add") !== null && presetInputAmountIsValid ? Number(presetInputAmount) : "" + + const [liquidity, setLiquidity] = useState( + toActualCoinDecimals({ + num: searchParams.get("add") !== null && presetInputAmountIsValid ? presetInputAmount! : "1", + }) ); - const [lp, setLP] = useState( - searchParams.get("remove") !== null && presetInputAmountIsValid ? Number(presetInputAmount) : "" + + const [lp, setLP] = useState( + toActualCoinDecimals({ + num: + searchParams.get("remove") !== null && presetInputAmountIsValid ? presetInputAmount! : "1", + }) ); + const [direction, setDirection] = useState<"add" | "remove">( searchParams.get("remove") !== null ? "remove" : "add" ); @@ -113,21 +113,19 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { const provideLiquidityResult = useSimulateProvideLiquidity({ marketAddress: market?.market.marketAddress, - quoteAmount: unfmtCoin(liquidity ?? 0), + quoteAmount: liquidity ?? 0, }); const { emojicoin } = market ? toCoinTypes(market?.market.marketAddress) : { emojicoin: "" }; const removeLiquidityResult = useSimulateRemoveLiquidity({ marketAddress: market?.market.marketAddress, - lpCoinAmount: unfmtCoin(lp ?? 0), + lpCoinAmount: lp ?? 0, typeTags: [emojicoin ?? ""], }); const enoughApt = - direction === "add" - ? aptBalance !== undefined && aptBalance >= unfmtCoin(liquidity ?? 0) - : true; + direction === "add" ? aptBalance !== undefined && aptBalance >= (liquidity ?? 0) : true; const enoughEmoji = direction === "add" ? emojicoinBalance !== undefined && @@ -135,7 +133,7 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { : true; const enoughEmojiLP = direction === "remove" - ? emojicoinLPBalance !== undefined && emojicoinLPBalance >= unfmtCoin(lp ?? 0) + ? emojicoinLPBalance !== undefined && emojicoinLPBalance >= (lp ?? 0) : true; useEffect(() => { @@ -157,7 +155,7 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { const isActionPossible = market !== undefined && - (direction === "add" ? liquidity !== "" : lp !== "") && + (direction === "add" ? liquidity !== 0n : lp !== 0n) && enoughApt && enoughEmoji && enoughEmojiLP; @@ -175,22 +173,20 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { {fmtCoin(aptBalance)} {")"} - setLiquidity(e.target.value === "" ? "" : Number(e.target.value))} - style={{ - color: direction === "remove" ? theme.colors.lightGray + "99" : "white", - }} - min={0} - step={0.01} - type={direction === "add" ? "number" : "text"} - disabled={direction === "remove"} - value={ - direction === "add" - ? liquidity - : (fmtCoin(removeLiquidityResult?.quote_amount) ?? "...") - } - > + {direction === "add" ? ( + setLiquidity(e)} + value={liquidity} + decimals={8} + /> + ) : ( + + )} @@ -217,7 +213,6 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { ? (fmtCoin(provideLiquidityResult?.base_amount) ?? "...") : (fmtCoin(removeLiquidityResult?.base_amount) ?? "...") } - type="text" disabled > @@ -236,18 +231,20 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { {")"} - setLP(e.target.value === "" ? "" : Number(e.target.value))} - disabled={direction === "add"} - > + {direction === "add" ? ( + + ) : ( + setLP(e)} + value={lp} + decimals={8} + /> + )} @@ -318,7 +315,7 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { aptosConfig: aptos.config, provider: account.address, marketAddress: market!.market.marketAddress, - quoteAmount: unfmtCoin(liquidity ?? 0), + quoteAmount: liquidity ?? 0, typeTags: [emojicoin, emojicoinLP], minLpCoinsOut: 1n, }); @@ -328,7 +325,7 @@ const Liquidity: React.FC = ({ market, geoblocked }) => { aptosConfig: aptos.config, provider: account.address, marketAddress: market!.market.marketAddress, - lpCoinAmount: unfmtCoin(lp), + lpCoinAmount: lp, typeTags: [emojicoin, emojicoinLP], minQuoteOut: 1n, }); diff --git a/src/typescript/frontend/src/lib/utils/decimals.ts b/src/typescript/frontend/src/lib/utils/decimals.ts index 3ed1bb4f0..341f428bc 100644 --- a/src/typescript/frontend/src/lib/utils/decimals.ts +++ b/src/typescript/frontend/src/lib/utils/decimals.ts @@ -20,26 +20,12 @@ export const toCoinDecimalString = (num: AnyNumberString, displayDecimals?: numb * @example * 1 APT => 100000000 */ -const toActualCoinDecimals = ({ - num, - round, - decimals, -}: { - num: AnyNumberString; - round?: number; - decimals?: number; -}): string => { +const toActualCoinDecimals = ({ num }: { num: AnyNumberString }): bigint => { if (typeof num === "string" && isNaN(parseFloat(num))) { - return "0"; - } - let res = Big(num.toString()).mul(Big(10 ** DECIMALS)); - if (typeof round !== "undefined") { - res = res.round(round); + return 0n; } - if (typeof decimals !== "undefined") { - return res.toFixed(decimals).toString(); - } - return res.toString(); + const res = Big(num.toString()).mul(Big(10 ** DECIMALS)); + return BigInt(res.toString()); }; /** @@ -68,7 +54,10 @@ const toDisplayCoinDecimals = ({ res = res.round(round); } if (typeof decimals !== "undefined") { - return res.toFixed(decimals).toString(); + if (res < Big(1)) { + return res.toPrecision(decimals).replace(/\.?0+$/, ""); + } + return res.toFixed(decimals).replace(/\.?0+$/, ""); } return res.toString(); }; diff --git a/src/typescript/frontend/tests/e2e/market-order.spec.ts b/src/typescript/frontend/tests/e2e/market-order.spec.ts index 687801dbc..8c00837a0 100644 --- a/src/typescript/frontend/tests/e2e/market-order.spec.ts +++ b/src/typescript/frontend/tests/e2e/market-order.spec.ts @@ -7,16 +7,16 @@ test("check sorting order", async ({ page }) => { const user = getFundedAccount("777"); const rat = SYMBOL_EMOJI_DATA.byName("rat")!.emoji; const emojis = ["cat", "dog", "eagle", "sauropod"]; - const markets = emojis.map(e => [rat, SYMBOL_EMOJI_DATA.byName(e)!.emoji]) + const markets = emojis.map((e) => [rat, SYMBOL_EMOJI_DATA.byName(e)!.emoji]); const client = new EmojicoinClient(); // Register markets. // They all start with rat to simplify the search. for (let i = 0; i < markets.length; i++) { - await client.register(user, markets[i]).then(res => res.handle); - const amount = 1n * ONE_APT_BIGINT / 100n * BigInt(10 ** (markets.length - i)); - await client.buy(user, markets[i], amount).then(res => res.handle); + await client.register(user, markets[i]).then((res) => res.handle); + const amount = ((1n * ONE_APT_BIGINT) / 100n) * BigInt(10 ** (markets.length - i)); + await client.buy(user, markets[i], amount).then((res) => res.handle); } await page.goto("/home"); @@ -51,14 +51,14 @@ test("check sorting order", async ({ page }) => { await filters.click(); // Expect the sort by daily volume button to be visible. - const dailyVolume = page.locator('#emoji-grid-header').getByText('24h Volume'); + const dailyVolume = page.locator("#emoji-grid-header").getByText("24h Volume"); expect(dailyVolume).toBeVisible(); // Sort by daily volume. await dailyVolume.click(); - const names = emojis.map(e => `rat,${e}`); - const patterns = names.map(e => new RegExp(e)); + const names = emojis.map((e) => `rat,${e}`); + const patterns = names.map((e) => new RegExp(e)); // Expect the markets to be in order of daily volume. marketGridItems = page.locator("#emoji-grid a").getByTitle(/RAT,/, { exact: true }); @@ -68,7 +68,7 @@ test("check sorting order", async ({ page }) => { await filters.click(); // Expect the sort by bump order button to be visible. - const bumpOrder = page.locator('#emoji-grid-header').getByText('Bump Order'); + const bumpOrder = page.locator("#emoji-grid-header").getByText("Bump Order"); expect(bumpOrder).toBeVisible(); // Sort by bump order. diff --git a/src/typescript/frontend/tests/e2e/search.spec.ts b/src/typescript/frontend/tests/e2e/search.spec.ts index 5324b475a..bac4615dd 100644 --- a/src/typescript/frontend/tests/e2e/search.spec.ts +++ b/src/typescript/frontend/tests/e2e/search.spec.ts @@ -6,7 +6,7 @@ import { sleep, SYMBOL_EMOJI_DATA } from "../../../sdk/src"; test("check search results", async ({ page }) => { const user = getFundedAccount("666"); const cat = SYMBOL_EMOJI_DATA.byName("cat")!.emoji; - const symbols = [cat,cat]; + const symbols = [cat, cat]; const client = new EmojicoinClient(); await client.register(user, symbols).then((res) => res.handle); @@ -32,11 +32,11 @@ test("check search results", async ({ page }) => { expect(emojiSearchCatButton).toBeVisible(); // Search for the cat,cat market. - await emojiSearchCatButton.click({force: true}); + await emojiSearchCatButton.click({ force: true }); emojiSearchCatButton = picker.getByLabel(cat).first(); expect(emojiSearchCatButton).toBeVisible(); - await emojiSearchCatButton.click({force: true}); + await emojiSearchCatButton.click({ force: true }); // Click on the cat,cat market. const marketCard = page.getByText("cat,cat", { exact: true }); diff --git a/src/typescript/sdk/src/utils/index.ts b/src/typescript/sdk/src/utils/index.ts index 9807ed61d..b3ec761ed 100644 --- a/src/typescript/sdk/src/utils/index.ts +++ b/src/typescript/sdk/src/utils/index.ts @@ -3,3 +3,4 @@ export * from "./hex"; export * from "./misc"; export * from "./type-tags"; export * from "./compare-bigint"; +export * from "./validation"; diff --git a/src/typescript/sdk/src/utils/validation.ts b/src/typescript/sdk/src/utils/validation.ts new file mode 100644 index 000000000..3bcd0386d --- /dev/null +++ b/src/typescript/sdk/src/utils/validation.ts @@ -0,0 +1,62 @@ +/** + * Removes all useless leading zeros. + * In the case of 0.[0-9]+, the leading zero will not be removed. + * @param {string} input - The string to process + * @returns {string} The processed string + */ +export const trimLeadingZeros = (input: string) => { + // Replace all leading zeros with one zero + input = input.replace(/^0+/, "0"); // The regex matches all leading 0s. + + if (input.startsWith("0") && !input.startsWith("0.") && input.length > 1) { + input = input.slice(1); + } + + return input; +}; + +/** + * Removes all leading zeros and transforms "," into ".". + * .[0-9]+ will be replaced with 0.[0-9]+ + * @param {string} input - The string to process + * @returns {string} The processed string + */ +export const sanitizeNumber = (input: string) => { + input = trimLeadingZeros(input.replace(/,/, ".")); + if (input.startsWith(".")) { + return `0${input}`; + } + return input; +}; + +/** + * Checks if the input is a number in construction. + * + * This facilitates using temporarily invalid numbers that will eventually be valid- aka, they are + * numbers in construction. + * + * For example, to input 0.001, you need to input "0", then "0." (invalid), + * which should be allowed to reach "0.001". + * + * Valid examples: + * - 0.1 + * - 0 + * - 0.000 + * - .0 + * - 0. + * + * Invalid examples: + * - 0.0.1 + * + * @param {string} input - The string to test + * @returns {boolean} True if the input is a number in construction + */ +export const isNumberInConstruction = (input: string) => /^[0-9]*(\.([0-9]*)?)?$/.test(input); + +/** + * Counts the number of digits after the decimal point in a string number. + * @param {string} input - The numeric string to analyze (e.g., "123.456") + * @returns {number} The count of digits after the decimal point (e.g., "123.456" returns 3) + */ +export const countDigitsAfterDecimal = (input: string) => + /\./.test(input) ? input.split(".")[1].length : 0; diff --git a/src/typescript/sdk/tests/unit/validation.test.ts b/src/typescript/sdk/tests/unit/validation.test.ts new file mode 100644 index 000000000..bc14f1666 --- /dev/null +++ b/src/typescript/sdk/tests/unit/validation.test.ts @@ -0,0 +1,84 @@ +import * as validation from "../../src/utils/validation"; + +describe("validation utility functions", () => { + it("should trim leading zeros", () => { + const givenAndExpected = [ + ["00", "0"], + ["00000000", "0"], + ["01", "1"], + ["001", "1"], + ["000000001", "1"], + ["00.1", "0.1"], + ["00000000.1", "0.1"], + ["01.1", "1.1"], + ["001.1", "1.1"], + ["000000001.1", "1.1"], + ["0.1", "0.1"], + ["10", "10"], + ["100", "100"], + ]; + + givenAndExpected.forEach(([given, expected]) => { + expect(validation.trimLeadingZeros(given)).toEqual(expected); + }); + }); + + it("should sanitize number", () => { + const givenAndExpected = [ + [",1", "0.1"], + [",0", "0.0"], + ["0,1", "0.1"], + ["0,0", "0.0"], + [",100", "0.100"], + [",000", "0.000"], + ["0000,0", "0.0"], + ["0000,001", "0.001"], + ]; + + givenAndExpected.forEach(([given, expected]) => { + expect(validation.sanitizeNumber(given)).toEqual(expected); + }); + }); + + it("should accurately test if number is in construction", () => { + const givenAndExpected: [string, boolean][] = [ + [".0", true], + [".1", true], + ["0.0", true], + ["0.1", true], + ["1.0", true], + ["1.1", true], + ["0", true], + ["0.000", true], + ["0.", true], + ["1.", true], + ["0.0.1", false], + ["0.abc", false], + ["abc.0", false], + ]; + + givenAndExpected.forEach(([given, expected]) => { + expect(validation.isNumberInConstruction(given)).toEqual(expected); + }); + }); + + it("should accurately return the number of decimals", () => { + const givenAndExpected: [string, number][] = [ + ["0", 0], + ["0.", 0], + [".0", 1], + ["0.0", 1], + ["0.00", 2], + ["00.00", 2], + ["000.00", 2], + ["0.0001", 4], + [".0001", 4], + ["54423536.23466245", 8], + [".23466245", 8], + ]; + + givenAndExpected.forEach(([given, expected]) => { + expect(validation.countDigitsAfterDecimal(given)).toEqual(expected); + }); + }); +});