modules/list/util.zzm

list-util-0.0.1 source code

Package

Name
list-util
Version
0.0.1
Uploaded
2026-05-28 09:57:26
Metadata
zuzu-distribution.json
Archive
Download .tar.gz
=encoding utf8

=head1 NAME

list/util - List utility functions.

=head1 SYNOPSIS

  from list/util import grep, head, map, sortnum_by, sum, ListUtil;
  
  say( sum( [ 10, 20, 30 ] ) );
  
  let users := [
      { name: "Zoe", age: 32, score: 87, active: true },
      { name: "Ada", age: 41, score: 98, active: true },
      { name: "Max", age: 27, score: 91, active: false },
      { name: "Bea", age: 36, score: 93, active: true },
  ];
  
  let by_name := ListUtil.sortstr_by( users, fn user → user{name} );
  let oldest  := ListUtil.max_by( users, fn user → user{age} );
  
  let leaderboard := users
      ▷ grep( ^^, fn user → user{active} )
      ▷ sortnum_by( ^^, fn user → user{score} )
      ▷ reverse( ^^ )
      ▷ head( ^^, 3 )
      ▷ map( ^^, fn user → user{name} );
  
  say( leaderboard );  // Ada, Bea, Zoe

=head1 DESCRIPTION

This pure-Zuzu module provides functions inspired by Perl's
C<List::Util>, C<List::MoreUtils>, and C<List::UtilsBy>. Functions are
exported directly, and the C<ListUtil> class provides static wrappers
for code that wants to avoid importing many names.

Most functions take the collection first. Predicate callbacks receive
one value. Reducer callbacks receive C<(accumulator, value)>. Key
callbacks for C<*_by> functions receive one value and are called once
per input value. Pair callbacks receive a Zuzu C<Pair> object.

=head2 Collection Support

C<Array> is the primary input type.

C<Bag> and C<Set> are accepted by order-insensitive functions. Their
iteration order is the order returned by the runtime's C<to_Array>
method and should not be treated as stable unless the runtime documents
it.

Ordered and position-sensitive functions require C<Array>. Pair helpers
which operate on pairs expect an C<Array> of C<Pair> objects.

=head1 FUNCTIONS

=head2 Reduction

=over

=item C<< reduce(Array values, Function callback) >>

Calls C<callback(accumulator, value)> from left to right. Returns
C<null> for an empty array.

  reduce( [ 1, 2, 3 ], fn ( a, b ) → a + b );  // 6

=item C<< reductions(Array values, Function callback) >>

Returns every intermediate accumulator value.

  reductions( [ 1, 2, 3 ], fn ( a, b ) → a + b );  // [ 1, 3, 6 ]

=back

=head2 Predicates And Searches

=over

=item C<< any(Array|Bag|Set values, Function predicate) >>

Returns true if any value satisfies C<predicate(value)>.

  any( [ 1, 2, 3 ], fn x → x > 2 );  // true

=item C<< all(Array|Bag|Set values, Function predicate) >>

Returns true if every value satisfies C<predicate(value)>.

  all( [ 2, 4, 6 ], fn x → x mod 2 = 0 );  // true

=item C<< none(Array|Bag|Set values, Function predicate) >>

Returns true if no value satisfies C<predicate(value)>.

  none( [ 1, 3, 5 ], fn x → x mod 2 = 0 );  // true

=item C<< notall(Array|Bag|Set values, Function predicate) >>

Returns true if at least one value does not satisfy C<predicate(value)>.

  notall( [ 2, 4, 5 ], fn x → x mod 2 = 0 );  // true

=item C<< first(Array values, Function predicate) >>

Returns the first matching value, or C<null>.

  first( [ 1, 2, 3 ], fn x → x > 1 );  // 2

=item C<< firstval(Array values, Function predicate) >>

Alias-style helper equivalent to C<first>.

  firstval( [ "a", "bb" ], fn x → length(x) > 1 );  // "bb"

