Using an ordering function when sorting

Revision as of 15:59, 16 September 2026 by Lchrisman (talk | contribs) (The ordering function: formatting)

New to Analytica 7.2

Release:

 • 4.6 •  5.0 •  5.1 •  5.2 •  5.3 •  5.4 •  6.0 •  6.1 •  6.2 •  6.3 •  6.4 •  6.5 •   •  6.6 •  7.0 •  7.1 •  7.2

The sorting functions Sort, SortIndex and Rank normally order values using Analytica's built-in ordering: numbers numerically, text alphabetically, and the various data types in a fixed relative order (see SortIndex#Descending). Occasionally that isn't the order you want. Each of these functions accepts an optional «lessThan» parameter: an ordering function of your own that decides, for any two items «x» and «y», whether «x» comes before «y».

The ordering function

An ordering function accepts two parameters and returns true when the first item should be ordered before the second, and false otherwise (including when the two are equivalent). You can pass:

  • a local (lambda) function written in place, e.g. lessThan: (Function(x, y) ::= x->age < y->age)
  • the name of (or a handle to) a User-Defined Function, e.g. lessThan: VersionLess
  • a Python callable

For example, version numbers written as text sort alphabetically, which puts "1.10.0" before "1.9.0". An ordering function that compares them part by part fixes this:

Function VersionKey(v : text atom) ::= 
Local p := SplitText(v, '.'); 
ParseNumber(Slice(p,1))*1e6 + ParseNumber(Slice(p,2))*1e3 + ParseNumber(Slice(p,3))
Function VersionLess(x, y : text atom) ::= VersionKey(x) < VersionKey(y)
SortIndex(['1.9.0', '1.10.0', '1.2.3', '2.0.0'], lessThan: VersionLess)
['1.2.3', '1.9.0', '1.10.0', '2.0.0']

The same «lessThan» parameter works in all three functions:

Sort(Versions, I, lessThan: VersionLess)
Rank(Versions, I, lessThan: VersionLess)

Requirements on the ordering

The function must define a consistent ordering: if it says «x» comes before «y», it must not also say «y» comes before «x», and if «x» is before «y» and «y» is before «z» then «x» must be before «z» (a strict weak ordering). Items for which neither lessThan(x, y) nor lessThan(y, x) is true are tied. Ties keep their original order (the sort is stable), and Rank gives tied items the same rank (as controlled by its «type» parameter). An inconsistent ordering function won't crash, but the resulting order is arbitrary.

A "depends on" relation in a graph is a partial order, not a total one, so it doesn't qualify directly. To sort the nodes of a dependency graph in dependency order, compute a depth (or level) for each node first and compare depths.

What is passed to the function

What «x» and «y» hold depends on how you declare the ordering function's first two parameters.

Atoms (the usual case)

When the parameters are undeclared, (x, y), or declared as atoms, e.g. (x, y : atom) or (x, y : text atom), the function receives one item at a time. If the array being sorted has indexes besides the sort index «I», Sort, SortIndex or Rank array abstracts over them in the usual way, sorting each slice separately, and the ordering function only ever sees atoms. For example, with A indexed by I, J and K, Sort(A, I, lessThan: f) sorts each [J, K] slice along I.

When you also specify a «keyIndex» (a multi-key sort), an atom-valued ordering function is applied key by key, exactly as the built-in comparison is: the primary key (the first element along «keyIndex») decides, unless the function considers the two items tied, in which case the next key breaks the tie, and so on.

Whole key vectors

For a multi-key sort you may instead want to see all of the keys at once. Declare the first two parameters as 1-D vectors along the key index, and the function receives, for each of the two items, its entire vector of keys along «keyIndex»:

Sort(Data, Person, keyIndex: K, lessThan: (Function(x, y : [K]) ::= Sum(x, K) < Sum(y, K)))

The index named in the declaration must be the «keyIndex» itself, or an error results. A user-defined function can instead take the key index as a parameter declared as Index, which receives the «keyIndex»:

Function RowSumLess(x, y : [KI] ; KI : Index) := Sum(x, KI) < Sum(y, KI)
Sort(Data, Person, keyIndex: K, lessThan: RowSumLess)

