Skip to content

Commit

Permalink
Add init. impl. of NumberBaseConversion (#72)
Browse files Browse the repository at this point in the history
* Add init. impl. of `NumberBaseConversion`

* Add tests for the component page

* Run lint and fmt, code cleanup

* Fix test case

* Prefix octal with `0`

* Fix test assertions in octal results

* Fix lib test in octal results

* Reimplement `convertNumberBase` function

* Rename convertBase, shorten impl., better naming
  • Loading branch information
7sne authored Jul 18, 2022
1 parent f3c8dbf commit 6e751a5
Show file tree
Hide file tree
Showing 11 changed files with 383 additions and 5 deletions.
133 changes: 133 additions & 0 deletions pages/base-conversion.page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { ReactElement, useState } from 'react';
import { SafeParseError, SafeParseReturnType } from 'zod';

import { ConversionInput } from '../src/components/ConversionInput';
import { CalculatorIcon } from '../src/components/icons/CalculatorIcon';
import { ToolContainer } from '../src/components/ToolContainer';
import { ToolHeader } from '../src/components/ToolHeader';
import { convertBase } from '../src/lib/convertBase';
import { Base, base } from '../src/lib/convertBaseProperties';
import { binarySchema } from '../src/misc/schemas/binarySchema';
import { hexSchema } from '../src/misc/schemas/hexSchema';
import { octalSchema } from '../src/misc/schemas/octalSchema';
import { unitSchema } from '../src/misc/schemas/unitSchema';
import { WithError } from '../src/misc/types';
import { zodResultMessage } from '../src/misc/zodResultMessage';

interface BaseConversionState extends Record<Base, WithError<string>> {}

const initialState: BaseConversionState = {
binary: { value: '' },
octal: { value: '' },
decimal: { value: '' },
hexadecimal: { value: '' },
};

export default function NumberBaseConversion(): ReactElement {
const [state, setState] = useState<BaseConversionState>(initialState);

function handleChangeValue(newValue: string, currentType: Base): void {
let parseResult: SafeParseReturnType<string, string> | undefined;
switch (currentType) {
case 'binary':
parseResult = binarySchema.safeParse(newValue);
break;
case 'octal':
parseResult = octalSchema.safeParse(newValue);
break;
case 'decimal':
parseResult = unitSchema.safeParse(newValue);
break;
case 'hexadecimal':
parseResult = hexSchema.safeParse(newValue);
break;
default:
// this won't happen
break;
}
if (parseResult && !parseResult.success) {
setState((state) => ({
...state,
[currentType]: {
value: newValue,
error: zodResultMessage(parseResult as SafeParseError<unknown>),
},
}));
// return here to not override error set above
return;
}
setState((oldState) => {
const newState: BaseConversionState = {
...oldState,
[currentType]: { value: newValue },
};
return newState;
});
setState((oldState) => {
const newState = { ...oldState };

for (const unit of base) {
if (unit === currentType) {
newState[unit] = { value: newValue };
} else {
const convertedValue = convertBase(newValue, currentType, unit);
if (convertedValue && !isNaN(parseInt(convertedValue))) {
newState[unit] = { value: convertedValue };
}
}
}
return newState;
});
}

return (
<ToolContainer>
<form className="mr-auto flex w-full flex-col items-start sm:items-center md:items-start">
<ToolHeader
icon={<CalculatorIcon height={24} width={24} />}
text={['Calculators', 'Number Base Conversion']}
/>
<section className="flex w-full flex-col gap-5">
<ConversionInputs
handleChangeValue={handleChangeValue}
state={state}
/>
</section>
</form>
</ToolContainer>
);
}
interface ConversionInputsProps {
state: BaseConversionState;
handleChangeValue: (newValue: string, currentType: Base) => void;
}

function ConversionInputs({
state,
handleChangeValue,
}: ConversionInputsProps): JSX.Element {
return (
<>
<ConversionInput
name="Binary"
{...state['binary']}
onChange={(e) => handleChangeValue(e.target.value, 'binary')}
/>
<ConversionInput
name="Octal"
{...state['octal']}
onChange={(e) => handleChangeValue(e.target.value, 'octal')}
/>
<ConversionInput
name="Decimal"
{...state['decimal']}
onChange={(e) => handleChangeValue(e.target.value, 'decimal')}
/>
<ConversionInput
name="Hexadecimal"
{...state['hexadecimal']}
onChange={(e) => handleChangeValue(e.target.value, 'hexadecimal')}
/>
</>
);
}
129 changes: 129 additions & 0 deletions pages/base-conversion.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { fireEvent, render } from '@testing-library/react';
import { expect } from 'earljs';

import NumberBaseConversion from './base-conversion.page';

describe(NumberBaseConversion.name, () => {
it('sets binary and gets a correct value in other fields', async () => {
const root = render(<NumberBaseConversion />);
const binaryField = (await root.findByLabelText(
/binary/i,
)) as HTMLInputElement;
const octalField = (await root.findByLabelText(
/Octal/i,
)) as HTMLInputElement;
const hexadecimalField = (await root.findByLabelText(
/Hexadecimal/i,
)) as HTMLInputElement;
const decimalField = (
await root.findAllByLabelText(/Decimal/i)
)[0] as HTMLInputElement;

fireEvent.change(binaryField, { target: { value: '10100101000001' } });

expect(binaryField.value).toEqual('10100101000001');
expect(octalField.value).toEqual('024501');
expect(hexadecimalField.value).toEqual('0x2941');
expect(decimalField.value).toEqual('10561');
});

it('sets hexadecimals and gets a correct value in other fields', async () => {
const root = render(<NumberBaseConversion />);
const hexadecimalField = (await root.findByLabelText(
/Hexadecimal/i,
)) as HTMLInputElement;
const binaryField = (await root.findByLabelText(
/binary/i,
)) as HTMLInputElement;
const decimalField = (
await root.findAllByLabelText(/Decimal/i)
)[0] as HTMLInputElement;
const octalField = (await root.findByLabelText(
/Octal/i,
)) as HTMLInputElement;

fireEvent.change(hexadecimalField, {
target: { value: '0x91923123124a3b331dddddd' },
});

expect(hexadecimalField.value).toEqual('0x91923123124a3b331dddddd');
expect(octalField.value).toEqual('02214443044304445073146167356735');
expect(decimalField.value).toEqual('2815753852291900309296242141');
expect(binaryField.value).toEqual(
'10010001100100100011000100100011000100100100101000111011001100110001110111011101110111011101',
);
});

it('sets hexadecimal, then adds unsupported chars, thus freezes calculation results in the other fields', async () => {
const root = render(<NumberBaseConversion />);
const hexadecimalField = (await root.findByLabelText(
/Hexadecimal/i,
)) as HTMLInputElement;
const binaryField = (await root.findByLabelText(
/binary/i,
)) as HTMLInputElement;
const decimalField = (
await root.findAllByLabelText(/Decimal/i)
)[0] as HTMLInputElement;

fireEvent.change(hexadecimalField, {
target: { value: '0x91923123124a3b331dddddd' },
});

expect(binaryField.value).toEqual(
'10010001100100100011000100100011000100100100101000111011001100110001110111011101110111011101',
);
expect(decimalField.value).toEqual('2815753852291900309296242141');

fireEvent.change(hexadecimalField, {
target: { value: '0x91923123124a3b331ddddddZZZZ' },
});

expect(hexadecimalField.value).toEqual('0x91923123124a3b331ddddddZZZZ');
expect(binaryField.value).toEqual(
'10010001100100100011000100100011000100100100101000111011001100110001110111011101110111011101',
);
expect(decimalField.value).toEqual('2815753852291900309296242141');
});

it('types letters and special signs to one of the fields and error gets displayed', async () => {
const root = render(<NumberBaseConversion />);
const octalField = (await root.findByLabelText(
/octal/i,
)) as HTMLInputElement;
const decimalField = (
await root.findAllByLabelText(/Decimal/i)
)[0] as HTMLInputElement;
const binaryField = (await root.findByLabelText(
/binary/i,
)) as HTMLInputElement;

fireEvent.change(octalField, { target: { value: '140' } });

let errorField = (await root.findAllByRole('alert'))[0];

expect(errorField.innerHTML).toEqual(
expect.stringMatching(
/The value must be a valid, octal number, 0-prefix is required/,
),
);

fireEvent.change(decimalField, { target: { value: '@%' } });

errorField = (await root.findAllByRole('alert'))[1];

expect(errorField.innerHTML).toEqual(
expect.stringMatching(
/The value mustn't contain letters or any special signs except dot/,
),
);

fireEvent.change(binaryField, { target: { value: '123' } });

errorField = (await root.findAllByRole('alert'))[0];

expect(errorField.innerHTML).toEqual(
expect.stringMatching(/The value must be a valid, binary number/),
);
});
});
3 changes: 1 addition & 2 deletions pages/token-unit-conversion.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { ToolHeader } from '../src/components/ToolHeader';
import { convertUnit } from '../src/lib/convertUnits';
import { decodeHex } from '../src/lib/decodeHex';
import { unitSchema } from '../src/misc/schemas/unitSchema';
import { WithError } from '../src/misc/types';

const DEFAULT_DECIMALS = 18;

Expand All @@ -25,8 +26,6 @@ interface UnitTypeExtended {
value: string;
}

type WithError<T> = { value: T; error?: string };

interface TokenUnitConversionState
extends Record<TokenUnitType, WithError<string>> {}

Expand Down
1 change: 1 addition & 0 deletions src/components/ToolTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ const tree: Tree = {
tools: [
{ title: 'Eth Unit Conversion', pageHref: 'eth-unit-conversion' },
{ title: 'Token Unit Conversion', pageHref: 'token-unit-conversion' },
{ title: 'Number Base Conversion', pageHref: 'number-base-conversion' },
],
},
decoders: {
Expand Down
57 changes: 57 additions & 0 deletions src/lib/convertBase.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { expect } from 'earljs';

import { convertBase } from './convertBase';

describe(convertBase.name, () => {
it('converts binary format correctly', () => {
expect(
convertBase(
'100000001011010000001000000010110100000010101000011010000001000000010110100000010101000010000011111110000011010100000101000000100000001011010000000100000010000000101101000000000100000010000000101101000000',
'binary',
'binary',
),
).toEqual(
'100000001011010000001000000010110100000010101000011010000001000000010110100000010101000010000011111110000011010100000101000000100000001011010000000100000010000000101101000000000100000010000000101101000000',
);
expect(
convertBase(
'100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
'binary',
'octal',
),
).toEqual('040000000000000000000000000000000000000000000000000');
expect(
convertBase(
'100000001011010000001000000010110100000010101000011010000001000000010110100000010101000010000011111110000011010100000101000000100000001011010000000100000010000000101101000000000100000010000000101101000000',
'binary',
'decimal',
),
).toEqual('12926134075920727636302982350177090457457998788034925005638464');
expect(
convertBase(
'100010101010101111111111111111010101010111111111111111101010101011111111111111110101010101111111111111111',
'binary',
'hexadecimal',
),
).toEqual(expect.stringMatching(/0x11557FFFAABFFFD55FFFEAAFFFF/i));
});

it('converts decimal format correctly', () => {
expect(
convertBase(
'122.9999999999999999999999999999999999999999999999999999999999000000000001',
'decimal',
'binary',
),
).toEqual('1111011');
expect(
convertBase(
'991213123874903250853278399121312387490325080989888843219798419175239825378532783000000841917523982537853278300000084191752398253785327830000008419175239825378532783000000',
'decimal',
'octal',
),
).toEqual(
'02032451277354503517050671117710665174541362015355755433271161163735017771002766626163316202665376213112150604707401405604661247057377575022164344535612415350312622001401736106151250437704700',
);
});
});
38 changes: 38 additions & 0 deletions src/lib/convertBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import BigNumber from 'bignumber.js';

import { Base } from './convertBaseProperties';

export function convertBase(
value: string,
from: Base,
to: Base,
): string | undefined {
return convertBaseWithBn(value, to, baseNameToBase[from], baseNameToBase[to]);
}

// @internal
const baseNameToBase = {
binary: 2,
octal: 8,
decimal: 10,
hexadecimal: 16,
};

// @internal
const baseNameToPrefix: Partial<Record<Base, string>> = {
octal: '0',
hexadecimal: '0x',
};

// @internal
function convertBaseWithBn(
value: string,
toBaseName: Base,
from: number,
to: number,
): string {
return (
String(baseNameToPrefix[toBaseName] || '') +
new BigNumber(value, from).toString(to)
);
}
3 changes: 3 additions & 0 deletions src/lib/convertBaseProperties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const base = ['binary', 'octal', 'decimal', 'hexadecimal'] as const;

export type Base = typeof base[number];
5 changes: 5 additions & 0 deletions src/misc/schemas/binarySchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { z } from 'zod';

export const binarySchema = z.string().regex(/\b[01]+\b/, {
message: 'The value must be a valid, binary number',
});
Loading

1 comment on commit 6e751a5

@vercel
Copy link

@vercel vercel bot commented on 6e751a5 Jul 18, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.