Skip to content

Commit ee623e8

Browse files
committed
feat(pinia-orm): support Intl.Collator for locale aware sorting
orderBy() (query & repository), useSortBy and the orderBy util now accept an Intl.Collator besides the existing sort flags. String values are then compared with the collator, enabling locale aware ordering (e.g. Lithuanian A, B, Š, T, U) and options like numeric sorting. Different flags can be given per order clause. closes #2006
1 parent 018acee commit ee623e8

10 files changed

Lines changed: 97 additions & 16 deletions

File tree

docs/content/2.api/1.composables/2.helpers/use-sort-by.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ useSortBy(users, 'name')
2020
// sort by the `name` attribute case insensitive
2121
useSortBy(users, 'name', 'SORT_FLAG_CASE')
2222

23+
// sort by the `name` attribute locale aware
24+
useSortBy(users, 'name', new Intl.Collator('lt'))
25+
2326
// sorts the collection by 'name' descending and then by 'lastname' ascending
2427
useSortBy(users, [
2528
['name', 'desc'],
@@ -36,6 +39,7 @@ useSortBy(users, (model) => model.age)
3639
````ts
3740
export type sorting<T> = ((record: T) => any) | string | [string, 'asc' | 'desc'][]
3841
export type SortFlags = 'SORT_REGULAR' | 'SORT_FLAG_CASE'
42+
export type SortComparator = SortFlags | Intl.Collator
3943

40-
export function useSortBy<T>(collection: T[], sort: sorting<T>, flags?: SortFlags): T[]
44+
export function useSortBy<T>(collection: T[], sort: sorting<T>, flags?: SortComparator): T[]
4145
````

docs/content/2.api/3.query/order-by.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,21 @@ useRepo(User)
2424

2525
// Sort user name by its third character.
2626
useRepo(User).orderBy(user => user.name[2]).get()
27+
28+
// Sort user names case insensitive.
29+
useRepo(User).orderBy('name', 'asc', 'SORT_FLAG_CASE').get()
30+
31+
// Sort user names locale aware with an Intl.Collator,
32+
// e.g. for Lithuanian: A, B, Š, T, U instead of A, B, T, U, Š.
33+
useRepo(User).orderBy('name', 'asc', new Intl.Collator('lt')).get()
2734
````
2835

36+
The optional third argument accepts either one of the sort flags (`'SORT_REGULAR'`, `'SORT_FLAG_CASE'`) or any [`Intl.Collator`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator) instance. A collator is used to compare string values, which enables locale aware sorting as well as options like numeric ordering (`new Intl.Collator(undefined, { numeric: true })`).
37+
2938
## Typescript Declarations
3039

3140
````ts
32-
function orderBy(field: OrderBy, direction: OrderDirection = 'asc'): Query
41+
function orderBy(field: OrderBy, direction: OrderDirection = 'asc', flags: SortComparator = 'SORT_REGULAR'): Query
42+
43+
type SortComparator = 'SORT_REGULAR' | 'SORT_FLAG_CASE' | Intl.Collator
3344
````

packages/pinia-orm/src/composables/collection/useCollect.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { Collection, Model } from '../../../src'
2-
import type { SortFlags } from '../../support/Utils'
2+
import type { SortComparator } from '../../support/Utils'
33
import { useSum } from './useSum'
44
import { useMax } from './useMax'
55
import { useMin } from './useMin'
@@ -15,7 +15,7 @@ export interface UseCollect<M extends Model = Model> {
1515
max: (field: string) => number
1616
pluck: (field: string) => any[]
1717
groupBy: (fields: string[] | string) => Record<string, Collection<M>>
18-
sortBy: (sort: sorting<M>, flags?: SortFlags) => M[]
18+
sortBy: (sort: sorting<M>, flags?: SortComparator) => M[]
1919
keys: () => string[]
2020
}
2121

@@ -29,7 +29,7 @@ export function useCollect<M extends Model = Model> (models: Collection<M>): Use
2929
max: field => useMax(models, field),
3030
pluck: field => usePluck(models, field),
3131
groupBy: fields => useGroupBy(models, fields),
32-
sortBy: (sort, flags: SortFlags = 'SORT_REGULAR') => useSortBy(models, sort, flags),
32+
sortBy: (sort, flags: SortComparator = 'SORT_REGULAR') => useSortBy(models, sort, flags),
3333
keys: () => useKeys(models),
3434
}
3535
}

