Skip to main content

Tools API Reference

Complete reference of all utility functions available in the tools module.

import { tools } from 'tods-competition-factory';

Array Utilities​

unique​

Extract unique values from an array.

const uniqueValues = tools.unique([1, 2, 2, 3, 3, 3]);
// Result: [1, 2, 3]

const uniqueRounds = tools.unique(matchUps.map((m) => m.roundNumber));

intersection​

Find common elements between two arrays.

const common = tools.intersection([1, 2, 3], [2, 3, 4]);
// Result: [2, 3]

overlap​

Check if two arrays have any overlapping elements.

const hasOverlap = tools.overlap([1, 2, 3], [3, 4, 5]);
// Result: true

shuffleArray​

Randomly shuffle array elements (non-destructive).

const shuffled = tools.shuffleArray([1, 2, 3, 4, 5]);

randomMember​

Select a random element from an array.

const randomItem = tools.randomMember(['A', 'B', 'C', 'D']);

randomPop​

Remove and return a random element from an array (destructive).

const arr = [1, 2, 3, 4, 5];
const item = tools.randomPop(arr); // Returns random item, modifies arr

chunkArray​

Split an array into chunks of specified size.

const chunks = tools.chunkArray([1, 2, 3, 4, 5, 6], 2);
// Result: [[1, 2], [3, 4], [5, 6]]

chunkByNth​

Distribute array elements into N chunks.

const chunks = tools.chunkByNth([1, 2, 3, 4, 5, 6], 3);
// Result: [[1, 4], [2, 5], [3, 6]]

chunkSizeProfile​

Split array by varying chunk sizes.

const chunks = tools.chunkSizeProfile([1, 2, 3, 4, 5, 6, 7], [2, 3, 2]);
// Result: [[1, 2], [3, 4, 5], [6, 7]]

generateRange​

Generate an array of numbers from start to end (inclusive).

const range = tools.generateRange(1, 5);
// Result: [1, 2, 3, 4, 5]

instanceCount​

Count occurrences of each value in an array.

const counts = tools.instanceCount(['A', 'B', 'A', 'C', 'B', 'A']);
// Result: { A: 3, B: 2, C: 1 }

countValues​

Group array indices by their values.

const grouped = tools.countValues([10, 20, 10, 30, 20]);
// Result: { 10: [0, 2], 20: [1, 4], 30: [3] }

groupValues​

Group object values by keys.

const grouped = tools.groupValues({ a: 1, b: 2, c: 1 });
// Result: { 1: ['a', 'c'], 2: ['b'] }

allNumeric​

Check if all array elements are numeric.

const isAllNumeric = tools.allNumeric([1, 2, 3]); // true
const notAllNumeric = tools.allNumeric([1, 'a', 3]); // false

noNumeric​

Check if array contains no numeric values.

const noNumbers = tools.noNumeric(['a', 'b', 'c']); // true

noNulls​

Remove null and undefined values from array.

const clean = tools.noNulls([1, null, 2, undefined, 3]);
// Result: [1, 2, 3]

occurrences​

Count occurrences of a specific value in array.

const count = tools.occurrences('A', ['A', 'B', 'A', 'C']);
// Result: 2

subSort​

Sort a portion of an array (non-destructive).

const sorted = tools.subSort([5, 4, 3, 2, 1], 1, 3, (a, b) => a - b);
// Result: [5, 2, 3, 4, 1] (sorted indices 1-3)

Object Utilities​

createMap​

Create a lookup map from an array of objects.

const participants = [
{ participantId: 'id1', participantName: 'Player 1' },
{ participantId: 'id2', participantName: 'Player 2' },
];
const participantsMap = tools.createMap(participants, 'participantId');
// Result: { id1: { participantId: 'id1', ... }, id2: { ... } }

hasAttributeValues​

Create a filter function that checks for specific attribute values.

const isCompleted = tools.hasAttributeValues({ matchUpStatus: 'COMPLETED' });
const completedMatchUps = matchUps.filter(isCompleted);

definedAttributes​

Extract only defined (non-undefined) attributes from an object.