=item C<< firstidx(Array values, Function predicate) >>

Returns the first matching index, or C<-1>.

  firstidx( [ 1, 4, 9 ], fn x → x > 3 );  // 1

=item C<< lastval(Array values, Function predicate) >>

Returns the last matching value, or C<null>.

  lastval( [ 1, 4, 9 ], fn x → x > 3 );  // 9

=item C<< lastidx(Array values, Function predicate) >>

Returns the last matching index, or C<-1>.

  lastidx( [ 1, 4, 9 ], fn x → x > 3 );  // 2

=item C<< onlyval(Array|Bag|Set values, Function predicate) >>

Returns the value if exactly one value matches, otherwise C<null>.

  onlyval( [ 1, 2, 3 ], fn x → x = 2 );  // 2

=item C<< onlyidx(Array values, Function predicate) >>

Returns the index if exactly one value matches, otherwise C<-1>.

  onlyidx( [ 1, 2, 3 ], fn x → x = 2 );  // 1

=back

=head2 Numeric And String Values

=over

=item C<< max(Array|Bag|Set values) >>

Returns the numerically greatest value, or C<null>.

  max( [ 7, 2, 10 ] );  // 10

=item C<< min(Array|Bag|Set values) >>

Returns the numerically smallest value, or C<null>.

  min( [ 7, 2, 10 ] );  // 2

=item C<< maxstr(Array|Bag|Set values) >>

Returns the string-greatest value, or C<null>.

  maxstr( [ "b", "aa" ] );  // "b"

=item C<< minstr(Array|Bag|Set values) >>

Returns the string-smallest value, or C<null>.

  minstr( [ "b", "aa" ] );  // "aa"

=item C<< sum(Array|Bag|Set values) >>

Returns the numeric sum, or C<null> for an empty collection.

  sum( [ 1, "2", 3 ] );  // 6

=item C<< sum0(Array|Bag|Set values) >>

Returns the numeric sum, using C<0> for an empty collection.

  sum0( [] );  // 0

=item C<< product(Array|Bag|Set values) >>

Returns the numeric product, using C<1> for an empty collection.

  product( [ 2, "3", 4 ] );  // 24

=back

=head2 Pairs

=over

=item C<< pairs(Array flat) >>

Converts a flat key/value array into an array of C<Pair> objects.

  pairs( [ "a", 1, "b", 2 ] );

=item C<< unpairs(Array pair_objects) >>

Converts an array of C<Pair> objects into a flat key/value array.

  unpairs( pairs( [ "a", 1 ] ) );  // [ "a", 1 ]

=item C<< pairkeys(Array pair_objects) >>

Returns pair keys.

  pairkeys( pairs( [ "a", 1, "b", 2 ] ) );  // [ "a", "b" ]

=item C<< pairvalues(Array pair_objects) >>

Returns pair values.

  pairvalues( pairs( [ "a", 1, "b", 2 ] ) );  // [ 1, 2 ]

=item C<< pairfirst(Array pair_objects, Function predicate) >>

Returns the first C<Pair> where C<predicate(pair)> is true, or C<null>.

  pairfirst( pairs( [ "a", 1 ] ), fn p → p.key eq "a" );

=item C<< pairgrep(Array pair_objects, Function predicate) >>

Returns all C<Pair> objects where C<predicate(pair)> is true.

  pairgrep( pairs( [ "a", 1, "b", 2 ] ), fn p → p.value > 1 );

=item C<< pairmap(Array pair_objects, Function mapper) >>

Maps each C<Pair> using C<mapper(pair)>.

  pairmap( pairs( [ "a", 1 ] ), fn p → p.key _ "=" _ p.value );

=back

=head2 Ordering And Uniqueness

=over

=item C<< sort(Array values, Function comparator) >>

Returns a sorted array using C<comparator(left, right)>.

  sort( [ 1, 3, 2 ], fn ( a, b ) → a ≶ b );  // [ 1, 2, 3 ]

=item C<< sortnum(Array|Bag|Set values) >>

Returns values sorted numerically.

  sortnum( [ "2", "10", "1" ] );  // [ "1", "2", "10" ]

=item C<< sortstr(Array|Bag|Set values) >>

Returns values sorted stringwise.

  sortstr( [ "b", "aa" ] );  // [ "aa", "b" ]

=item C<< reverse(Array|Bag|Set values) >>

Returns values in reverse order.

  reverse( [ 1, 2, 3 ] );  // [ 3, 2, 1 ]

=item C<< map(Array|Bag|Set values, Function mapper) >>

Returns C<mapper(value)> for each value.

  map( [ 1, 2, 3 ], fn x → x × 2 );  // [ 2, 4, 6 ]

=item C<< grep(Array|Bag|Set values, Function predicate) >>

Returns values where C<predicate(value)> is true.

  grep( [ 1, 2, 3 ], fn x → x > 1 );  // [ 2, 3 ]

=item C<< shuffle(Array|Bag|Set values) >>

Returns the values in random order.

  shuffle( [ 1, 2, 3 ] );

=item C<< sample(Array|Bag|Set values, Number count) >>

Returns up to C<count> random values in random order, without
replacement. If C<count> is larger than the input collection, all values
are returned in random order.

  sample( [ 1, 2, 3 ], 2 );

=item C<< uniq(Array|Bag|Set values) >>

Returns first-seen unique values using Zuzu equality.

  uniq( [ 1, 1, 2 ] );  // [ 1, 2 ]

=item C<< uniqint(Array|Bag|Set values) >>

Returns first-seen values unique by integer value.

  uniqint( [ 1.1, 1.9, 2.1 ] );  // [ 1.1, 2.1 ]

=item C<< uniqnum(Array|Bag|Set values) >>

Returns first-seen values unique by numeric value.

  uniqnum( [ "1", 1, 2 ] );  // [ "1", 2 ]

=item C<< uniqstr(Array|Bag|Set values) >>

Returns first-seen values unique by string value.

  uniqstr( [ 1, "1", 2 ] );  // [ 1, 2 ]

=item C<< head(Array values, Number count := 1) >>

Returns the first C<count> values.

  head( [ 1, 2, 3 ], 2 );  // [ 1, 2 ]

=item C<< tail(Array values, Number count := 1) >>

Returns the last C<count> values.

  tail( [ 1, 2, 3 ], 2 );  // [ 2, 3 ]

=item C<< zip(Array first, Array second, ...) >>

Returns rows formed from the given arrays, using C<null> where an input
array is short.

  zip( [ "a", "b" ], [ 1, 2 ] );  // [ [ "a", 1 ], [ "b", 2 ] ]

=item C<< mesh(Array first, Array second, ...) >>

Returns the same values as C<zip>, flattened.

  mesh( [ "a", "b" ], [ 1, 2 ] );  // [ "a", 1, "b", 2 ]

=back

=head2 Keyed Helpers

=over

=item C<< sort_by(Array|Bag|Set values, Function key_function) >>

Sorts values numerically by C<key_function(value)>. The key function is
called once per value, using a Schwartzian transform.

  sort_by( users, fn user → user{age} );

=item C<< sortnum_by(Array|Bag|Set values, Function key_function) >>

Alias for C<sort_by>.

  sortnum_by( users, fn user → user{age} );

=item C<< sortstr_by(Array|Bag|Set values, Function key_function) >>

Sorts values stringwise by C<key_function(value)>.

  sortstr_by( users, fn user → user{name} );

=item C<< max_by(Array|Bag|Set values, Function key_function) >>

Returns the value with the greatest numeric key. The key function is
called once per value.

  max_by( users, fn user → user{age} );

