Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

numericFormatter locale issue #1503

Merged
merged 21 commits into from
Sep 30, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions lib/ui/elements/numericInput.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ function oninput(e, params) {
params.stringValue = e.target.value

// Assign numeric newValue.
params.newValue = mapp.utils.unformatStringValue(params)
params.newValue = params.onRangeInput ? params.stringValue : mapp.utils.unformatStringValue(params);

if (params.numericChecks(params.newValue, params)) {

Expand All @@ -81,13 +81,15 @@ function oninput(e, params) {
}

// The invalid input should not be formatted.
if (params.invalid) return;
if (params.invalid) return;

// Pass valid newValue to callback method.
params.callback(params.newValue)

// Re-format the params numeric value (newValue || value) and set as input string value.
e.target.value = mapp.utils.formatNumericValue(params)
//Mark the onRangeInput to false is the origin of the input call could come from either a slider or input
params.onRangeInput = false;
}

/**
Expand Down
6 changes: 4 additions & 2 deletions lib/ui/elements/slider.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export default function slider(params) {
max=${params.max}
step=${params.step}
value=${params.value}
oninput=${onRangeInput}>`
oninput=${e => onRangeInput(e, params)}>`

return params.sliderElement

Expand All @@ -80,11 +80,13 @@ export default function slider(params) {

@param {Object} e oninput event from range type input.
*/
function onRangeInput(e) {
function onRangeInput(e, params) {

// Range type input return a string target.value.
const val = Number(e.target.value)

params.onRangeInput = true;

numericInput.value = val

// Trigger formatting and numeric checks.
Expand Down
29 changes: 15 additions & 14 deletions lib/ui/elements/slider_ab.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -70,28 +70,28 @@ export default function slider_ab(params) {

// Check whether value is a number.
if (isNaN(value)) return false;

if (params.data_id === 'a' && value > maxInputParams.newValue) {

return false
}

if (params.data_id === 'b' && value < minInputParams.newValue) {

return false
}

if (params.min && value < params.min) {

// The value is smaller than min.
return false
}

if (params.max) {

return value <= params.max
}

return true
}

Expand Down Expand Up @@ -119,18 +119,18 @@ export default function slider_ab(params) {
max=${params.max}
step=${params.step}
value=${params.val_a}
oninput=${onRangeInput}/>
oninput=${e => onRangeInput(e, params)}/>
<input data-id="b" type="range"
name="maxRangeInput"
min=${params.min}
max=${params.max}
step=${params.step}
value=${params.val_b}
oninput=${onRangeInput}/>`
oninput=${e => onRangeInput(e, params)}/>`

// The sliderElement property is required to update the range input on numeric input.
minInputParams.sliderElement = element
maxInputParams.sliderElement = element
minInputParams.sliderElement = element
maxInputParams.sliderElement = element

/**
@function onRangeInput
Expand All @@ -142,10 +142,11 @@ export default function slider_ab(params) {

@param {Object} e oninput event from range type input.
*/
function onRangeInput(e) {
function onRangeInput(e, params) {

// Range type input return a string target.value.
const val = Number(e.target.value)
params.onRangeInput = true;

// Check whether input event is from minRangeInput.
if (e.target.dataset.id === 'a') {
Expand Down
76 changes: 44 additions & 32 deletions lib/utils/numericFormatter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,8 @@ export function formatNumericValue(params) {
// "integer" type values must not have fraction digits.
params.formatterParams.options.maximumFractionDigits ??= params.round || params.type === 'integer' ? 0 : 2

params.localeString = numericValue.toLocaleString(
params.formatterParams.locale,
params.formatterParams.options)
// Format a numeric value into a localized string representation
params.localeString = new Intl.NumberFormat(params.formatterParams.locale, params.formatterParams.options).format(numericValue);
}

// The trailing fraction character will be store by the unformatStringvalue method if present.
Expand All @@ -79,20 +78,24 @@ export function formatNumericValue(params) {
}

/**
@function unformatStringValue

@description
The unformatStringValue will splice the suffix and prefix from a stringValue and remove seperators as defined by the localeString locale.

The numeric value will be returned as parsed float.

@param {Object} params The config object argument.
@property {String} params.suffix Suffix to be removed from stringValue.
@property {String} params.prefix Prefix to be removed from stringValue.
@property {Object} params.formatterParams Configuration for the localeString.

@returns {numeric} A numeric value extracted from the stringValue.
*/
* Unformats a string value by removing prefix, suffix, and locale-specific formatting.
*
* @function unformatStringValue
* @description
* This function removes the specified prefix and suffix from the input string,
* then unformats the remaining value based on the provided locale settings.
* It handles locale-specific decimal separators and removes non-numeric characters.
* The resulting numeric value is returned as a parsed float.
*
* @param {Object} params - The configuration object.
* @param {string} params.stringValue - The input string to be unformatted.
* @param {string} [params.prefix] - Prefix to be removed from stringValue.
* @param {string} [params.suffix] - Suffix to be removed from stringValue.
* @param {Object} [params.formatterParams] - Configuration for locale-specific formatting.
* @param {string} [params.formatterParams.locale] - The locale to use for unformatting (e.g., 'en-US', 'de-DE').
*
* @returns {number|null} The numeric value extracted from the stringValue, or null if the input is empty or invalid.
*/
export function unformatStringValue(params) {

if (!params.stringValue) return null;
Expand All @@ -113,26 +116,35 @@ export function unformatStringValue(params) {

if (stringValue.length && params.formatterParams?.locale) {

// Determine decimal separator and fraction characters.
const chars = (1234.5).toLocaleString(params.formatterParams?.locale).match(/(\D+)/g);
// Create a number formatter using the specified locale
const numberFormatter = new Intl.NumberFormat(params.formatterParams.locale);

// Remove the decimal separator from stringValue.
stringValue = stringValue.split(chars[0]).join('');
// Get the parts of a formatted number (1.1 is used as an example)
const parts = numberFormatter.formatToParts(10000.1);

// Find the decimal separator used in this locale
const decimalSeperator = parts.find(part => part.type === 'decimal')?.value;

//Find the thousand seperator used in the locale
const thousandSeperator = parts.find(part => part.type === 'group')?.value;

// Replace the locale-specific decimal separator with a standard period
let normalisedValue = stringValue.replaceAll(thousandSeperator, '');

// Replace the locale-specific decimal separator with a standard period
normalisedValue = normalisedValue.replace(decimalSeperator, '.');

// Remove any non-numeric characters, except for period and minus sign
const cleanedValue = normalisedValue.replace(/[^\d.-]/g, '');

//Delete the lastCharacter param
delete params.lastCharacter

// Preserve fraction character at end of stringValue
if (stringValue.endsWith(chars[1])) {
params.lastCharacter = chars[1]
}
// Parse the cleaned string into a float (decimal) number
stringValue = parseFloat(cleanedValue);

return stringValue;

// Preserve 0 fraction at end of stringValue
if (stringValue.endsWith(chars[1]+0)) {
params.lastCharacter = chars[1]+0
}

// Replace the fraction character with dot in stringValue.
stringValue = stringValue.split(chars[1]).join('.');
}

if (stringValue === '') return null;
Expand Down
5 changes: 4 additions & 1 deletion tests/browser/local.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { ui_elementsTest } from '../lib/ui/elements/_elements.test.mjs';
import { ui_layers } from '../lib/ui/layers/_layers.test.mjs';
import { entriesTest } from '../lib/ui/locations/entries/_entries.test.mjs';
import { uiTest } from '../lib/ui/_ui.test.mjs';
import { utilsTest } from '../lib/utils/_utils.test.mjs';
import { formatTest } from '../lib/layer/format/_format.test.mjs';
import { ui_locations } from '../lib/ui/locations/_locations.test.mjs';

Expand Down Expand Up @@ -56,6 +57,8 @@ await ui_layers.filtersTest(mapview);

await uiTest.Tabview();

await utilsTest.numericFormatterTest();

await formatTest.vectorTest(mapview);

await ui_locations.infojTest();
await ui_locations.infojTest();
4 changes: 2 additions & 2 deletions tests/lib/layer/format/vector.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* @param {object} mapview
*/
export async function vectorTest(mapview) {
codi.describe('Layer Format: Vector', async () => {
await codi.describe('Layer Format: Vector', async () => {

/**
* ### Should be able to create a cluster layer
Expand All @@ -19,7 +19,7 @@ export async function vectorTest(mapview) {
* 4. We expect the format of the layer to change to 'cluster'
* @function it
*/
codi.it('Should create a cluster layer', async () => {
await codi.it('Should create a cluster layer', async () => {
const layer_params = {
mapview: mapview,
'key': 'cluster_test',
Expand Down
4 changes: 2 additions & 2 deletions tests/lib/ui/locations/infoj.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @function injojTest
*/
export async function infojTest() {
codi.describe('UI Locations: infojTest', async () => {
await codi.describe('UI Locations: infojTest', async () => {
/**
* ### It should create an infoj with a correct order
* 1. We define an infoj with a combination of different entries with keys, fields and queries
Expand Down Expand Up @@ -68,7 +68,7 @@ export async function infojTest() {
'value_4',
'value_5',
'value_6'
];
];

// Asserting we get the expected results and order
codi.assertEqual(results, expected, 'The infoj order needs to be as defined in the expected');
Expand Down
3 changes: 3 additions & 0 deletions tests/lib/utils/_utils.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { numericFormatterTest } from './numericFormatter.test.mjs';

export const utilsTest = { numericFormatterTest }
64 changes: 64 additions & 0 deletions tests/lib/utils/numericFormatter.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
/**
* @module utils/numericFormatter
*/

/**
* This function is used as an entry point for the numericFormatter Test
* @function numericFormatterTest
*/
export async function numericFormatterTest() {

await codi.describe('Utils: numericFormatter Test', async () => {
const params = {
value: 654321.987,
prefix: '$',
formatterParams: {
locale: 'en-UK'
}
};

const expected_value = 654321.99
/**
* ### Should unformat UK locale string
* This test is used to check if a localised string to UK returns the correct string.
* @function it
*/
await codi.it('Should unformat UK locale strings', async () => {

mapp.utils.formatNumericValue(params);
const unformattedString = mapp.utils.unformatStringValue(params)
codi.assertEqual(unformattedString, expected_value, `We expect the value to equal ${expected_value}, we received ${unformattedString}`)
});
/**
* ### Should unformat DE locale string
* This test is used to check if a localised string to DE returns the correct string.
* @function it
*/
await codi.it('Should unformat DE locale strings', async () => {
//Settings the locale to 'DE'
params.formatterParams.locale = 'de-AT';

mapp.utils.formatNumericValue(params);
const unformattedString = mapp.utils.unformatStringValue(params)
codi.assertEqual(unformattedString, expected_value, `We expect the value to equal ${expected_value}, we received ${unformattedString}`)
});

await codi.it('Should unformat PL locale strings', async () => {
//Settings the locale to 'PL'
params.formatterParams.locale = 'PL';
mapp.utils.formatNumericValue(params);

const unformattedString = mapp.utils.unformatStringValue(params)
codi.assertEqual(unformattedString, expected_value, `We expect the value to equal ${expected_value}, we received ${unformattedString}`)
});

await codi.it('Should unformat RUB locale strings', async () => {
//Settings the locale to 'RUB'
params.formatterParams.locale = 'RUB';
mapp.utils.formatNumericValue(params);

const unformattedString = mapp.utils.unformatStringValue(params)
codi.assertEqual(unformattedString, expected_value, `We expect the value to equal ${expected_value}, we received ${unformattedString}`)
});
});
}
Loading