const clean = tools.definedAttributes({
a: 1,
b: undefined,
c: null,
d: 2,
});
// Result: { a: 1, c: null, d: 2 }

extractAttributes​

Create an extractor function for specific object attributes.

const getName = tools.extractAttributes('participantName');
const names = participants.map(getName);

undefinedToNull​

Convert undefined values to null in an object.

const converted = tools.undefinedToNull({ a: 1, b: undefined });
// Result: { a: 1, b: null }

generateHashCode​

Generate a hash code from an object or string.

const hash = tools.generateHashCode({ key: 'value' });
const stringHash = tools.generateHashCode('text');

Math & Validation​

isPowerOf2​

Check if a number is a power of 2.

tools.isPowerOf2(8); // true
tools.isPowerOf2(16); // true
tools.isPowerOf2(12); // false

nearestPowerOf2​

Find the nearest power of 2 to a number.

tools.nearestPowerOf2(10); // 8
tools.nearestPowerOf2(20); // 16

nextPowerOf2​

Find the next power of 2 greater than or equal to a number.

tools.nextPowerOf2(10); // 16
tools.nextPowerOf2(16); // 16

isConvertableInteger​

Check if a value can be converted to an integer.

tools.isConvertableInteger('123'); // true
tools.isConvertableInteger('12.5'); // false
tools.isConvertableInteger('abc'); // false

isNumeric​

Check if a value is numeric.

tools.isNumeric(123); // true
tools.isNumeric('123'); // true
tools.isNumeric('abc'); // false

isOdd​

Check if a number is odd.

tools.isOdd(3); // true
tools.isOdd(4); // false

Date & Time​

Four different questions hide inside "a date", and answering the wrong one is the most common source of off-by-an-hour and off-by-a-day bugs in competition data:

QuestionModuleExample
Which calendar day?tools.plainDate2026-09-09 — the same day in Auckland and Los Angeles
What time on the clock?tools.plainTime14:00 — not a moment until a day and a zone are supplied
Which actual moment, at a venue?tools.zonedDateTime2026-09-09 + 14:00 + America/New_York
Which absolute instant?ISO strings with Z2026-09-09T18:00:00.000Z

tools.dateTime predates that split and spans the first two. It is fully supported and its behaviour is unchanged — it now re-exports from the intent modules, so there is exactly one implementation of each helper — but new code should reach for the module that names the intent. A reader of plainDate.extractDate(x) knows no zone was involved; a reader of dateTime.extractDate(x) has to go and check.

The names are the TC39 Temporal ones on purpose. When Temporal.PlainDate and Temporal.PlainTime are available on every runtime the factory supports, these become one-for-one substitutions rather than a rename.

tools.plainDate​

A calendar day: no clock, no zone. Takes and returns ISO date strings (YYYY-MM-DD); Date appears only as internal arithmetic.

import { tools } from 'tods-competition-factory';

// The calendar day, whatever the instant was wearing
tools.plainDate.extractDate('2026-09-09T14:00:00+05:30'); // '2026-09-09'
tools.plainDate.extractDate('not-a-date'); // '' — never throws

tools.plainDate.generateDateRange('2026-09-09', '2026-09-12');
// ['2026-09-09', '2026-09-10', '2026-09-11', '2026-09-12'] (inclusive both ends)

tools.plainDate.addDays('2026-09-28', 5); // '2026-10-03'
tools.plainDate.addWeek('2026-09-09'); // '2026-09-16'
tools.plainDate.sameDay('2026-09-09T01:00', '2026-09-09T23:00'); // true
tools.plainDate.isISODateString('tomorrow'); // false

Also available: formatDate, isValidDateString, dateStringDaysChange, getDateByWeek, dateFromDay, subtractWeek, weekdays, isDateInPast, localizeDate.

Nothing in plainDate resolves an offset. Anything that needs one belongs in tools.zonedDateTime.

tools.plainTime​

A wall clock: no day, no zone. 14:00 is a plainTime; it is not a moment until a day and a zone are supplied.

tools.plainTime.extractTime('2026-09-09T14:00'); // '14:00'
tools.plainTime.extractTime('2026-09-09'); // undefined — no clock to read

