Skip to content
Snippets Groups Projects
Verified Commit 6490636e authored by Volker Schukai's avatar Volker Schukai :alien:
Browse files

feat: Clean up code and improve number normalization logic #321

Summary of changes
- Camera capture component:
  - Reformatted import statements for consistency.
  - Improved spacing and indentation for better readability.
  - Added detailed comments to clarify the logic within the capture method and other functions.

- DataTable filter component:
  - Introduced `normalizeNumber` function which normalizes strings to numbers while considering locale-specific decimal separators.
  - Extended the queries in `collectSearchQueries` to handle different normalization cases.

- Popper component:
  - Reformatted import statements for consistency.
  - Improved readability through consistent spacing.

- Added new test cases for range comparison expressions:
  - Verified various scenarios such as valid single values, ranges, exclusives, and handling spaces correctly.
  - Ensured error handling for invalid inputs remains robust.
parent 041bf3b2
No related branches found
No related tags found
No related merge requests found
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>inspect update label behavoiur #320</title>
<script src="./320.mjs" type="module"></script>
</head>
<body>
<h1>inspect update label behavoiur #320</h1>
<p></p>
<ul>
<li><a href="https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/320">Issue #320</a></li>
<li><a href="/">Back to overview</a></li>
</ul>
<main>
<monster-test></monster-test>
</main>
</body>
</html>
/**
* @file development/issues/open/320.mjs
* @url https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/320
* @description inspect update label behavoiur
* @issue 320
*/
import "../../../source/components/style/property.pcss";
import "../../../source/components/style/link.pcss";
import "../../../source/components/style/color.pcss";
import "../../../source/components/style/theme.pcss";
import "../../../source/components/style/normalize.pcss";
import "../../../source/components/style/typography.pcss";
import { instanceSymbol } from "../../../source/constants.mjs";
import { CustomElement } from "../../../source/dom/customelement.mjs";
import {
assembleMethodSymbol,
registerCustomElement,
} from "../../../source/dom/customelement.mjs";
import {getLocaleOfDocument} from "../../../source/dom/locale.mjs";
class Test extends CustomElement {
get defaults() {
return Object.assign({}, super.defaults, {
templates: {
main: getTemplate(),
},
labels: getTranslations(),
});
}
/**
* @return {string}
*/
static getTag() {
return "monster-test";
}
}
/**
* @private
* @returns {object}
*/
function getTranslations() {
const locale = getLocaleOfDocument();
switch (locale.language) {
case "de":
return {
"title" : "value"
};
default:
return {
"title" : "value"
};
}
}
/**
* @private
* @return {string}
*/
function getTemplate() {
// language=HTML
return `
<div>
<monster-popper-button
data-monster-attributes="data-monster-option-labels-button path:labels.openButton"
data-monster-role="openButton">
<monster-form>
<monster-field-set
data-monster-attributes="data-monster-option-labels-title path:labels.changeCredentialTitle">
<p data-monster-replace="path:labels | debug | index:title"></p>
</monster-field-set>
</monster-form>
</monster-popper-button>
</div>`;
}
registerCustomElement(Test);
......@@ -209,7 +209,8 @@ function setupLazyCameraInit() {
let initialized = false;
const observer = new IntersectionObserver((entries) => {
const observer = new IntersectionObserver(
(entries) => {
for (const entry of entries) {
if (entry.isIntersecting && !initialized) {
initialized = true;
......@@ -217,15 +218,16 @@ function setupLazyCameraInit() {
initCameraControl.call(self);
}
}
}, {
},
{
root: null,
threshold: 0.1,
});
},
);
observer.observe(this);
}
/**
* @private
*/
......
......@@ -71,6 +71,7 @@ import "../form/context-error.mjs";
import "../form/message-state-button.mjs";
import { getLocaleOfDocument } from "../../dom/locale.mjs";
import { normalizeNumber } from "../../i18n/util.mjs";
export { Filter };
......@@ -1259,6 +1260,27 @@ function collectSearchQueries() {
// return query as url encoded
return encodeURIComponent(query);
},
"to-int-2": (value, key) => {
const query = normalizeNumber(value);
if (Number.isNaN(query)) {
return key + " IS NULL";
}
return key + "=" + encodeURIComponent(Math.round(query * 100));
},
"to-int-3": (value, key) => {
const query = normalizeNumber(value);
if (Number.isNaN(query)) {
return "";
}
return key + "=" + encodeURIComponent(Math.round(query * 1000));
},
"to-int-4": (value, key) => {
const query = normalizeNumber(value);
if (Number.isNaN(query)) {
return "";
}
return key + "=" + encodeURIComponent(Math.round(query * 10000));
},
},
});
......
......@@ -14,6 +14,47 @@
import { languages } from "./map/languages.mjs";
/**
* Normalizes a number represented as a string by converting it into a valid floating-point number format,
* based on the provided or detected locale. It accounts for different decimal and a thousand separator conventions.
*
* @param {string} input - The string representation of the number to normalize. May include locale-specific formatting.
* @param {string} [locale=navigator.language] - The locale used to determine the decimal separator convention. Defaults to the user's browser locale.
* @return {number} The normalized number as a floating-point value. Returns NaN if the input is not a parsable number.
*/
export function normalizeNumber(input, locale = navigator.language) {
if (input === null || input === undefined) return NaN;
if (typeof input === "number") return input; // If input is already a number, return it directly
if (typeof input !== "string") return NaN;
const cleaned = input.trim().replace(/\s/g, "");
const decimalSeparator = getDecimalSeparator(locale);
let normalized = cleaned;
if (decimalSeparator === "," && cleaned.includes(".")) {
normalized = cleaned.replace(/\./g, "").replace(",", ".");
} else if (decimalSeparator === "." && cleaned.includes(",")) {
normalized = cleaned.replace(/,/g, "").replace(".", ".");
} else if (decimalSeparator === "," && cleaned.includes(",")) {
normalized = cleaned.replace(",", ".");
}
const result = parseFloat(normalized);
return Number.isNaN(result) ? NaN : result;
}
/**
* Retrieves the decimal separator for a given locale.
*
* @param {string} [locale=navigator.language] - The locale identifier to determine the decimal separator. Defaults to the user's current locale if not provided.
* @return {string} The decimal separator used in the specified locale.
*/
function getDecimalSeparator(locale = navigator.language) {
const numberWithDecimal = 1.1;
const formatted = new Intl.NumberFormat(locale).format(numberWithDecimal);
return formatted.replace(/\d/g, "")[0]; // z.B. "," oder "."
}
/**
* Determines the user's preferred language based on browser settings and available language options.
*
......
......@@ -15,7 +15,7 @@
export { generateRangeComparisonExpression };
/**
* The `generateRangeComparisonExpression()` function is function that generates a string representation
* The `generateRangeComparisonExpression()` function is a function that generates a string representation
* of a comparison expression based on a range of values. It takes three arguments:
*
* - expression (required): a string representation of a range of values in the format of start1-end1,start2-end2,value3....
......@@ -33,6 +33,8 @@ export { generateRangeComparisonExpression };
* eqOp (string, default: '=='): the equality operator to use in the expression.
* geOp (string, default: '>='): the greater than or equal to operator to use in the expression.
* leOp (string, default: '<='): the less than or equal to operator to use in the expression.
* gtOp (string, default: '>'): the greater than operator to use in the expression.
* ltOp (string, default: '<'): the less than operator to use in the expression.
*
* Examples
*
......@@ -58,10 +60,16 @@ export { generateRangeComparisonExpression };
* @param {string} [options.eqOp='=='] - The comparison operator for equality to use.
* @param {string} [options.geOp='>='] - The comparison operator for greater than or equal to to use.
* @param {string} [options.leOp='<='] - The comparison operator for less than or equal to to use.
* @param {string} [options.gtOp='>'] - The comparison operator for greater than to use.
* @param {string} [options.ltOp='<'] - The comparison operator for less than to use.
* @return {string} The generated comparison expression.
* @throws {Error} If the input is invalid.
* @summary Generates a comparison expression based on a range of values.
*/
/**
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
* SPDX-License-Identifier: AGPL-3.0
*/
function generateRangeComparisonExpression(
expression,
valueName,
......@@ -74,48 +82,110 @@ function generateRangeComparisonExpression(
eqOp = "==",
geOp = ">=",
leOp = "<=",
gtOp = ">",
ltOp = "<",
} = options;
const ranges = expression.split(",");
let comparison = "";
for (let i = 0; i < ranges.length; i++) {
const range = ranges[i].trim();
if (range === "") {
throw new Error(`Invalid range '${range}'`);
} else if (range.includes("-")) {
const [start, end] = range
.split("-")
.map((s) => (s === "" ? null : parseFloat(s)));
if ((start !== null && isNaN(start)) || (end !== null && isNaN(end))) {
throw new Error(`Invalid value in range '${range}'`);
if (
typeof expression !== "string" ||
typeof valueName !== "string" ||
!expression.trim()
) {
throw new Error("Invalid input");
}
if (start !== null && end !== null && start > end) {
const encode = (s) => (urlEncode ? encodeURIComponent(s) : s);
const and = encode(andOp);
const or = encode(orOp);
const space = urlEncode ? "%20" : " ";
function parseRange(range) {
range = range.trim();
if (!range) throw new Error("Empty range");
// Sonderfall: nur Bindestriche wie "--"
if (/^-+$/.test(range)) {
throw new Error(`Invalid range '${range}'`);
}
const compStart =
start !== null
? `${valueName}${urlEncode ? encodeURIComponent(geOp) : geOp}${start}`
: "";
const compEnd =
end !== null
? `${valueName}${urlEncode ? encodeURIComponent(leOp) : leOp}${end}`
: "";
const compRange = `${compStart}${
compStart && compEnd ? ` ${andOp} ` : ""
}${compEnd}`;
comparison += ranges.length > 1 ? `(${compRange})` : compRange;
} else {
const value = parseFloat(range);
if (isNaN(value)) {
// Bereich: z.B. -10--5, 4-6
const rangeMatch = range.match(
/^(-?\d+(?:\.\d+)?)\s*-\s*(-?\d+(?:\.\d+)?)/,
);
if (rangeMatch) {
const sNum = parseFloat(rangeMatch[1]);
const eNum = parseFloat(rangeMatch[2]);
if (isNaN(sNum) || isNaN(eNum))
throw new Error(`Invalid value '${range}'`);
if (sNum > eNum) throw new Error(`Invalid range '${range}'`);
return `${valueName}${encode(geOp)}${sNum}${space}${and}${space}${valueName}${encode(leOp)}${eNum}`;
}
const compValue = `${valueName}${
urlEncode ? encodeURIComponent(eqOp) : eqOp
}${value}`;
comparison += ranges.length > 1 ? `(${compValue})` : compValue;
// Exklusiver Bereich: 4~6 → x>4 && x<6
const exclMatch = range.match(/^(-?\d+(?:\.\d+)?)\s*~\s*(-?\d+(?:\.\d+)?)/);
if (exclMatch) {
const sNum = parseFloat(exclMatch[1]);
const eNum = parseFloat(exclMatch[2]);
if (isNaN(sNum) || isNaN(eNum))
throw new Error(`Invalid value '${range}'`);
return `${valueName}${encode(gtOp)}${sNum}${space}${and}${space}${valueName}${encode(ltOp)}${eNum}`;
}
if (i < ranges.length - 1) {
comparison += ` ${orOp} `;
// Offene Intervalle exklusiv: 4~ → x>4, ~6 → x<6
const openRightExclMatch = range.match(/^(-?\d+(?:\.\d+)?)~$/);
if (openRightExclMatch) {
const sNum = parseFloat(openRightExclMatch[1]);
if (isNaN(sNum)) throw new Error(`Invalid value in '${range}'`);
return `${valueName}${encode(gtOp)}${sNum}`;
}
const openLeftExclMatch = range.match(/^~(-?\d+(?:\.\d+)?)$/);
if (openLeftExclMatch) {
const eNum = parseFloat(openLeftExclMatch[1]);
if (isNaN(eNum)) throw new Error(`Invalid value in '${range}'`);
return `${valueName}${encode(ltOp)}${eNum}`;
}
return comparison;
// Offene Intervalle inklusiv: 4- → x>=4, -6 → x<=6
const openRightInclMatch = range.match(/^(-?\d+(?:\.\d+)?)\-$/);
if (openRightInclMatch) {
const sNum = parseFloat(openRightInclMatch[1]);
if (isNaN(sNum)) throw new Error(`Invalid value in '${range}'`);
return `${valueName}${encode(geOp)}${sNum}`;
}
const openLeftInclMatch = range.match(/^\-(-?\d+(?:\.\d+)?)$/);
if (openLeftInclMatch) {
const eNum = parseFloat(openLeftInclMatch[1]);
if (isNaN(eNum)) throw new Error(`Invalid value in '${range}'`);
return `${valueName}${encode(leOp)}${eNum}`;
}
// <n oder >n
if (range.startsWith("<")) {
const n = parseFloat(range.slice(1));
if (isNaN(n)) throw new Error(`Invalid value in '${range}'`);
return `${valueName}${encode(ltOp)}${n}`;
}
if (range.startsWith(">")) {
const n = parseFloat(range.slice(1));
if (isNaN(n)) throw new Error(`Invalid value in '${range}'`);
return `${valueName}${encode(gtOp)}${n}`;
}
// Einzelwert NUR, wenn wirklich reine Zahl
if (/^-?\d+(\.\d+)?$/.test(range)) {
const value = parseFloat(range);
return `${valueName}${encode(eqOp)}${value}`;
}
throw new Error(`Invalid value '${range}'`);
}
const parts = expression
.split(",")
.map((r) => r.trim())
.filter(Boolean)
.map(parseRange);
const sep = space + or + space;
return parts.length > 1 ? parts.map((p) => `(${p})`).join(sep) : parts[0];
}
......@@ -156,7 +156,7 @@ function getMonsterVersion() {
}
/** don't touch, replaced by make with package.json version */
monsterVersion = new Version("4.11.0");
monsterVersion = new Version("4.11.1");
return monsterVersion;
}
......@@ -7,7 +7,7 @@ describe('Monster', function () {
let monsterVersion
/** don´t touch, replaced by make with package.json version */
monsterVersion = new Version("4.11.0")
monsterVersion = new Version("4.11.1")
let m = getMonsterVersion();
......
import { expect } from "chai";
import { generateRangeComparisonExpression } from "../../../source/text/generate-range-comparison-expression.mjs";
describe('generateRangeComparisonExpression', function () {
it('soll Einzelwerte korrekt behandeln', function () {
expect(generateRangeComparisonExpression("5", "x")).to.equal("x==5");
expect(generateRangeComparisonExpression("0", "age")).to.equal("age==0");
expect(generateRangeComparisonExpression("-3", "score")).to.equal("score<=3");
expect(generateRangeComparisonExpression("7.5", "n")).to.equal("n==7.5");
});
it('soll einfache inklusive Ranges korrekt behandeln', function () {
expect(generateRangeComparisonExpression("4-6", "x")).to.equal("x>=4 && x<=6");
expect(generateRangeComparisonExpression("10-15", "a")).to.equal("a>=10 && a<=15");
expect(generateRangeComparisonExpression("-5-5", "y")).to.equal("y>=-5 && y<=5");
expect(generateRangeComparisonExpression("4.1-6.2", "x")).to.equal("x>=4.1 && x<=6.2");
});
it('soll exklusive Ranges (mit ~) korrekt behandeln', function () {
expect(generateRangeComparisonExpression("4~6", "x")).to.equal("x>4 && x<6");
expect(generateRangeComparisonExpression("10~15", "a")).to.equal("a>10 && a<15");
expect(generateRangeComparisonExpression("-4~4", "y")).to.equal("y>-4 && y<4");
});
it('soll offene Intervalle links korrekt behandeln', function () {
expect(generateRangeComparisonExpression("-6", "x")).to.equal("x<=6");
expect(generateRangeComparisonExpression("-2", "y")).to.equal("y<=2");
expect(generateRangeComparisonExpression(" -3 ", "z")).to.equal("z<=3");
});
it('soll offene Intervalle rechts korrekt behandeln', function () {
expect(generateRangeComparisonExpression("4-", "x")).to.equal("x>=4");
expect(generateRangeComparisonExpression("10-", "z")).to.equal("z>=10");
expect(generateRangeComparisonExpression(" -5- ", "y")).to.equal("y>=-5");
});
it('soll die Operatoren < und > korrekt behandeln', function () {
expect(generateRangeComparisonExpression(">4", "x")).to.equal("x>4");
expect(generateRangeComparisonExpression("<6", "x")).to.equal("x<6");
expect(generateRangeComparisonExpression(">-4.2", "n")).to.equal("n>-4.2");
expect(generateRangeComparisonExpression("--2", "x")).to.equal("x<=-2");
});
it('soll mehrere Bedingungen per Komma ODER-verknüpfen', function () {
expect(generateRangeComparisonExpression("4-6,10,20~25", "x"))
.to.equal("(x>=4 && x<=6) || (x==10) || (x>20 && x<25)");
expect(generateRangeComparisonExpression("0,5-10", "y"))
.to.equal("(y==0) || (y>=5 && y<=10)");
expect(generateRangeComparisonExpression(">1,<3", "z"))
.to.equal("(z>1) || (z<3)");
});
it('soll urlEncode korrekt unterstützen', function () {
expect(generateRangeComparisonExpression("4-6", "x", { urlEncode: true }))
.to.equal("x%3E%3D4%20%26%26%20x%3C%3D6");
expect(generateRangeComparisonExpression("10", "n", { urlEncode: true }))
.to.equal("n%3D%3D10");
});
it('soll Fehler werfen bei falschen Eingaben', function () {
expect(() => generateRangeComparisonExpression("", "x")).to.throw("Invalid input");
expect(() => generateRangeComparisonExpression("abc", "x")).to.throw("Invalid value 'abc'");
expect(() => generateRangeComparisonExpression("4-3", "x")).to.throw("Invalid range '4-3'");
expect(() => generateRangeComparisonExpression("4-foo", "x")).to.throw("Invalid value '4-foo'");
expect(() => generateRangeComparisonExpression("4~foo", "x")).to.throw("Invalid value '4~foo'");
expect(() => generateRangeComparisonExpression(">", "x")).to.throw("Invalid value in '>'");
expect(() => generateRangeComparisonExpression("<", "x")).to.throw("Invalid value in '<'");
expect(() => generateRangeComparisonExpression("--", "x")).to.throw("Invalid range '--'");
expect(() => generateRangeComparisonExpression("-foo", "x")).to.throw("Invalid value '-foo'");
expect(() => generateRangeComparisonExpression("foo-", "x")).to.throw("Invalid value 'foo-'");
expect(() => generateRangeComparisonExpression("5~", "x")).to.not.throw();
expect(() => generateRangeComparisonExpression("~5", "x")).to.not.throw();
expect(() => generateRangeComparisonExpression(">", "")).to.throw("Invalid value in '>'");
expect(() => generateRangeComparisonExpression("1,,2", "x")).not.to.throw();
expect(() => generateRangeComparisonExpression(",", "x")).to.not.throw();
});
it('soll Leerzeichen robust behandeln', function () {
expect(generateRangeComparisonExpression(" 4 - 6 , 8 ", "x"))
.to.equal("(x>=4 && x<=6) || (x==8)");
expect(generateRangeComparisonExpression(" -10--5 ", "temp"))
.to.equal("temp>=-10 && temp<=-5");
});
it('soll negative Werte korrekt behandeln', function () {
expect(generateRangeComparisonExpression("-10--5", "temp"))
.to.equal("temp>=-10 && temp<=-5");
expect(generateRangeComparisonExpression("> -3", "z")).to.equal("z>-3");
expect(generateRangeComparisonExpression(" -4~4 ", "y")).to.equal("y>-4 && y<4");
expect(generateRangeComparisonExpression("-5", "x")).to.equal("x<=5");
});
});
......@@ -55,7 +55,7 @@ describe('generateRangeComparisonExpression', () => {
{
expression: '1,3,5',
valueName: 'x',
expected: '(x%3D%3D1) || (x%3D%3D3) || (x%3D%3D5)',
expected: '(x%3D%3D1)%20%7C%7C%20(x%3D%3D3)%20%7C%7C%20(x%3D%3D5)',
},
{
expression: '-10',
......@@ -70,12 +70,12 @@ describe('generateRangeComparisonExpression', () => {
{
expression: '1-3,6-8',
valueName: 'y',
expected: '(y%3E%3D1 && y%3C%3D3) || (y%3E%3D6 && y%3C%3D8)',
expected: '(y%3E%3D1%20%26%26%20y%3C%3D3)%20%7C%7C%20(y%3E%3D6%20%26%26%20y%3C%3D8)',
},
{
expression: '1-3,5,7-9',
valueName: 'z',
expected: '(z%3E%3D1 && z%3C%3D3) || (z%3D%3D5) || (z%3E%3D7 && z%3C%3D9)',
expected: '(z%3E%3D1%20%26%26%20z%3C%3D3)%20%7C%7C%20(z%3D%3D5)%20%7C%7C%20(z%3E%3D7%20%26%26%20z%3C%3D9)',
},
];
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment