diff --git a/docs/content/2.api/1.composables/2.helpers/use-sort-by.md b/docs/content/2.api/1.composables/2.helpers/use-sort-by.md index 77fa467b3..af3c12907 100644 --- a/docs/content/2.api/1.composables/2.helpers/use-sort-by.md +++ b/docs/content/2.api/1.composables/2.helpers/use-sort-by.md @@ -20,6 +20,9 @@ useSortBy(users, 'name') // sort by the `name` attribute case insensitive useSortBy(users, 'name', 'SORT_FLAG_CASE') +// sort by the `name` attribute locale aware +useSortBy(users, 'name', new Intl.Collator('lt')) + // sorts the collection by 'name' descending and then by 'lastname' ascending useSortBy(users, [ ['name', 'desc'], @@ -36,6 +39,7 @@ useSortBy(users, (model) => model.age) ````ts export type sorting = ((record: T) => any) | string | [string, 'asc' | 'desc'][] export type SortFlags = 'SORT_REGULAR' | 'SORT_FLAG_CASE' +export type SortComparator = SortFlags | Intl.Collator -export function useSortBy(collection: T[], sort: sorting, flags?: SortFlags): T[] +export function useSortBy(collection: T[], sort: sorting, flags?: SortComparator): T[] ```` diff --git a/docs/content/2.api/3.query/order-by.md b/docs/content/2.api/3.query/order-by.md index cb39a66b4..8caa937ac 100644 --- a/docs/content/2.api/3.query/order-by.md +++ b/docs/content/2.api/3.query/order-by.md @@ -24,10 +24,21 @@ useRepo(User) // Sort user name by its third character. useRepo(User).orderBy(user => user.name[2]).get() + +// Sort user names case insensitive. +useRepo(User).orderBy('name', 'asc', 'SORT_FLAG_CASE').get() + +// Sort user names locale aware with an Intl.Collator, +// e.g. for Lithuanian: A, B, Š, T, U instead of A, B, T, U, Š. +useRepo(User).orderBy('name', 'asc', new Intl.Collator('lt')).get() ```` +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 })`). + ## Typescript Declarations ````ts -function orderBy(field: OrderBy, direction: OrderDirection = 'asc'): Query +function orderBy(field: OrderBy, direction: OrderDirection = 'asc', flags: SortComparator = 'SORT_REGULAR'): Query + +type SortComparator = 'SORT_REGULAR' | 'SORT_FLAG_CASE' | Intl.Collator ```` diff --git a/packages/pinia-orm/src/composables/collection/useCollect.ts b/packages/pinia-orm/src/composables/collection/useCollect.ts index 2217a5c52..bb8b447db 100644 --- a/packages/pinia-orm/src/composables/collection/useCollect.ts +++ b/packages/pinia-orm/src/composables/collection/useCollect.ts @@ -1,5 +1,5 @@ import type { Collection, Model } from '../../../src' -import type { SortFlags } from '../../support/Utils' +import type { SortComparator } from '../../support/Utils' import { useSum } from './useSum' import { useMax } from './useMax' import { useMin } from './useMin' @@ -15,7 +15,7 @@ export interface UseCollect { max: (field: string) => number pluck: (field: string) => any[] groupBy: (fields: string[] | string) => Record> - sortBy: (sort: sorting, flags?: SortFlags) => M[] + sortBy: (sort: sorting, flags?: SortComparator) => M[] keys: () => string[] } @@ -29,7 +29,7 @@ export function useCollect (models: Collection): Use max: field => useMax(models, field), pluck: field => usePluck(models, field), groupBy: fields => useGroupBy(models, fields), - sortBy: (sort, flags: SortFlags = 'SORT_REGULAR') => useSortBy(models, sort, flags), + sortBy: (sort, flags: SortComparator = 'SORT_REGULAR') => useSortBy(models, sort, flags), keys: () => useKeys(models), } } diff --git a/packages/pinia-orm/src/composables/collection/useSortBy.ts b/packages/pinia-orm/src/composables/collection/useSortBy.ts index e547498ae..a8d3b57ec 100644 --- a/packages/pinia-orm/src/composables/collection/useSortBy.ts +++ b/packages/pinia-orm/src/composables/collection/useSortBy.ts @@ -1,4 +1,4 @@ -import type { SortFlags } from '../../support/Utils' +import type { SortComparator } from '../../support/Utils' import { orderBy } from '../../support/Utils' export type sorting = ((record: T) => any) | string | [string, 'asc' | 'desc'][] @@ -7,7 +7,7 @@ export type sorting = ((record: T) => any) | string | [string, 'asc' | 'desc' * Creates an array of elements, sorted in specified order by the results * of running each element in a collection thru each iteratee. */ -export function useSortBy> (collection: T[], sort: sorting, flags?: SortFlags): T[] { +export function useSortBy> (collection: T[], sort: sorting, flags?: SortComparator): T[] { const directions = [] const iteratees = [] diff --git a/packages/pinia-orm/src/query/Options.ts b/packages/pinia-orm/src/query/Options.ts index 0b9932c19..f0805ad41 100644 --- a/packages/pinia-orm/src/query/Options.ts +++ b/packages/pinia-orm/src/query/Options.ts @@ -1,4 +1,5 @@ import type { Model, WithKeys } from '../model/Model' +import type { SortComparator } from '../support/Utils' import type { Query } from './Query' export interface Where { @@ -23,6 +24,7 @@ export interface WhereGroup { export interface Order { field: OrderBy direction: OrderDirection + flags?: SortComparator } export interface Group { diff --git a/packages/pinia-orm/src/query/Query.ts b/packages/pinia-orm/src/query/Query.ts index 1784529ea..8b68cf3f0 100644 --- a/packages/pinia-orm/src/query/Query.ts +++ b/packages/pinia-orm/src/query/Query.ts @@ -9,6 +9,7 @@ import { isFunction, orderBy, } from '../support/Utils' +import type { SortComparator } from '../support/Utils' import type { Collection, Element, Elements, GroupedCollection, Item, NormalizedData } from '../data/Data' import type { Database } from '../database/Database' import { Relation } from '../model/attributes/relations/Relation' @@ -359,8 +360,8 @@ export class Query { /** * Add an "order by" clause to the query. */ - orderBy (field: OrderBy, direction: OrderDirection = 'asc'): this { - this.orders.push({ field, direction }) + orderBy (field: OrderBy, direction: OrderDirection = 'asc', flags: SortComparator = 'SORT_REGULAR'): this { + this.orders.push({ field, direction, flags }) return this } @@ -625,8 +626,9 @@ export class Query { protected filterOrder (models: Collection): Collection { const fields = this.orders.map(order => order.field) const directions = this.orders.map(order => order.direction) + const flags = this.orders.map(order => order.flags ?? 'SORT_REGULAR') - return orderBy(models, fields, directions) + return orderBy(models, fields, directions, flags) } /** diff --git a/packages/pinia-orm/src/repository/Repository.ts b/packages/pinia-orm/src/repository/Repository.ts index 8d4bd7617..dadd589ed 100644 --- a/packages/pinia-orm/src/repository/Repository.ts +++ b/packages/pinia-orm/src/repository/Repository.ts @@ -1,6 +1,7 @@ import type { Pinia } from 'pinia' import type { Constructor } from '../types' import { assert, isArray } from '../support/Utils' +import type { SortComparator } from '../support/Utils' import type { Collection, Element, Item } from '../data/Data' import type { Database } from '../database/Database' import type { Model, WithKeys } from '../model/Model' @@ -332,8 +333,8 @@ export class Repository { /** * Add an "order by" clause to the query. */ - orderBy (field: OrderBy, direction?: OrderDirection): Query { - return this.query().orderBy(field, direction) + orderBy (field: OrderBy, direction?: OrderDirection, flags?: SortComparator): Query { + return this.query().orderBy(field, direction, flags) } /** diff --git a/packages/pinia-orm/src/support/Utils.ts b/packages/pinia-orm/src/support/Utils.ts index dd95a5167..f0c16f52a 100644 --- a/packages/pinia-orm/src/support/Utils.ts +++ b/packages/pinia-orm/src/support/Utils.ts @@ -8,6 +8,8 @@ interface SortableArray { export type SortFlags = 'SORT_REGULAR' | 'SORT_FLAG_CASE' +export type SortComparator = SortFlags | Intl.Collator + /** * Compare two values with custom string operator */ @@ -77,7 +79,7 @@ export function orderBy ( collection: T[], iteratees: (((record: T) => any) | string)[], directions: string[], - flags: SortFlags = 'SORT_REGULAR', + flags: SortComparator | SortComparator[] = 'SORT_REGULAR', ): T[] { let index = -1 @@ -130,7 +132,7 @@ function compareMultiple ( object: SortableArray, other: SortableArray, directions: string[], - flags: SortFlags, + flags: SortComparator | SortComparator[], ): number { let index = -1 @@ -139,7 +141,7 @@ function compareMultiple ( const length = objCriteria.length while (++index < length) { - const result = compareAscending(objCriteria[index], othCriteria[index], flags) + const result = compareAscending(objCriteria[index], othCriteria[index], isArray(flags) ? flags[index] ?? 'SORT_REGULAR' : flags) if (result) { const direction = directions[index] @@ -153,8 +155,13 @@ function compareMultiple ( /** * Compares values to sort them in ascending order. */ -function compareAscending (value: any, other: any, flags: SortFlags): number { +function compareAscending (value: any, other: any, flags: SortComparator): number { if (value !== other) { + if (typeof flags === 'object' && typeof value === 'string' && typeof other === 'string') { + const result = flags.compare(value, other) + return result > 0 ? 1 : result < 0 ? -1 : 0 + } + const valIsDefined = value !== undefined const valIsNull = value === null const valIsReflexive = value === value diff --git a/packages/pinia-orm/tests/feature/repository/retrieves_order_by.spec.ts b/packages/pinia-orm/tests/feature/repository/retrieves_order_by.spec.ts index 12922fcc9..656063f2c 100644 --- a/packages/pinia-orm/tests/feature/repository/retrieves_order_by.spec.ts +++ b/packages/pinia-orm/tests/feature/repository/retrieves_order_by.spec.ts @@ -115,6 +115,34 @@ describe('feature/repository/retrieves_order_by', () => { assertModels(users, expected) }) + it('can sort records locale aware with a collator', () => { + const userRepo = useRepo(User) + + fillState({ + users: { + 1: { id: 1, name: 'T', age: 40 }, + 2: { id: 2, name: 'A', age: 30 }, + 3: { id: 3, name: 'Š', age: 20 }, + 4: { id: 4, name: 'U', age: 20 }, + 5: { id: 5, name: 'B', age: 50 }, + }, + }) + + const users = userRepo.orderBy('name', 'asc', new Intl.Collator('lt')).get() + + const expected = [ + { id: 2, name: 'A', age: 30 }, + { id: 5, name: 'B', age: 50 }, + { id: 3, name: 'Š', age: 20 }, + { id: 1, name: 'T', age: 40 }, + { id: 4, name: 'U', age: 20 }, + ] + + expect(users).toHaveLength(5) + assertInstanceOf(users, User) + assertModels(users, expected) + }) + it('can sort nested records by pivot', () => { Model.clearRegistries() class User extends Model { diff --git a/packages/pinia-orm/tests/unit/support/Utils_Order_By.spec.ts b/packages/pinia-orm/tests/unit/support/Utils_Order_By.spec.ts index 5cdad1d24..928fa793b 100644 --- a/packages/pinia-orm/tests/unit/support/Utils_Order_By.spec.ts +++ b/packages/pinia-orm/tests/unit/support/Utils_Order_By.spec.ts @@ -98,4 +98,30 @@ describe('unit/support/Utils_Order_by', () => { expect(orderBy(collection, [v => v.id], ['asc'])).toEqual(expected) }) + + it('can order collection with a collator', () => { + const collection = [{ name: 'T' }, { name: 'Š' }, { name: 'A' }] + + const expected = [{ name: 'A' }, { name: 'Š' }, { name: 'T' }] + + expect(orderBy(collection, ['name'], ['asc'], new Intl.Collator('lt'))).toEqual(expected) + }) + + it('can order collection with different flags per field', () => { + const collection = [ + { name: 'T', group: 'b' }, + { name: 'Š', group: 'B' }, + { name: 'A', group: 'B' }, + { name: 'B', group: 'a' }, + ] + + const expected = [ + { name: 'B', group: 'a' }, + { name: 'A', group: 'B' }, + { name: 'Š', group: 'B' }, + { name: 'T', group: 'b' }, + ] + + expect(orderBy(collection, ['group', 'name'], ['asc', 'asc'], ['SORT_FLAG_CASE', new Intl.Collator('lt')])).toEqual(expected) + }) })