=item C<< maxnum_by(Array|Bag|Set values, Function key_function) >>

Alias for C<max_by>.

  maxnum_by( users, fn user → user{age} );

=item C<< maxstr_by(Array|Bag|Set values, Function key_function) >>

Returns the value with the greatest string key.

  maxstr_by( users, fn user → user{name} );

=item C<< min_by(Array|Bag|Set values, Function key_function) >>

Returns the value with the smallest numeric key. The key function is
called once per value.

  min_by( users, fn user → user{age} );

=item C<< minnum_by(Array|Bag|Set values, Function key_function) >>

Alias for C<min_by>.

  minnum_by( users, fn user → user{age} );

=item C<< minstr_by(Array|Bag|Set values, Function key_function) >>

Returns the value with the smallest string key.

  minstr_by( users, fn user → user{name} );

=item C<< uniq_by(Array|Bag|Set values, Function key_function) >>

Returns first-seen values unique by string key. The key function is
called once per value.

  uniq_by( users, fn user → user{name} );

=back

=head1 CLASS

=head2 C<ListUtil>

C<ListUtil> provides every exported function as a static method with
the same signature.

  from list/util import ListUtil;
  say( ListUtil.sum( [ 1, 2, 3 ] ) );

=head1 COPYRIGHT AND LICENCE

B<< list/util >> is copyright Toby Inkster.

It is free software; you may redistribute it and/or modify it under
the terms of either the Artistic License 1.0 or the GNU General Public
License version 2.

=cut

function _ordered_array ( values, String name ) {
	if ( typeof values ne "Array" ) {
		die name _ " expects an Array";
	}
	return values;
}

function _collection_array ( values, String name ) {
	if ( typeof values eq "Array" ) {
		return values;
	}
	if ( typeof values eq "Bag" ⋁ typeof values eq "Set" ) {
		return values.to_Array;
	}
	die name _ " expects an Array, Bag, or Set";
}

function _pair_array ( values, String name ) {
	let out := _ordered_array( values, name );
	for ( let pair in out ) {
		if ( ¬ ( pair instanceof Pair ) ) {
			die name _ " expects an Array of Pair objects";
		}
	}
	return out;
}

function _key_string ( value ) {
	return "" _ value;
}

function _key_number ( value ) {
	return 0 + value;
}

function _key_integer ( value ) {
	return int(value);
}

function _array_contains_value ( Array values, value ) {
	for ( let existing in values ) {
		return true if existing ≡ value;
	}
	return false;
}

function _array_contains_key ( Array keys, key ) {
	for ( let existing in keys ) {
		return true if existing eq key;
	}
	return false;
}

function reduce ( values, Function callback ) {
	let items := _ordered_array( values, "reduce" );
	return null if items.empty;

	let accumulator := items[0];
	let i := 1;
	while ( i < items.length ) {
		accumulator := callback( accumulator, items[i] );
		i++;
	}
	return accumulator;
}

function reductions ( values, Function callback ) {
	let items := _ordered_array( values, "reductions" );
	let out := [];
	return out if items.empty;

	let accumulator := items[0];
	out.push(accumulator);
	let i := 1;
	while ( i < items.length ) {
		accumulator := callback( accumulator, items[i] );
		out.push(accumulator);
		i++;
	}
	return out;
}

function any ( values, Function predicate ) {
	for ( let value in _collection_array( values, "any" ) ) {
		return true if predicate(value);
	}
	return false;
}

function all ( values, Function predicate ) {
	for ( let value in _collection_array( values, "all" ) ) {
		return false if ¬ predicate(value);
	}
	return true;
}

function none ( values, Function predicate ) {
	return ¬ any( values, predicate );
}

function notall ( values, Function predicate ) {
	return ¬ all( values, predicate );
}

function first ( values, Function predicate ) {
	for ( let value in _ordered_array( values, "first" ) ) {
		return value if predicate(value);
	}
	return null;
}

