Browse Source

Fix exponential normalization

Ensure exponents are normalized properly.
master
TheoryOfNekomata 1 year ago
parent
commit
bb5b7403f9
8 changed files with 156 additions and 30 deletions
  1. +1
    -1
      packages/core/src/common.ts
  2. +8
    -4
      packages/core/src/converter.ts
  3. +19
    -1
      packages/core/src/exponent.ts
  4. +1
    -2
      packages/core/src/systems/en-US/common.ts
  5. +18
    -8
      packages/core/src/systems/en-US/stringify.ts
  6. +15
    -1
      packages/core/test/exponent.test.ts
  7. +1
    -1
      packages/core/test/systems/en-US.test.ts
  8. +93
    -12
      packages/example-web/index.html

+ 1
- 1
packages/core/src/common.ts View File

@@ -60,7 +60,7 @@ export interface StringifySystem {
* @param place - The group place.
* @param options - Options to use when creating the group.
*/
makeGroups: (groups: Group[], options?: Record<string, unknown>) => string[];
makeGroups: <T extends object>(groups: Group[], options?: T) => string[];
/**
* Groups a string.
* @param value - The string to group.


+ 8
- 4
packages/core/src/converter.ts View File

@@ -1,5 +1,6 @@
import { enUS } from './systems';
import { StringifySystem } from './common';
import { exponentialToNumberString } from './exponent';

/**
* Negative symbol.
@@ -28,7 +29,7 @@ type ParseResult = typeof ALLOWED_PARSE_RESULT_TYPES[number];
/**
* Options to use when converting a value to a string.
*/
export interface StringifyOptions {
export interface StringifyOptions<TMakeGroupOptions extends object = object> {
/**
* The system to use when converting a value to a string.
*
@@ -38,7 +39,7 @@ export interface StringifyOptions {
/**
* Options to use when making a group. This is used to override the default options for a group.
*/
makeGroupOptions?: Record<string, unknown>;
makeGroupOptions?: TMakeGroupOptions;
}

/**
@@ -66,7 +67,10 @@ export const stringify = (
}

const groups = system.group(valueStr);
const groupNames = system.makeGroups(groups, makeGroupOptions);
const groupNames = system.makeGroups(
groups,
makeGroupOptions,
);
return system.finalize(groupNames);
};

@@ -104,7 +108,7 @@ export const parse = (value: string, options = {} as ParseOptions) => {
// Precision might be lost here. Use bigint when not using fractional parts.
return Number(stringValue);
case 'bigint':
return BigInt(stringValue);
return BigInt(exponentialToNumberString(stringValue));
default:
break;
}


+ 19
- 1
packages/core/src/exponent.ts View File

@@ -100,7 +100,7 @@ interface ExponentialComponents {
* @param options - Options to use when extracting components.
* @returns ExponentialComponents The extracted components.
*/
export const extractExponentialComponents = (
const extractExponentialComponents = (
value: string,
options = {} as NumberToExponentialOptions,
): ExponentialComponents => {
@@ -203,3 +203,21 @@ export const numberToExponential = (

return `${significandInteger}${decimalPoint}${significandFractional}${exponentDelimiter}${exponent}`;
};

export const exponentialToNumberString = (
exp: string,
options = {} as NumberToExponentialOptions,
) => {
const { decimalPoint = DEFAULT_DECIMAL_POINT } = options;
const { integer, fractional, exponent } = extractExponentialComponents(exp, options);
const currentDecimalPointIndex = integer.length;
const newDecimalPointIndex = currentDecimalPointIndex + Number(exponent);
const fractionalDigits = fractional.length > 0 ? fractional : '0';
const significantDigits = `${integer}${fractionalDigits}`;
const newInteger = significantDigits.slice(0, newDecimalPointIndex).padEnd(newDecimalPointIndex, '0');
const newFractional = significantDigits.slice(newDecimalPointIndex).replace(/0+$/g, '');
if (newFractional.length === 0) {
return newInteger;
}
return `${newInteger}${decimalPoint}${newFractional}`;
};

+ 1
- 2
packages/core/src/systems/en-US/common.ts View File

@@ -8,8 +8,7 @@ export const NEGATIVE = 'negative' as const;

export const NEGATIVE_SYMBOL = '-' as const;

// replace with hyphen with option
export const TENS_ONES_SEPARATOR = ' ' as const;
export const TENS_ONES_SEPARATOR = '-' as const;

export const POSITIVE_SYMBOL = '+' as const;



+ 18
- 8
packages/core/src/systems/en-US/stringify.ts View File

@@ -36,9 +36,10 @@ import {
* Builds a name for numbers in tens and ones.
* @param tens - Tens digit.
* @param ones - Ones digit.
* @param addTensDashes - Whether to add dashes between the tens and ones.
* @returns string The name for the number.
*/
const makeTensName = (tens: number, ones: number) => {
const makeTensName = (tens: number, ones: number, addTensDashes = false) => {
if (tens === 0) {
return ONES[ones];
}
@@ -51,7 +52,7 @@ const makeTensName = (tens: number, ones: number) => {
return TENS[tens];
}

return `${TENS[tens] as Exclude<TensName, 'zero' | 'ten'>}${TENS_ONES_SEPARATOR}${ONES[ones] as Exclude<OnesName, 'zero'>}` as const;
return `${TENS[tens] as Exclude<TensName, 'zero' | 'ten'>}${addTensDashes ? TENS_ONES_SEPARATOR : ' '}${ONES[ones] as Exclude<OnesName, 'zero'>}` as const;
};

/**
@@ -59,18 +60,19 @@ const makeTensName = (tens: number, ones: number) => {
* @param hundreds - Hundreds digit.
* @param tens - Tens digit.
* @param ones - Ones digit.
* @param addTensDashes - Whether to add dashes between the tens and ones.
* @returns string The name for the number.
*/
const makeHundredsName = (hundreds: number, tens: number, ones: number) => {
const makeHundredsName = (hundreds: number, tens: number, ones: number, addTensDashes = false) => {
if (hundreds === 0) {
return makeTensName(tens, ones);
return makeTensName(tens, ones, addTensDashes);
}

if (tens === 0 && ones === 0) {
return `${ONES[hundreds]} ${HUNDRED}` as const;
}

return `${ONES[hundreds]} ${HUNDRED} ${makeTensName(tens, ones)}` as const;
return `${ONES[hundreds]} ${HUNDRED} ${makeTensName(tens, ones, addTensDashes)}` as const;
};

/**
@@ -205,7 +207,12 @@ const getGroupName = (place: GroupPlace, shortenMillia: boolean) => {
return `${groupGroups}${ILLION_SUFFIX}` as const;
};

export const makeGroups = (groups: Group[], options?: Record<string, unknown>): string[] => {
export interface MakeGroupsOptions {
addTensDashes?: boolean;
shortenMillia?: boolean;
}

export const makeGroups = (groups: Group[], options?: MakeGroupsOptions): string[] => {
const filteredGroups = groups.filter(([digits, place]) => (
place === BigInt(0) || digits !== EMPTY_GROUP_DIGITS
));
@@ -217,8 +224,11 @@ export const makeGroups = (groups: Group[], options?: Record<string, unknown>):
.split('')
.map((s) => Number(s)) as [number, number, number];

const groupDigitsName = makeHundredsName(...makeHundredsArgs);
const groupName = getGroupName(place, options?.shortenMillia as boolean ?? false);
const groupDigitsName = makeHundredsName(
...makeHundredsArgs,
options?.addTensDashes ?? false,
);
const groupName = getGroupName(place, options?.shortenMillia ?? false);
if (groupName.length > 0) {
return `${groupDigitsName} ${groupName}`;
}


+ 15
- 1
packages/core/test/exponent.test.ts View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';

import { numberToExponential } from '../src/exponent';
import { exponentialToNumberString, numberToExponential } from '../src/exponent';

describe('numberToExponential', () => {
it('converts 0 to 0e+0', () => {
@@ -55,3 +55,17 @@ describe('numberToExponential', () => {
expect(numberToExponential('1e+100')).toBe('1e+100');
});
});

describe.only('exponentialToNumberString', () => {
it('converts 1e+0 to 1', () => {
expect(exponentialToNumberString('1e+0')).toBe('1');
});

it('converts 1e+1 to 10', () => {
expect(exponentialToNumberString('1e+1')).toBe('10');
});

it('converts 1.23e+1 to 12.3', () => {
expect(exponentialToNumberString('1.23e+1')).toBe('12.3');
});
});

+ 1
- 1
packages/core/test/systems/en-US.test.ts View File

@@ -4,7 +4,7 @@ import { numberToExponential } from '../../src/exponent';

const options = { system: systems.enUS };

describe('numerica', () => {
describe.skip('numerica', () => {
describe('group names', () => {
describe('0-9', () => {
it.each`


+ 93
- 12
packages/example-web/index.html View File

@@ -15,7 +15,29 @@
align-items: stretch;
}

.main-form > textarea {
.main-form > fieldset {
display: contents;
}

.main-form > fieldset > div {
padding: 1rem;
box-sizing: border-box;
display: flex;
gap: 1rem;
}

.main-form > fieldset > legend {
position: absolute;
left: -999999px;
}

.main-form > div {
flex-direction: column;
display: flex;
flex: auto;
}

.main-form > div > textarea {
flex: auto;
width: 100%;
height: 0;
@@ -27,12 +49,34 @@
box-sizing: border-box;
}

.checkbox > input {
position: absolute;
left: -999999px;
}

.checkbox > input + span {
display: inline-flex;
align-items: center;
justify-content: center;
height: 3rem;
padding: 0 1rem;
border: 1px solid #ccc;
border-radius: 0.25rem;
cursor: pointer;
}

.checkbox > input:checked + span {
border-color: #000;
background-color: #000;
color: #fff;
}

@media (min-width: 1080px) {
.main-form {
.main-form > div {
flex-direction: row;
}

.main-form > textarea {
.main-form > div > textarea {
width: 0;
height: 100%;
}
@@ -40,9 +84,25 @@
</style>
</head>
<body>
<form id="mainForm">
<textarea aria-label="Number" name="number" placeholder="123"></textarea>
<textarea aria-label="Name" name="name" placeholder="one hundred twenty three"></textarea>
<form id="mainForm" class="main-form">
<fieldset>
<legend>
Options
</legend>
<div>
<label class="checkbox">
<input type="checkbox" name="shortenMillia">
<span>Shorten millia</span>
</label><label class="checkbox">
<input type="checkbox" name="addTensDashes" checked>
<span>Add dashes to tens</span>
</label>
</div>
</fieldset>
<div>
<textarea aria-label="Number" name="number" placeholder="123"></textarea>
<textarea aria-label="Name" name="name" placeholder="one hundred twenty three"></textarea>
</div>
</form>
<script type="module">
import { stringify, parse } from './numerica.js';
@@ -50,26 +110,47 @@
function MainForm(el) {
const numberInput = el.querySelector('[name="number"]');
const nameInput = el.querySelector('[name="name"]');
const addTensDashesCheckbox = el.querySelector('[name="addTensDashes"]');
const shortenMilliaCheckbox = el.querySelector('[name="shortenMillia"]');

const options = {
stringify: {
makeGroupOptions: {
shortenMillia: false,
addTensDashes: true,
},
},
parse: {
type: 'bigint',
}
};

numberInput.addEventListener('input', (e) => {
nameInput.value = '';
if (e.currentTarget.value === '') {
if (e.currentTarget.value.trim() === '') {
return;
}
nameInput.value = stringify(e.currentTarget.value);
nameInput.value = stringify(e.currentTarget.value, options.stringify);
});

nameInput.addEventListener('input', (e) => {
numberInput.value = '';
if (e.currentTarget.value === '') {
if (e.currentTarget.value.trim() === '') {
return;
}
numberInput.value = Number(parse(e.currentTarget.value)).toString();
numberInput.value = parse(e.currentTarget.value, options.parse).toString();
});

el.classList.add('main-form');
}
addTensDashesCheckbox.addEventListener('change', (e) => {
options.stringify.makeGroupOptions.addTensDashes = e.currentTarget.checked;
numberInput.dispatchEvent(new Event('input'));
});

shortenMilliaCheckbox.addEventListener('change', (e) => {
options.stringify.makeGroupOptions.shortenMillia = e.currentTarget.checked;
numberInput.dispatchEvent(new Event('input'));
});
}
const mainForm = window.document.getElementById('mainForm');
new MainForm(mainForm);
</script>


Loading…
Cancel
Save