A parameter declared as Vector also receives the whole key vector. «keyIndex» is required for this form, and since the function then decides the order from all of the keys at once, «descending» must be a single boolean rather than an array indexed by «keyIndex».

Only atoms, or 1-D vectors along «keyIndex», are ever passed. The dimensionality you declare for the ordering function never changes how Sort, SortIndex or Rank array abstract (that is fixed by their own parameter declarations), so a declaration such as (x, y : [J, K]) is an error. To compare arbitrary slices, sort references to them instead:

Sort(\[J, K] A, I, lessThan: (Function(rx, ry : reference atom) ::= Sum(#rx, J, K) < Sum(#ry, J, K)))

Other parameters

Beyond the first two parameters, the sorting function fills in only:

  • a parameter declared as Index, which receives «keyIndex» (as above), and
  • a parameter named «caseInsensitive», which receives the value passed to the sorting function's own «caseInsensitive» parameter.

Any other parameter of the ordering function must be optional.

Null values

When an item is Null and the ordering function's parameters are declared with an atomic data type that excludes Null, such as (x, y : text atom), the call short-circuits and returns Null without evaluating the function body, which is the standard behavior for atomic parameters. Whenever the ordering function returns Null (or Undefined) for a pair of items, the sorting function falls back to the built-in ordering for that pair, which places Null after everything else (or first when descending: true). So an ordering function with typed parameters gets the standard treatment of Null for free.

To decide the placement of Null yourself, leave the parameters untyped (or declare them orNull) so that Null is passed in. This puts Null values first:

SortIndex(D, I, lessThan: (Function(x, y) ::= (x = Null and y <> Null) or (x <> Null and y <> Null and x < y)))

Descending order

descending: true reverses the order that the function defines, so you don't need to write a second function for the reverse order. In a multi-key sort with an atom-valued ordering function, «descending» may be indexed by «keyIndex» as usual.

Examples

Some orderings that are much easier with an ordering function:

  • Version numbers, so that 1.10.0 comes after 1.9.0 (above).
  • Natural order for names with a number appended, so that x2 comes before x10:
Function NatLess(x, y : text atom) := Local px := SelectText(x, 1, 1); Local py := SelectText(y, 1, 1); px < py or (px = py and ParseNumber(SelectText(x, 2)) < ParseNumber(SelectText(y, 2)))
  • Pinning selected items to the top, with the rest in alphabetical order:
Function PinLess(x, y : text atom) := Local px := Sum(Pinned = x, Pinned) > 0; Local py := Sum(Pinned = y, Pinned) > 0; (px and not py) or (px = py and x < y)
  • Changing the relative order of data types, e.g. text before numbers:
SortIndex(D, I, lessThan: (Function(a, b) ::= (IsText(a) and not IsText(b)) or (IsText(a) = IsText(b) and a < b)))
  • Ordering Struct instances by a member:
Sort(People, I, lessThan: (Function(x, y) ::= x->age < y->age))
  • Sorting file paths by the file name only, keeping the full path:
Function FileNameOf(p : text atom) := Local parts := SplitText(p, '\'); Slice(parts, Size(parts))
SortIndex(Paths, lessThan: (Function(x, y) ::= FileNameOf(x) < FileNameOf(y)))
  • Sorting handles to objects by an attribute:
SortIndex(Objs, lessThan: (Function(x, y) ::= (Title of x) < (Title of y)))

Other orderings, such as an enumerated order (S, M, L, XL) or ordering by text length, are also easy without an ordering function, e.g. SortIndex(@[Sizes = x], I) or SortIndex(TextLength(x), I). The «lessThan» form is still convenient with Sort and Rank, where the values themselves stay in place.

Performance

An ordering function is called O(n log n) times, and each call evaluates an Analytica expression, so sorting with one is considerably slower than the built-in comparison (on the order of 105 comparisons per second). When the order can be expressed as a computed key, SortIndex(key, I) is faster. Sorts that use an ordering function run single-threaded.

History

See Also

Comments
Loading comments...