function max ( values ) {
	let items := _collection_array( values, "max" );
	return null if items.empty;

	let best := items[0];
	let i := 1;
	while ( i < items.length ) {
		best := items[i] if _key_number( items[i] ) > _key_number(best);
		i++;
	}
	return best;
}

function maxstr ( values ) {
	let items := _collection_array( values, "maxstr" );
	return null if items.empty;

	let best := items[0];
	let i := 1;
	while ( i < items.length ) {
		best := items[i] if _key_string( items[i] ) gt _key_string(best);
		i++;
	}
	return best;
}

function min ( values ) {
	let items := _collection_array( values, "min" );
	return null if items.empty;

	let best := items[0];
	let i := 1;
	while ( i < items.length ) {
		best := items[i] if _key_number( items[i] ) < _key_number(best);
		i++;
	}
	return best;
}

function minstr ( values ) {
	let items := _collection_array( values, "minstr" );
	return null if items.empty;

	let best := items[0];
	let i := 1;
	while ( i < items.length ) {
		best := items[i] if _key_string( items[i] ) lt _key_string(best);
		i++;
	}
	return best;
}

function product ( values ) {
	let total := 1;
	for ( let value in _collection_array( values, "product" ) ) {
		total ×= _key_number(value);
	}
	return total;
}

function sum ( values ) {
	let items := _collection_array( values, "sum" );
	return null if items.empty;

	let total := 0;
	for ( let value in items ) {
		total += _key_number(value);
	}
	return total;
}

function sum0 ( values ) {
	let total := 0;
	for ( let value in _collection_array( values, "sum0" ) ) {
		total += _key_number(value);
	}
	return total;
}

function pairs ( values ) {
	let items := _ordered_array( values, "pairs" );
	let out := [];
	let i := 0;
	while ( i < items.length ) {
		out.push(
			new Pair(
				pair: [
					items[i],
					i + 1 < items.length ? items[i + 1] : null,
				],
			),
		);
		i += 2;
	}
	return out;
}

function unpairs ( values ) {
	let out := [];
	for ( let pair in _pair_array( values, "unpairs" ) ) {
		out.push( pair.key );
		out.push( pair.value );
	}
	return out;
}

function pairkeys ( values ) {
	return _pair_array( values, "pairkeys" ).map( fn p → p.key );
}

function pairvalues ( values ) {
	return _pair_array( values, "pairvalues" ).map( fn p → p.value );
}

function pairfirst ( values, Function predicate ) {
	for ( let pair in _pair_array( values, "pairfirst" ) ) {
		return pair if predicate(pair);
	}
	return null;
}

function pairgrep ( values, Function predicate ) {
	let out := [];
	for ( let pair in _pair_array( values, "pairgrep" ) ) {
		out.push(pair) if predicate(pair);
	}
	return out;
}

function pairmap ( values, Function mapper ) {
	let out := [];
	for ( let pair in _pair_array( values, "pairmap" ) ) {
		out.push( mapper(pair) );
	}
	return out;
}

function sort ( values, Function comparator ) {
	return _ordered_array( values, "sort" ).sort(comparator);
}

function sortnum ( values ) {
	return _collection_array( values, "sortnum" ).sortnum;
}

function sortstr ( values ) {
	return _collection_array( values, "sortstr" ).sortstr;
}

function reverse ( values ) {
	return _collection_array( values, "reverse" ).reverse;
}

function map ( values, Function mapper ) {
	let out := [];
	for ( let value in _collection_array( values, "map" ) ) {
		out.push( mapper(value) );
	}
	return out;
}

function grep ( values, Function predicate ) {
	let out := [];
	for ( let value in _collection_array( values, "grep" ) ) {
		out.push(value) if predicate(value);
	}
	return out;
}

function shuffle ( values ) {
	return _collection_array( values, "shuffle" ).shuffle;
}