tools.plainTime.timeStringMinutes('14:30'); // 870 — minutes since midnight
tools.plainTime.dayMinutesToTimeString(870); // '14:30'
tools.plainTime.dayMinutesToTimeString(1500); // '01:00' — wraps rather than overflowing

tools.plainTime.convertTime('14:00'); // '2:00 PM'
tools.plainTime.convertTime('2:00 PM', true); // '14:00'
tools.plainTime.isTimeString('24:00'); // false — no wall clock shows that

Also available: tidyTime, validTimeValue, splitTime, militaryTime, regularTime, timeSort, HHMMSS.

timeSort is a comparator, so pass it to .sort() rather than calling it directly:

['14:00', '09:30', '23:15'].sort(tools.plainTime.timeSort);
// ['09:30', '14:00', '23:15']

dateTime​

Object containing date/time utility functions. Spans the calendar-day and wall-clock intents; see the table above for which module to prefer in new code.

// Get ISO date string
const isoDate = tools.dateTime.getIsoDateString(scheduleObject);

// Format date
const formatted = tools.dateTime.formatDate(new Date(), '-', 'YMD');

// Add days to date
const future = tools.dateTime.addDays(new Date(), 7);

// Get date range
const dates = tools.dateTime.generateDateRange('2024-01-01', '2024-01-07');

isValidEmbargoDate​

Validate that a string is a valid ISO 8601 datetime with timezone context (Z or ±HH:MM). Used internally by all publishing methods; also available for pre-validation.

tools.dateTime.isValidEmbargoDate('2024-06-15T10:00:00Z'); // true
tools.dateTime.isValidEmbargoDate('2024-06-15T10:00:00+05:30'); // true
tools.dateTime.isValidEmbargoDate('2024-06-15T10:00:00'); // false — no timezone
tools.dateTime.isValidEmbargoDate('2024-06-15'); // false — date only
tools.dateTime.isValidEmbargoDate(42); // false — not a string

// Also available as a standalone import
import { tools } from 'tods-competition-factory';
const { isValidEmbargoDate } = tools;

generateDateRange​

Generate an array of dates between start and end dates.

const dates = tools.generateDateRange('2024-01-01', '2024-01-05');
// Result: ['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04', '2024-01-05']

now​

The engine's current instant as a Date: the configured clock if setClock has fixed one, otherwise the wall clock. Added in 7.5.0.

const instant = tools.now();

nowIso​

The engine's current instant as an ISO 8601 string, the form every record timestamp takes. Added in 7.5.0.

const stamp = tools.nowIso(); // '2026-10-01T12:00:00.000Z'

setClock​

Fixes the clock that now and nowIso read, and that every timestamp a mutation writes into a record reads. Process-wide. Pass an ISO string, epoch milliseconds, a Date, or a function returning any of those; no argument restores the wall clock. Returns { success }, or { error: INVALID_DATE } for an unparseable instant. The same as engine.setClock. Added in 7.5.0.

tools.setClock('2026-10-01T12:00:00.000Z'); // frozen
tools.setClock(); // wall clock

Timezone​

Zero-dependency timezone utilities built on Intl.DateTimeFormat. These functions handle DST transitions correctly and require no external libraries.

import { tools } from 'tods-competition-factory';
// All functions are under tools.timeZone

isValidIANATimeZone​

Validate that a string is a recognized IANA timezone identifier.

tools.timeZone.isValidIANATimeZone('America/New_York'); // true
tools.timeZone.isValidIANATimeZone('Europe/London'); // true
tools.timeZone.isValidIANATimeZone('UTC'); // true
tools.timeZone.isValidIANATimeZone('Not/A/Zone'); // false
tools.timeZone.isValidIANATimeZone(''); // false

Used internally to validate localTimeZone when creating tournament records.

getTimeZoneOffsetMinutes​

Returns the UTC offset in minutes for a given IANA timezone at a specific instant. Positive values are east of UTC, negative values are west.