packages/pinia-orm/src/composables/collection/useSortBy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { SortFlags } from '../../support/Utils'
1+
import type { SortComparator } from '../../support/Utils'
22
import { orderBy } from '../../support/Utils'
33

44
export type sorting<T> = ((record: T) => any) | string | [string, 'asc' | 'desc'][]
@@ -7,7 +7,7 @@ export type sorting<T> = ((record: T) => any) | string | [string, 'asc' | 'desc'
77
* Creates an array of elements, sorted in specified order by the results
88
* of running each element in a collection thru each iteratee.
99
*/
10-
export function useSortBy<T extends Record<string, any>> (collection: T[], sort: sorting<T>, flags?: SortFlags): T[] {
10+
export function useSortBy<T extends Record<string, any>> (collection: T[], sort: sorting<T>, flags?: SortComparator): T[] {
1111
const directions = []
1212
const iteratees = []
1313

packages/pinia-orm/src/query/Options.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Model, WithKeys } from '../model/Model'
2+
import type { SortComparator } from '../support/Utils'
23
import type { Query } from './Query'
34

45
export interface Where<T = Model> {
@@ -23,6 +24,7 @@ export interface WhereGroup {
2324
export interface Order {
2425
field: OrderBy
2526
direction: OrderDirection
27+
flags?: SortComparator
2628
}
2729

2830
export interface Group {

packages/pinia-orm/src/query/Query.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
isFunction,
1010
orderBy,
1111
} from '../support/Utils'
12+
import type { SortComparator } from '../support/Utils'
1213
import type { Collection, Element, Elements, GroupedCollection, Item, NormalizedData } from '../data/Data'
1314
import type { Database } from '../database/Database'
1415
import { Relation } from '../model/attributes/relations/Relation'
@@ -359,8 +360,8 @@ export class Query<M extends Model = Model> {
359360
/**
360361
* Add an "order by" clause to the query.
361362
*/
362-
orderBy (field: OrderBy, direction: OrderDirection = 'asc'): this {
363-
this.orders.push({ field, direction })
363+
orderBy (field: OrderBy, direction: OrderDirection = 'asc', flags: SortComparator = 'SORT_REGULAR'): this {
364+
this.orders.push({ field, direction, flags })
364365

365366
return this
366367
}
@@ -625,8 +626,9 @@ export class Query<M extends Model = Model> {
625626
protected filterOrder (models: Collection<M>): Collection<M> {
626627
const fields = this.orders.map(order => order.field)
627628
const directions = this.orders.map(order => order.direction)
629+
const flags = this.orders.map(order => order.flags ?? 'SORT_REGULAR')
628630

629-
return orderBy(models, fields, directions)
631+
return orderBy(models, fields, directions, flags)
630632
}
631633

632634
/**

packages/pinia-orm/src/repository/Repository.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Pinia } from 'pinia'
22
import type { Constructor } from '../types'
33
import { assert, isArray } from '../support/Utils'
4+
import type { SortComparator } from '../support/Utils'
45
import type { Collection, Element, Item } from '../data/Data'
56
import type { Database } from '../database/Database'
67
import type { Model, WithKeys } from '../model/Model'
@@ -332,8 +333,8 @@ export class Repository<M extends Model = Model> {
332333
/**
333334
* Add an "order by" clause to the query.
334335
*/
335-
orderBy (field: OrderBy, direction?: OrderDirection): Query<M> {
336-
return this.query().orderBy(field, direction)
336+
orderBy (field: OrderBy, direction?: OrderDirection, flags?: SortComparator): Query<M> {
337+
return this.query().orderBy(field, direction, flags)
337338
}
338339

339340
/**

packages/pinia-orm/src/support/Utils.ts

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ interface SortableArray<T> {
88

99
export type SortFlags = 'SORT_REGULAR' | 'SORT_FLAG_CASE'
1010

11+
export type SortComparator = SortFlags | Intl.Collator
12+
1113
/**
1214
* Compare two values with custom string operator
1315
*/
@@ -77,7 +79,7 @@ export function orderBy<T extends Element> (
7779
collection: T[],
7880
iteratees: (((record: T) => any) | string)[],
7981
directions: string[],
80-
flags: SortFlags = 'SORT_REGULAR',
82+
flags: SortComparator | SortComparator[] = 'SORT_REGULAR',
8183
): T[] {
8284
let index = -1
8385

@@ -130,7 +132,7 @@ function compareMultiple<T> (
130132
object: SortableArray<T>,
131133
other: SortableArray<T>,
132134
directions: string[],
133-
flags: SortFlags,
135+
flags: SortComparator | SortComparator[],
134136
): number {
135137
let index = -1
136138

@@ -139,7 +141,7 @@ function compareMultiple<T> (
139141
const length = objCriteria.length
140142

141143
while (++index < length) {
142-
const result = compareAscending(objCriteria[index], othCriteria[index], flags)
144+
const result = compareAscending(objCriteria[index], othCriteria[index], isArray(flags) ? flags[index] ?? 'SORT_REGULAR' : flags)
143145

144146
if (result) {
145147
const direction = directions[index]
@@ -153,8 +155,13 @@ function compareMultiple<T> (
153155
/**
154156
* Compares values to sort them in ascending order.
155157
*/
156-
function compareAscending (value: any, other: any, flags: SortFlags): number {
158+
function compareAscending (value: any, other: any, flags: SortComparator): number {
157159
if (value !== other) {
160+
if (typeof flags === 'object' && typeof value === 'string' && typeof other === 'string') {
161+
const result = flags.compare(value, other)
162+
return result > 0 ? 1 : result < 0 ? -1 : 0
163+
}
164+
158165
const valIsDefined = value !== undefined
159166
const valIsNull = value === null
160167
const valIsReflexive = value === value

packages/pinia-orm/tests/feature/repository/retrieves_order_by.spec.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,34 @@ describe('feature/repository/retrieves_order_by', () => {
115115
assertModels(users, expected)
116116
})
117117

118+
it('can sort records locale aware with a collator', () => {
119+
const userRepo = useRepo(User)
120+
121+
fillState({
122+
users: {
123+
1: { id: 1, name: 'T', age: 40 },
124+
2: { id: 2, name: 'A', age: 30 },
125+
3: { id: 3, name: 'Š', age: 20 },
126+
4: { id: 4, name: 'U', age: 20 },
127+
5: { id: 5, name: 'B', age: 50 },
128+
},
129+
})
130+
131+
const users = userRepo.orderBy('name', 'asc', new Intl.Collator('lt')).get()
132+
133+
const expected = [
134+
{ id: 2, name: 'A', age: 30 },
135+
{ id: 5, name: 'B', age: 50 },
136+
{ id: 3, name: 'Š', age: 20 },
137+
{ id: 1, name: 'T', age: 40 },
138+
{ id: 4, name: 'U', age: 20 },
139+
]
140+
141+
expect(users).toHaveLength(5)
142+
assertInstanceOf(users, User)
143+
assertModels(users, expected)
144+
})
145+
118146
it('can sort nested records by pivot', () => {
119147
Model.clearRegistries()
120148
class User extends Model {

packages/pinia-orm/tests/unit/support/Utils_Order_By.spec.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,30 @@ describe('unit/support/Utils_Order_by', () => {
9898

9999
expect(orderBy(collection, [v => v.id], ['asc'])).toEqual(expected)
100100
})
101+
102+
it('can order collection with a collator', () => {
103+
const collection = [{ name: 'T' }, { name: 'Š' }, { name: 'A' }]
104+
105+
const expected = [{ name: 'A' }, { name: 'Š' }, { name: 'T' }]
106+
107+
expect(orderBy(collection, ['name'], ['asc'], new Intl.Collator('lt'))).toEqual(expected)
108+
})
109+
110+
it('can order collection with different flags per field', () => {
111+
const collection = [
112+
{ name: 'T', group: 'b' },
113+
{ name: 'Š', group: 'B' },
114+
{ name: 'A', group: 'B' },
115+
{ name: 'B', group: 'a' },
116+
]
117+
118+
const expected = [
119+
{ name: 'B', group: 'a' },
120+
{ name: 'A', group: 'B' },
121+
{ name: 'Š', group: 'B' },
122+
{ name: 'T', group: 'b' },
123+
]
124+
125+
expect(orderBy(collection, ['group', 'name'], ['asc', 'asc'], ['SORT_FLAG_CASE', new Intl.Collator('lt')])).toEqual(expected)
126+
})
101127
})

0 commit comments

Comments
 (0)