function sample ( values, Number count ) {
	let shuffled := _collection_array( values, "sample" ).shuffle;
	let limit := count < 0 ? 0 : count;
	limit := shuffled.length if limit > shuffled.length;

	let out := [];
	let i := 0;
	while ( i < limit ) {
		out.push( shuffled[i] );
		i++;
	}
	return out;
}

function uniq ( values ) {
	let out := [];
	for ( let value in _collection_array( values, "uniq" ) ) {
		out.push(value) if ¬ _array_contains_value( out, value );
	}
	return out;
}

function _uniq_by_key ( values, Function key_function, String name ) {
	let out := [];
	let keys := [];
	for ( let value in _collection_array( values, name ) ) {
		let key := _key_string( key_function(value) );
		if ( ¬ _array_contains_key( keys, key ) ) {
			keys.push(key);
			out.push(value);
		}
	}
	return out;
}

function uniqint ( values ) {
	return _uniq_by_key( values, fn value → _key_integer(value), "uniqint" );
}

function uniqnum ( values ) {
	return _uniq_by_key( values, fn value → _key_number(value), "uniqnum" );
}

function uniqstr ( values ) {
	return _uniq_by_key( values, fn value → _key_string(value), "uniqstr" );
}

function head ( values, Number count := 1 ) {
	let items := _ordered_array( values, "head" );
	let out := [];
	let limit := count > items.length ? items.length : count;
	let i := 0;
	while ( i < limit ) {
		out.push( items[i] );
		i++;
	}
	return out;
}

function tail ( values, Number count := 1 ) {
	let items := _ordered_array( values, "tail" );
	let start := items.length - count;
	start := 0 if start < 0;

	let out := [];
	let i := start;
	while ( i < items.length ) {
		out.push( items[i] );
		i++;
	}
	return out;
}

function _validate_zip_inputs ( Array lists, String name ) {
	for ( let list in lists ) {
		if ( typeof list ne "Array" ) {
			die name _ " expects Array arguments";
		}
	}
	return lists;
}

function _zip_lists ( Array lists ) {
	_validate_zip_inputs( lists, "zip" );

	let max_length := 0;
	for ( let list in lists ) {
		max_length := list.length if list.length > max_length;
	}

	let out := [];
	let i := 0;
	while ( i < max_length ) {
		let row := [];
		for ( let list in lists ) {
			row.push( i < list.length ? list[i] : null );
		}
		out.push(row);
		i++;
	}
	return out;
}

function zip ( ... lists ) {
	return _zip_lists(lists);
}

function mesh ( ... lists ) {
	_validate_zip_inputs( lists, "mesh" );
	let out := [];
	for ( let row in _zip_lists(lists) ) {
		for ( let value in row ) {
			out.push(value);
		}
	}
	return out;
}

function firstval ( values, Function predicate ) {
	for ( let value in _ordered_array( values, "firstval" ) ) {
		return value if predicate(value);
	}
	return null;
}

function firstidx ( values, Function predicate ) {
	let items := _ordered_array( values, "firstidx" );
	let i := 0;
	while ( i < items.length ) {
		return i if predicate( items[i] );
		i++;
	}
	return -1;
}

function lastval ( values, Function predicate ) {
	let items := _ordered_array( values, "lastval" );
	let i := items.length - 1;
	while ( i ≥ 0 ) {
		return items[i] if predicate( items[i] );
		i--;
	}
	return null;
}

function lastidx ( values, Function predicate ) {
	let items := _ordered_array( values, "lastidx" );
	let i := items.length - 1;
	while ( i ≥ 0 ) {
		return i if predicate( items[i] );
		i--;
	}
	return -1;
}

function onlyval ( values, Function predicate ) {
	let found := false;
	let match := null;
	for ( let value in _collection_array( values, "onlyval" ) ) {
		if ( predicate(value) ) {
			return null if found;
			found := true;
			match := value;
		}
	}
	return found ? match : null;
}

