feat: add number base converter

This commit is contained in:
rusconn
2022-03-27 23:34:58 +09:00
parent 20df0b8c19
commit 96e019deba
14 changed files with 273 additions and 5 deletions

View File

@@ -7,7 +7,7 @@ A web clone of [DevToys](https://github.com/veler/DevToys)
- [x] Add site layout
- [x] Add all tools page mock
- [ ] Implement tools
- [ ] Converters
- [x] Converters
- [ ] Encoders / Decoders
- [ ] Formatters
- [ ] Generators

View File

@@ -25,6 +25,7 @@
"@mui/material": "^5.5.1",
"ace-builds": "^1.4.14",
"fast-deep-equal": "^3.1.3",
"fp-ts": "^2.11.9",
"fuse.js": "^6.5.3",
"next": "12.1.0",
"react": "17.0.2",

View File

@@ -0,0 +1,18 @@
import { TextField } from "@mui/material";
import { ComponentPropsWithoutRef, memo } from "react";
type Props = ComponentPropsWithoutRef<typeof TextField>;
const StyledComponent = (props: Props) => (
<TextField
fullWidth
hiddenLabel
size="small"
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
/>
);
export const Component = memo(StyledComponent);
export default Component;

View File

@@ -2,5 +2,6 @@ import CodeEditorHalfLoading from "./CodeEditorHalfLoading";
import Configurations from "./Configurations";
import Main from "./Main";
import MainItem from "./MainItem";
import TextField from "./TextField";
export { CodeEditorHalfLoading, Configurations, Main, MainItem };
export { CodeEditorHalfLoading, Configurations, Main, MainItem, TextField };

View File

@@ -53,7 +53,12 @@ const toolGroups = [
href: pagesPath.converters.json_yaml.$url(),
disabled: false,
},
{ icon: <Numbers />, title: "Number Base", href: pagesPath.$url(), disabled: true },
{
icon: <Numbers />,
title: "Number Base",
href: pagesPath.converters.number_base.$url(),
disabled: false,
},
],
},
{

View File

@@ -0,0 +1,36 @@
import { AutoFixHigh } from "@mui/icons-material";
import { FormControlLabel, Switch } from "@mui/material";
import { SwitchBaseProps } from "@mui/material/internal/SwitchBase";
import { memo } from "react";
import { Configurations } from "@/components/common";
type SwitchChecked = NonNullable<SwitchBaseProps["checked"]>;
type OnSwitchChange = NonNullable<SwitchBaseProps["onChange"]>;
type Props = {
format: SwitchChecked;
onFormatChange: OnSwitchChange;
};
const StyledComponent = ({ format, onFormatChange }: Props) => (
<Configurations
configurations={[
{
icon: <AutoFixHigh />,
title: "Format number",
input: (
<FormControlLabel
labelPlacement="start"
label={format ? "On" : "Off"}
control={<Switch checked={format} onChange={onFormatChange} />}
/>
),
},
]}
/>
);
export const Component = memo(StyledComponent);
export default Component;

View File

@@ -0,0 +1,145 @@
import { Stack } from "@mui/material";
import { ComponentPropsWithoutRef, memo, useCallback, useState } from "react";
import { Main, MainItem, TextField } from "@/components/common";
import {
formatBinary,
formatDecimal,
formatHexadecimal,
formatOctal,
isBinary,
isDecimal,
isHexadecimal,
isOctal,
unformatBinary,
unformatDecimal,
unformatHexadecimal,
unformatOctal,
} from "@/libs/string";
import Configuration from "./Configuration";
type TextFieldValue = ComponentPropsWithoutRef<typeof TextField>["value"];
type OnTextFieldChange = NonNullable<ComponentPropsWithoutRef<typeof TextField>["onChange"]>;
type Props = {
dec: TextFieldValue;
hex: TextFieldValue;
oct: TextFieldValue;
bin: TextFieldValue;
onDecChange: OnTextFieldChange;
onHexChange: OnTextFieldChange;
onOctChange: OnTextFieldChange;
onBinChange: OnTextFieldChange;
} & ComponentPropsWithoutRef<typeof Configuration>;
const StyledComponent = ({
format,
dec,
hex,
oct,
bin,
onFormatChange,
onDecChange,
onHexChange,
onOctChange,
onBinChange,
}: Props) => (
<Main title="Number Base Converter">
<MainItem title="Configuration">
<Configuration {...{ format, onFormatChange }} />
</MainItem>
<Stack spacing={1}>
<MainItem title="Decimal">
<TextField value={dec} onChange={onDecChange} />
</MainItem>
<MainItem title="Hexadecimal">
<TextField value={hex} onChange={onHexChange} />
</MainItem>
<MainItem title="Octal">
<TextField value={oct} onChange={onOctChange} />
</MainItem>
<MainItem title="Binary">
<TextField value={bin} onChange={onBinChange} />
</MainItem>
</Stack>
</Main>
);
export const Component = memo(StyledComponent);
const Container = () => {
const [format, setFormat] = useState(false);
const [int, setInt] = useState<bigint | undefined>(BigInt(42));
const onFormatChange: Props["onFormatChange"] = useCallback((_e, checked) => {
setFormat(checked);
}, []);
const trySetInt = (
value: string,
prefix: string,
validate: (x: string) => boolean,
unformat: (x: string) => string
) => {
if (value === "") {
setInt(undefined);
return;
}
const unformatted = unformat(value);
if (!validate(unformatted)) {
return;
}
const newInt = BigInt(`${prefix}${unformatted}`);
setInt(newInt);
};
const onDecChange: Props["onDecChange"] = useCallback(({ currentTarget: { value } }) => {
trySetInt(value, "", isDecimal, unformatDecimal);
}, []);
const onHexChange: Props["onHexChange"] = useCallback(({ currentTarget: { value } }) => {
trySetInt(value, "0x", isHexadecimal, unformatHexadecimal);
}, []);
const onOctChange: Props["onOctChange"] = useCallback(({ currentTarget: { value } }) => {
trySetInt(value, "0o", isOctal, unformatOctal);
}, []);
const onBinChange: Props["onBinChange"] = useCallback(({ currentTarget: { value } }) => {
trySetInt(value, "0b", isBinary, unformatBinary);
}, []);
const newDec = int?.toString(10) ?? "";
const newHex = int?.toString(16).toUpperCase() ?? "";
const newOct = int?.toString(8) ?? "";
const newBin = int?.toString(2) ?? "";
const dec = format ? formatDecimal(newDec) : newDec;
const hex = format ? formatHexadecimal(newHex) : newHex;
const oct = format ? formatOctal(newOct) : newOct;
const bin = format ? formatBinary(newBin) : newBin;
return (
<Component
{...{
format,
dec,
hex,
oct,
bin,
onFormatChange,
onDecChange,
onHexChange,
onOctChange,
onBinChange,
}}
/>
);
};
export default Container;

View File

@@ -0,0 +1,3 @@
import Content from "./Content";
export { Content };

View File

@@ -39,8 +39,8 @@ const tools = [
title: "Number Base Converter",
description: "Convert numbers from one base to another",
keywords: "number base converter",
href: pagesPath.$url(),
disabled: true,
href: pagesPath.converters.number_base.$url(),
disabled: false,
},
{
icon: <Code />,

View File

@@ -16,5 +16,12 @@ export const theme = createTheme({
disableRipple: true,
},
},
MuiTextField: {
styleOverrides: {
root: {
backgroundColor: "white",
},
},
},
},
});