// Winter (EST = UTC-5)
tools.timeZone.getTimeZoneOffsetMinutes('America/New_York', new Date('2024-01-15T12:00:00Z'));
// Result: -300

// Summer (EDT = UTC-4)
tools.timeZone.getTimeZoneOffsetMinutes('America/New_York', new Date('2024-06-15T12:00:00Z'));
// Result: -240

tools.timeZone.getTimeZoneOffsetMinutes('UTC'); // 0
tools.timeZone.getTimeZoneOffsetMinutes('Asia/Kolkata'); // 330 (UTC+5:30)

// An unrecognised or absent zone is `undefined` — never a substituted 0, and
// never the offset of whichever machine happens to be running.
tools.timeZone.getTimeZoneOffsetMinutes('Not/A/Zone'); // undefined
tools.timeZone.getTimeZoneOffsetMinutes(); // undefined

Returns: offset in minutes, or undefined when the zone is absent or unrecognised.

wallClockToUTC​

Convert a wall-clock date and time at a specific IANA timezone to a UTC ISO string. Handles DST transitions automatically.

// 3:00 AM Eastern in summer (EDT, UTC-4)
tools.timeZone.wallClockToUTC('2024-06-20', '03:00', 'America/New_York');
// Result: '2024-06-20T07:00:00.000Z'

// 3:00 AM Eastern in winter (EST, UTC-5)
tools.timeZone.wallClockToUTC('2024-01-15', '03:00', 'America/New_York');
// Result: '2024-01-15T08:00:00.000Z'

// Invalid timezone returns error
tools.timeZone.wallClockToUTC('2024-06-20', '03:00', 'Invalid/Zone');
// Result: { error: INVALID_TIME_ZONE }

// A malformed date and a malformed time are different errors
tools.timeZone.wallClockToUTC('not-a-date', '03:00', 'America/New_York');
// Result: { error: INVALID_DATE }
tools.timeZone.wallClockToUTC('2024-06-20', 'noon', 'America/New_York');
// Result: { error: INVALID_TIME }
ParameterTypeDescription
datestringWall-clock date in YYYY-MM-DD format
timestringWall-clock time in HH:MM format
timeZonestringIANA timezone identifier

Returns: UTC ISO string (ending in Z), or { error } carrying INVALID_TIME_ZONE, INVALID_DATE or INVALID_TIME.

utcToWallClock​

Convert a UTC ISO string to wall-clock date and time in a specific timezone.

tools.timeZone.utcToWallClock('2024-06-20T07:00:00.000Z', 'America/New_York');
// Result: { date: '2024-06-20', time: '03:00' }

// Handles day boundaries
tools.timeZone.utcToWallClock('2024-06-21T03:00:00.000Z', 'America/New_York');
// Result: { date: '2024-06-20', time: '23:00' }

// Invalid timezone returns error
tools.timeZone.utcToWallClock('2024-06-20T07:00:00.000Z', 'Invalid/Zone');
// Result: { error: INVALID_TIME_ZONE }

// An unparseable instant is refused rather than throwing
tools.timeZone.utcToWallClock('garbage', 'America/New_York');
// Result: { error: INVALID_DATE }
ParameterTypeDescription
utcIsostringUTC ISO 8601 datetime string
timeZonestringIANA timezone identifier

Returns: { date: string, time: string }, or { error } carrying INVALID_TIME_ZONE or INVALID_DATE.

toEmbargoUTC​

Convenience wrapper that converts a wall-clock date/time at a timezone to a validated UTC embargo string. The result is guaranteed to pass isValidEmbargoDate() and can be passed directly to publishEvent, publishOrderOfPlay, or publishParticipants.

const embargo = tools.timeZone.toEmbargoUTC('2024-06-15', '08:00', 'America/New_York');
// Result: '2024-06-15T12:00:00.000Z'

// Use directly in publishing
engine.publishEvent({
eventId,
drawDetails: {
[drawId]: { publishingDetail: { published: true, embargo } },
},
});
ParameterTypeDescription
datestringWall-clock date in YYYY-MM-DD format
timestringWall-clock time in HH:MM format
timeZonestringIANA timezone identifier