function onlyidx ( values, Function predicate ) {
	let items := _ordered_array( values, "onlyidx" );
	let found := false;
	let match := -1;
	let i := 0;
	while ( i < items.length ) {
		if ( predicate( items[i] ) ) {
			return -1 if found;
			found := true;
			match := i;
		}
		i++;
	}
	return match;
}

function _sort_by_key ( values, Function key_function, String name, String mode ) {
	let decorated := [];
	let i := 0;
	for ( let value in _collection_array( values, name ) ) {
		let raw_key := key_function(value);
		decorated.push(
			{
				key: mode eq "number" ? _key_number(raw_key) : _key_string(raw_key),
				index: i,
				value: value,
			},
		);
		i++;
	}

	return decorated.sort( function ( left, right ) {
		let comparison := mode eq "number"
			? left{key} ≶ right{key}
			: left{key} cmp right{key};
		return comparison if comparison ≠ 0;
		return left{index} ≶ right{index};
	} ).map( fn row → row{value} );
}

function sort_by ( values, Function key_function ) {
	return _sort_by_key( values, key_function, "sort_by", "number" );
}

function sortnum_by ( values, Function key_function ) {
	return sort_by( values, key_function );
}

function sortstr_by ( values, Function key_function ) {
	return _sort_by_key( values, key_function, "sortstr_by", "string" );
}

function max_by ( values, Function key_function ) {
	let items := _collection_array( values, "max_by" );
	return null if items.empty;

	let best := items[0];
	let best_key := _key_number( key_function(best) );
	let i := 1;
	while ( i < items.length ) {
		let value := items[i];
		let key := _key_number( key_function(value) );
		if ( key > best_key ) {
			best := value;
			best_key := key;
		}
		i++;
	}
	return best;
}

function maxnum_by ( values, Function key_function ) {
	return max_by( values, key_function );
}

function maxstr_by ( values, Function key_function ) {
	let items := _collection_array( values, "maxstr_by" );
	return null if items.empty;

	let best := items[0];
	let best_key := _key_string( key_function(best) );
	let i := 1;
	while ( i < items.length ) {
		let value := items[i];
		let key := _key_string( key_function(value) );
		if ( key gt best_key ) {
			best := value;
			best_key := key;
		}
		i++;
	}
	return best;
}

function min_by ( values, Function key_function ) {
	let items := _collection_array( values, "min_by" );
	return null if items.empty;

	let best := items[0];
	let best_key := _key_number( key_function(best) );
	let i := 1;
	while ( i < items.length ) {
		let value := items[i];
		let key := _key_number( key_function(value) );
		if ( key < best_key ) {
			best := value;
			best_key := key;
		}
		i++;
	}
	return best;
}

function minnum_by ( values, Function key_function ) {
	return min_by( values, key_function );
}

function minstr_by ( values, Function key_function ) {
	let items := _collection_array( values, "minstr_by" );
	return null if items.empty;

	let best := items[0];
	let best_key := _key_string( key_function(best) );
	let i := 1;
	while ( i < items.length ) {
		let value := items[i];
		let key := _key_string( key_function(value) );
		if ( key lt best_key ) {
			best := value;
			best_key := key;
		}
		i++;
	}
	return best;
}

function uniq_by ( values, Function key_function ) {
	return _uniq_by_key( values, key_function, "uniq_by" );
}

class ListUtil {
	static method reduce ( values, Function callback ) {
		return reduce( values, callback );
	}

	static method reductions ( values, Function callback ) {
		return reductions( values, callback );
	}

	static method any ( values, Function predicate ) {
		return any( values, predicate );
	}

	static method all ( values, Function predicate ) {
		return all( values, predicate );
	}

	static method none ( values, Function predicate ) {
		return none( values, predicate );
	}

	static method notall ( values, Function predicate ) {
		return notall( values, predicate );
	}

