@soleil-se/app-util 5.12.3 → 5.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/common/index.d.ts +3 -0
- package/common/index.js +4 -1
- package/common/localized-compare/index.d.ts +20 -0
- package/common/localized-compare/index.js +103 -0
- package/package.json +2 -2
- package/common/server/index.d.ts +0 -6
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,12 @@ All notable changes to this project will be documented in this file.
|
|
|
7
7
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
8
8
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
9
9
|
|
|
10
|
+
## [5.13.0] - 2026-01-16
|
|
11
|
+
|
|
12
|
+
- Add comparator functions `localizedCompare` and `localizedCompareBy` to sort strings and objects
|
|
13
|
+
by a specific property using localized string comparison since String.localeCompare does not
|
|
14
|
+
work properly with nordic characters in Rhino.
|
|
15
|
+
|
|
10
16
|
## [5.12.3] - 2025-12-03
|
|
11
17
|
|
|
12
18
|
- Even better type definitions for Svelte render functions.
|
package/common/index.d.ts
CHANGED
|
@@ -79,3 +79,6 @@ export const isOffline: boolean;
|
|
|
79
79
|
* @constant {boolean}
|
|
80
80
|
*/
|
|
81
81
|
export const isOnline: boolean;
|
|
82
|
+
import { localizedCompare } from "./localized-compare";
|
|
83
|
+
import { localizedCompareBy } from "./localized-compare";
|
|
84
|
+
export { localizedCompare, localizedCompareBy };
|
package/common/index.js
CHANGED
|
@@ -3,6 +3,7 @@ import app from '@sitevision/api/common/app';
|
|
|
3
3
|
|
|
4
4
|
import { nativeRequire } from '../server';
|
|
5
5
|
import getLegacyRouteUri from './legacy/getRouteUri';
|
|
6
|
+
import { localizedCompare, localizedCompareBy } from './localized-compare';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Regex for selecting leading slashes
|
|
@@ -150,7 +151,7 @@ export function getRouteUri(route = '', params = undefined) {
|
|
|
150
151
|
*/
|
|
151
152
|
export function getViewUri(route = '', params = undefined) {
|
|
152
153
|
if (!router?.getUrl) {
|
|
153
|
-
console.warn('[@soleil-
|
|
154
|
+
console.warn('[@soleil-se/app-util] getViewUri requires router.getUrl support.');
|
|
154
155
|
return undefined;
|
|
155
156
|
}
|
|
156
157
|
|
|
@@ -183,3 +184,5 @@ export function setAppProps(data) {
|
|
|
183
184
|
export function getAppProps(key) {
|
|
184
185
|
return key ? appProps[key] : appProps;
|
|
185
186
|
}
|
|
187
|
+
|
|
188
|
+
export { localizedCompare, localizedCompareBy };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compares two strings in a localized manner, taking into account special characters.
|
|
3
|
+
* The order is defined as: a-z å ä æ ö ø (case-insensitive, with lowercase before uppercase).
|
|
4
|
+
* Characters not in this set are compared based on their Unicode code points after removing accents.
|
|
5
|
+
* @param {string} a - The first string to compare.
|
|
6
|
+
* @param {string} b - The second string to compare.
|
|
7
|
+
* @returns {number} Negative if a < b, positive if a > b, zero if equal.
|
|
8
|
+
*/
|
|
9
|
+
export function localizedCompare(a: string, b: string): number;
|
|
10
|
+
/**
|
|
11
|
+
* Creates a comparator function for sorting objects by a specific property or properties.
|
|
12
|
+
* The property values are compared using localized string comparison.
|
|
13
|
+
* When given an array of properties, sorts by the first property, then by the second if equal, etc.
|
|
14
|
+
* @param {string|string[]} prop - The property name(s) to compare by.
|
|
15
|
+
* @returns {(a: Object, b: Object) => number} A comparator function that accepts two objects and returns their sort order.
|
|
16
|
+
* @example
|
|
17
|
+
* arr.sort(localizedCompareBy('title'))
|
|
18
|
+
* arr.sort(localizedCompareBy(['lastName', 'firstName']))
|
|
19
|
+
*/
|
|
20
|
+
export function localizedCompareBy(prop: string | string[]): (a: any, b: any) => number;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const charOrder = 'abcdefghijklmnopqrstuvwxyzåäæöø';
|
|
2
|
+
|
|
3
|
+
function normalizeChar(char) {
|
|
4
|
+
// Remove accents from unknown characters (é → e, ñ → n)
|
|
5
|
+
return char.normalize('NFD').replace(/[\u0300-\u036f]/g, '');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getCharInfo(char) {
|
|
9
|
+
const lowerChar = char.toLowerCase();
|
|
10
|
+
let index = charOrder.indexOf(lowerChar);
|
|
11
|
+
let isAccented = false;
|
|
12
|
+
|
|
13
|
+
// If not found, try normalized version
|
|
14
|
+
if (index === -1) {
|
|
15
|
+
const normalized = normalizeChar(lowerChar);
|
|
16
|
+
index = charOrder.indexOf(normalized);
|
|
17
|
+
|
|
18
|
+
if (index !== -1) {
|
|
19
|
+
isAccented = true;
|
|
20
|
+
} else {
|
|
21
|
+
// Completely unknown character
|
|
22
|
+
index = charOrder.length + char.charCodeAt(0);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
baseIndex: index,
|
|
28
|
+
isAccented,
|
|
29
|
+
isLower: char === char.toLowerCase() && char !== char.toUpperCase(),
|
|
30
|
+
accentCode: isAccented ? char.normalize('NFD').charCodeAt(1) || 0 : 0,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Compares two strings in a localized manner, taking into account special characters.
|
|
36
|
+
* The order is defined as: a-z å ä æ ö ø (case-insensitive, with lowercase before uppercase).
|
|
37
|
+
* Characters not in this set are compared based on their Unicode code points after removing accents.
|
|
38
|
+
* @param {string} a - The first string to compare.
|
|
39
|
+
* @param {string} b - The second string to compare.
|
|
40
|
+
* @returns {number} Negative if a < b, positive if a > b, zero if equal.
|
|
41
|
+
*/
|
|
42
|
+
export function localizedCompare(a, b) {
|
|
43
|
+
// Handle null/undefined
|
|
44
|
+
if (a == null) return b == null ? 0 : -1;
|
|
45
|
+
if (b == null) return 1;
|
|
46
|
+
|
|
47
|
+
// Ensure strings
|
|
48
|
+
const strA = String(a);
|
|
49
|
+
const strB = String(b);
|
|
50
|
+
|
|
51
|
+
const minLength = Math.min(strA.length, strB.length);
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < minLength; i += 1) {
|
|
54
|
+
const infoA = getCharInfo(strA[i]);
|
|
55
|
+
const infoB = getCharInfo(strB[i]);
|
|
56
|
+
|
|
57
|
+
// 1. Compare base character (case and accent insensitive)
|
|
58
|
+
if (infoA.baseIndex !== infoB.baseIndex) {
|
|
59
|
+
return infoA.baseIndex - infoB.baseIndex;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 2. Non-accented before accented (e.g., 'e' < 'é')
|
|
63
|
+
if (infoA.isAccented !== infoB.isAccented) {
|
|
64
|
+
return infoA.isAccented ? 1 : -1;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 3. If both accented, compare accent types (é vs è)
|
|
68
|
+
if (infoA.isAccented && infoA.accentCode !== infoB.accentCode) {
|
|
69
|
+
return infoA.accentCode - infoB.accentCode;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 4. Lowercase before uppercase (e.g., 'e' < 'E')
|
|
73
|
+
if (infoA.isLower !== infoB.isLower) {
|
|
74
|
+
return infoA.isLower ? -1 : 1;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return strA.length - strB.length;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Creates a comparator function for sorting objects by a specific property or properties.
|
|
83
|
+
* The property values are compared using localized string comparison.
|
|
84
|
+
* When given an array of properties, sorts by the first property, then by the second if equal, etc.
|
|
85
|
+
* @param {string|string[]} prop - The property name(s) to compare by.
|
|
86
|
+
* @returns {(a: Object, b: Object) => number} A comparator function that accepts two objects and returns their sort order.
|
|
87
|
+
* @example
|
|
88
|
+
* arr.sort(localizedCompareBy('title'))
|
|
89
|
+
* arr.sort(localizedCompareBy(['lastName', 'firstName']))
|
|
90
|
+
*/
|
|
91
|
+
export function localizedCompareBy(prop) {
|
|
92
|
+
const props = Array.isArray(prop) ? prop : [prop];
|
|
93
|
+
|
|
94
|
+
return (a, b) => {
|
|
95
|
+
for (let i = 0; i < props.length; i += 1) {
|
|
96
|
+
const result = localizedCompare(a[props[i]], b[props[i]]);
|
|
97
|
+
if (result !== 0) {
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return 0;
|
|
102
|
+
};
|
|
103
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soleil-se/app-util",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.13.0",
|
|
4
4
|
"description": "Utility functions for WebApps, RESTApps and Widgets in Sitevision.",
|
|
5
5
|
"main": "./common/index.js",
|
|
6
6
|
"author": "Soleil AB",
|
|
@@ -25,5 +25,5 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"create-type-definitions": "node ../../utils/createTypeDefinitions.js ./common/index.js ./client/index.js ./client/svelte/index.js ./client/svelte/3/index.js ./client/svelte/4/index.js ./client/svelte/5/index.js ./server/index.js ./server/svelte/index.js ./server/svelte/3/index.js ./server/svelte/4/index.js ./server/svelte/5/index.js ./server/app-data/index.js ./server/global-app-data/index.js"
|
|
27
27
|
},
|
|
28
|
-
"gitHead": "
|
|
28
|
+
"gitHead": "3c3ef92d604eeed89887a94964edf2146e2a746a"
|
|
29
29
|
}
|