Returns: UTC ISO string (ending in Z), or { error } carrying INVALID_TIME_ZONE, INVALID_DATE or INVALID_TIME.


tools.zonedDateTime​

tools.timeZone is a thin adapter; tools.zonedDateTime is the implementation beneath it and the zoned member of the calendar-intent set (plainDate, plainTime, zonedDateTime). Reach for it when you need the raw epoch-millisecond form, or when you need to know which frame a conversion used.

import { tools } from 'tods-competition-factory';

// Which frame did this conversion actually use?
tools.zonedDateTime.zonedWallClockToMs({ date: '2026-07-15', time: '09:00', timeZone: 'America/New_York' });
// { ms: 1784725200000, source: 'zone' }

// No zone: the caller's own offset frame, and it says so.
tools.zonedDateTime.zonedWallClockToMs({ date: '2026-07-15', time: '09:00', utcOffsetMinutes: -240 });
// { ms: 1784725200000, source: 'offset' }

// A zone the system cannot honour is REFUSED, not silently replaced.
tools.zonedDateTime.zonedWallClockToMs({
date: '2026-07-15',
time: '09:00',
utcOffsetMinutes: -240,
timeZone: 'Not/AZone',
});
// null

tools.zonedDateTime.zonedParts({ ms: 1784725200000, timeZone: 'America/New_York' });
// { date: '2026-07-15', time: '09:00', source: 'zone' }

tools.zonedDateTime.offsetMinutesAt(1784725200000, 'America/New_York'); // -240
tools.zonedDateTime.isZone('America/New_York'); // true — cached, so repeat checks are free

The frame rule​

timeZoneresultsource
absentthe caller's utcOffsetMinutes (default 0)'offset'
present, unrecognisednull — refused—
present, recognisedresolved per instant, DST-correct'zone'

A caller that supplies no zone has declared its own frame, which is legitimate — most tournaments carry no zone. A caller that supplies a zone the system cannot honour has made a config error; substituting a different frame turns a 90-minute recovery figure into a 330-minute one that still reads as measured. source is returned so the difference is visible in the value rather than assumed.


ID & Code Generation​

UUID​

Generate a unique identifier.

const id = tools.UUID();
// Result: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890'

const prefixed = tools.UUID('PREFIX');
// Result: 'PREFIX-a1b2c3d4-e5f6-7890-abcd-ef1234567890'

UUIDS​

Generate multiple unique identifiers at once.

const ids = tools.UUIDS(3);
// Result: ['uuid1', 'uuid2', 'uuid3']

const prefixedIds = tools.UUIDS(3, 'MATCHUP');

generateTimeCode​

Generate a time-based code.

const code = tools.generateTimeCode();
const indexedCode = tools.generateTimeCode(5);

createSeededRandom​

Create a seeded random function (mulberry32) returning numbers in [0, 1). The same seed always yields the same stream. The function carries its seed, and state() returns its current internal state, so createSeededRandom(rng.state()) continues the stream. Added in 7.5.0.

const rng = tools.createSeededRandom(7);
const value = rng(); // same value for seed 7, every run

randomSource​

The function the engine draws randomness from right now: the configured source if setRandomSource has set one, otherwise Math.random. Added in 7.5.0.

const random = tools.randomSource();
const value = random();

setRandomSource​

Sets the engine's process-wide source of randomness, which UUID and every placement or generation step read. Pass a seed (a finite number, for a seeded mulberry32) or a () => number; no argument restores Math.random. A call's own random parameter still takes precedence. Returns { success }, or { error: INVALID_VALUES } for anything else. The same as engine.setRandomSource. Added in 7.5.0.

tools.setRandomSource(7);
tools.setRandomSource(); // back to Math.random

Sorting​

numericSort​

Numeric sort comparator function.

const sorted = [10, 2, 5, 1].sort(tools.numericSort);
// Result: [1, 2, 5, 10]

matchUpSort​

Sort matchUps by schedule, round, position.

const sorted = matchUps.sort(tools.matchUpSort);

matchUpScheduleSort​

Sort matchUps by schedule time.

const sorted = matchUps.sort(tools.matchUpScheduleSort);