	static method first ( values, Function predicate ) {
		return first( values, predicate );
	}

	static method max ( values ) {
		return max(values);
	}

	static method maxstr ( values ) {
		return maxstr(values);
	}

	static method min ( values ) {
		return min(values);
	}

	static method minstr ( values ) {
		return minstr(values);
	}

	static method product ( values ) {
		return product(values);
	}

	static method sum ( values ) {
		return sum(values);
	}

	static method sum0 ( values ) {
		return sum0(values);
	}

	static method pairs ( values ) {
		return pairs(values);
	}

	static method unpairs ( values ) {
		return unpairs(values);
	}

	static method pairkeys ( values ) {
		return pairkeys(values);
	}

	static method pairvalues ( values ) {
		return pairvalues(values);
	}

	static method pairfirst ( values, Function predicate ) {
		return pairfirst( values, predicate );
	}

	static method pairgrep ( values, Function predicate ) {
		return pairgrep( values, predicate );
	}

	static method pairmap ( values, Function mapper ) {
		return pairmap( values, mapper );
	}

	static method sort ( values, Function comparator ) {
		return sort( values, comparator );
	}

	static method sortnum ( values ) {
		return sortnum(values);
	}

	static method sortstr ( values ) {
		return sortstr(values);
	}

	static method reverse ( values ) {
		return reverse(values);
	}

	static method map ( values, Function mapper ) {
		return map( values, mapper );
	}

	static method grep ( values, Function predicate ) {
		return grep( values, predicate );
	}

	static method shuffle ( values ) {
		return shuffle(values);
	}

	static method sample ( values, Number count ) {
		return sample( values, count );
	}

	static method uniq ( values ) {
		return uniq(values);
	}

	static method uniqint ( values ) {
		return uniqint(values);
	}

	static method uniqnum ( values ) {
		return uniqnum(values);
	}

	static method uniqstr ( values ) {
		return uniqstr(values);
	}

	static method head ( values, Number count := 1 ) {
		return head( values, count );
	}

	static method tail ( values, Number count := 1 ) {
		return tail( values, count );
	}

	static method zip ( ... lists ) {
		return _zip_lists(lists);
	}

	static method mesh ( ... lists ) {
		let out := [];
		for ( let row in _zip_lists(lists) ) {
			for ( let value in row ) {
				out.push(value);
			}
		}
		return out;
	}

	static method firstval ( values, Function predicate ) {
		return firstval( values, predicate );
	}

	static method firstidx ( values, Function predicate ) {
		return firstidx( values, predicate );
	}

	static method lastval ( values, Function predicate ) {
		return lastval( values, predicate );
	}

	static method lastidx ( values, Function predicate ) {
		return lastidx( values, predicate );
	}

	static method onlyval ( values, Function predicate ) {
		return onlyval( values, predicate );
	}

	static method onlyidx ( values, Function predicate ) {
		return onlyidx( values, predicate );
	}

	static method sort_by ( values, Function key_function ) {
		return sort_by( values, key_function );
	}

	static method sortnum_by ( values, Function key_function ) {
		return sortnum_by( values, key_function );
	}

	static method sortstr_by ( values, Function key_function ) {
		return sortstr_by( values, key_function );
	}

	static method max_by ( values, Function key_function ) {
		return max_by( values, key_function );
	}

	static method maxnum_by ( values, Function key_function ) {
		return maxnum_by( values, key_function );
	}

	static method maxstr_by ( values, Function key_function ) {
		return maxstr_by( values, key_function );
	}

	static method min_by ( values, Function key_function ) {
		return min_by( values, key_function );
	}

	static method minnum_by ( values, Function key_function ) {
		return minnum_by( values, key_function );
	}

	static method minstr_by ( values, Function key_function ) {
		return minstr_by( values, key_function );
	}

	static method uniq_by ( values, Function key_function ) {
		return uniq_by( values, key_function );
	}
}