json-tim
Version:
Functions for timsort with SQL-like json
161 lines (139 loc) • 5.25 kB
JavaScript
export function identity(id, field, record) { return record[field] }
/**
* Peforms timsort on an SQL-like object
* @params {object} json - an SQL-like object
* @params {array} sort - a list of sort specifications
* @params {array} ids - a subset of record ids to sort, if not specified, applied to all
* @params {object} transform - an object of {field: function} pairs which modified the value to use when sorting
* functions of in transform should be defined to take the arguments (id, record, field)
*/
export function timsort(json, sort, ids=undefined, transform={}) {
let use, sortable, ignorable;
use = (ids === undefined) ? Object.keys(json) : ids.concat();
[sortable, ignorable] = sortableRecords(json, use, sort.map(s=>s.field), transform)
// no sort specified, return filtered in order that it was given
if (!sort.length) return use
// setting up the comparison functions fo rthe TimSort
let comparisons = sort.map(({field, isAscending}) => {
// each comparsion function takes the two record ids, extracts their values,
// and then passes it to the simpleSort function
return (id1, id2) => {
const extractor = (field in transform) ? transform[field] : identity
let a = extractor(id1, field, json[id1])
let b = extractor(id2, field, json[id2])
return simpleSort(a, b, isAscending)
}
})
// the actual TimSort
let sorted = sortable.sort((a, b) => {
// while there is no difference, keep trying comparisons
let difference
for (let i = 0; i < comparisons.length; i++) {
difference = comparisons[i](a, b);
if (!difference) continue
else break
}
return difference;
});
// return sorted and tack on the unsortable
return sorted.concat(ignorable)
}
/**
* Determines which records can be sorted (has sortable values)
* @params {object} json - an SQL-like object
* @params {array} ids - a subset of record ids to sort, if not specified, applied to all
* @params {array} fields - a list of fields which occur in each record
* @params {object} transform - an object of {field: function} pairs which modified the value to use when sorting
* functions of in transform should be defined to take the arguments (id, record, field)
*/
export function sortableRecords(json, ids, fields, transform) {
let sortable = [], ignorable = [];
ids.forEach(id => {
let isSortable = isRecordSortable(json, id, fields, transform)
if (isSortable) {
// if all fields have a defined value, this record is sortable
sortable.push(id)
} else {
// at least one field is missing a value, not sortable
ignorable.push(id)
}
})
return [sortable, ignorable]
}
/**
* Determines which if given record can be sorted on specified fields
* @params {object} json - an SQL-like object
* @params {array} id - the record id under consideration
* @params {array} fields - the fields on which to sort
* @params {object} transform - an object of {field: function} pairs which modified the value to use when sorting
* functions of in transform should be defined to take the arguments (id, record, field)
*/
export function isRecordSortable(json, id, fields, transform) {
let anyMissing = fields.some(field => {
let extractor = (field in transform) ? transform[field] : identity
let v = extractor(id, field, json[id])
return (typeof v === 'undefined' || v == null)
})
return !anyMissing
}
/**
* Comparison function for string values.
* Use of the method localeCompare with the following options specified:
* - lang='en'
* - sensitivity='base'
* - ignorePunctuation=true
* @params a - first value
* @params b - second value
* @params {boolean} isAscending - whether or not to sort ascending or descending
*/
export function stringSort(a, b, isAscending) {
const lang = 'en'
const opts = { sensitivity: 'base', ignorePunctuation: true }
return isAscending
? a.localeCompare(b, lang, opts)
: b.localeCompare(a, lang, opts)
}
/**
* Comparison function for numerical values.
* By default JavaScript does _not_ sort numbers numerically.
* @params a - first value
* @params b - second value
* @params {boolean} isAscending - whether or not to sort ascending or descending
*/
export function numberSort(a, b, isAscending) {
return isAscending
? a - b
: b - a
}
export function dateSort(a, b, isAscending) {
return isAscending
? a - b
: b - a
}
/**
* A simple, dynamic sorting function. If both values are are string, compares
* strings; if both numeric, compares magnitude, else returns none.
* @params a - first value
* @params b - second value
* @params {boolean} isAscending - whether or not to sort ascending or descending
*/
export function simpleSort(a, b, isAscending) {
if (typeof a == 'string' && typeof b == 'string') {
return stringSort(a, b, isAscending)
}
else if (typeof a == 'number' && typeof b == 'number') {
return numberSort(a, b, isAscending)
}
else if (
typeof a === 'object' &&
typeof b === 'object' &&
!isNaN(Date.parse(a)) &&
!isNaN(Date.parse(b))
) {
return dateSort(a, b, isAscending)
}
else {
// return numberSort(a, b, isAscending)
return //-Infinity
}
}