matchUpChronologicalSort​

Sort matchUps by (scheduledDate, scheduledTime). The comparator returns 0 whenever either matchUp lacks the relevant field, so a stable sort preserves input order for mixed-or-unscheduled inputs. Use this when the caller has already pre-sorted by the canonical matchUpSort order and only wants chronological refinement layered on top (e.g. Garman → Pro hand-off).

const sorted = matchUps.sort(tools.matchUpChronologicalSort);

structureSort​

Sort draw structures by stage, size, and sequence. See dedicated page. Since 7.7.0 the deprioritizeCompleted option works: a round robin container is read through its groups, and a matchUp that will never be played (a BYE, as well as ABANDONED, CANCELLED and DEAD_RUBBER) counts as done.

const sorted = structures.sort(tools.structureSort);

Data Transformation​

makeDeepCopy​

Create a deep copy of a JSON object. See dedicated page.

const copy = tools.makeDeepCopy(object);
const copyWithExtensions = tools.makeDeepCopy(object, true); // Flatten extensions

JSON2CSV​

Convert JSON array to CSV format. See dedicated page.

const csv = tools.JSON2CSV(arrayOfObjects, {
columnAccessors: ['id', 'name', 'score'],
columnMap: { id: 'ID', name: 'Name', score: 'Score' },
});

flattenJSON​

Flatten a nested JSON object.

const flat = tools.flattenJSON(
{
a: { b: { c: 1 } },
},
'.',
);
// Result: { 'a.b.c': 1 }

attributeFilter​

Create a filter function for object attributes.

const filter = tools.attributeFilter({ matchUpStatus: 'COMPLETED' });
const completed = matchUps.filter(filter);

canonicalJson​

The canonical JSON text of a value per RFC 8785 (JSON Canonicalization Scheme): object keys sorted, no whitespace, numbers and strings serialised as JSON.stringify does. Two values with the same content produce the same bytes. An undefined property is omitted and toJSON is honoured, as with JSON.stringify. Throws for a value with no JSON form at the top level (undefined, a function, a symbol), and for a non-finite number or a bigint anywhere. Added in 7.5.0.

const text = tools.canonicalJson({ b: 1, a: [true, null] });
// Result: '{"a":[true,null],"b":1}'

canonicalizeJsonText​

Parse JSON text and re-serialise it with canonicalJson: the canonical form of any JSON text. Added in 7.5.0.

const text = tools.canonicalizeJsonText('{ "b": 1, "a": 2 }');
// Result: '{"a":2,"b":1}'

String Utilities​

constantToString​

Convert a constant-case string to readable format.

const readable = tools.constantToString('SINGLE_ELIMINATION');
// Result: 'Single Elimination'

Tournament Utilities​

dehydrateMatchUps​

Remove computed attributes from matchUps for storage.

const dehydrated = tools.dehydrateMatchUps(matchUps);

visualizeScheduledMatchUps​

Generate color-coded console output of match schedule.

tools.visualizeScheduledMatchUps({
scheduledMatchUps,
showGlobalLogs: true,
});

parseScoreString​

Parse a score string into structured data.

const parsed = tools.parseScoreString({
scoreString: '6-4 3-6 7-6(3)',
matchUpFormat: 'SET3-S:6/TB7',
});
// Result: { sets: [...], winningSide: 1, ... }

Not in tools Module​

The following functions are available through governors, not the tools module:

From queryGovernor​

import { queryGovernor } from 'tods-competition-factory';

// Find extension by name
const { extension } = queryGovernor.findExtension({ element, name });

// Get time item
const { timeItem } = queryGovernor.getTimeItem({ element, itemType });

From matchUpGovernor​

import { matchUpGovernor } from 'tods-competition-factory';

// Calculate win criteria for tieFormats
const { valueGoal } = matchUpGovernor.calculateWinCriteria({
collectionDefinitions,
});

From scoreGovernor​

import { scoreGovernor } from 'tods-competition-factory';

// Check if score has value
const hasValue = scoreGovernor.checkScoreHasValue({ matchUp });

See Also​