View File

@@ -6,6 +6,12 @@ export const pagesPath = {
hash: url?.hash,
}),
},
number_base: {
$url: (url?: { hash?: string }) => ({
pathname: "/converters/number-base" as const,
hash: url?.hash,
}),
},
},
$url: (url?: { hash?: string }) => ({ pathname: "/" as const, hash: url?.hash }),
};

34
src/libs/string.ts Normal file
View File

@@ -0,0 +1,34 @@
import { chunksOf, flatten, intersperse, map, reverse } from "fp-ts/Array";
import { pipe } from "fp-ts/function";
const match = (regex: RegExp) => (x: string) => regex.test(x);
export const isDecimal = match(/^[0-9]*$/);
export const isHexadecimal = match(/^[0-9A-F]*$/i);
export const isOctal = match(/^[0-7]*$/);
export const isBinary = match(/^[0-1]*$/);
const formatNumber = (digits: number, sep: string) => (x: string) =>
pipe(
x,
y => y.split(""),
reverse,
chunksOf(digits),
map(reverse),
intersperse([sep]),
reverse,
flatten,
xs => xs.join("")
);
export const formatDecimal = formatNumber(3, ",");
export const formatHexadecimal = formatNumber(4, " ");
export const formatOctal = formatNumber(3, " ");
export const formatBinary = formatNumber(4, " ");
const unformatNumber = (sep: string) => (x: string) => x.replaceAll(sep, "");
export const unformatDecimal = unformatNumber(",");
export const unformatHexadecimal = unformatNumber(" ");
export const unformatOctal = unformatNumber(" ");
export const unformatBinary = unformatNumber(" ");

View File

@@ -0,0 +1,7 @@
import type { NextPage } from "next";
import { Content } from "@/components/pages/converters/number-base";
const Page: NextPage = Content;
export default Page;

View File

@@ -1285,6 +1285,11 @@ flatted@^3.1.0:
resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3"
integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg==
fp-ts@^2.11.9:
version "2.11.9"
resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.11.9.tgz#bbc204e0932954b59c98a282635754a4b624a05e"
integrity sha512-GhYlNKkCOfdjp71ocdtyaQGoqCswEoWDJLRr+2jClnBBq2dnSOtd6QxmJdALq8UhfqCyZZ0f0lxadU4OhwY9nw==
fs.realpath@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"