Date and Time Handling
Overview
The Competition Factory handles dates and times across several domains: tournament scheduling, match timing, embargo enforcement, and timezone conversion. All temporal data uses ISO 8601 format, and the factory is zero-dependency — every date, time, and timezone operation is built on platform APIs (Date, Intl.DateTimeFormat) with no external libraries.
Date Formats Used
| Context | Format | Example |
|---|---|---|
| Tournament dates, scheduled dates | YYYY-MM-DD | '2024-06-15' |
| Timestamps (embargo, createdAt, start/end times) | ISO 8601 with timezone | '2024-06-15T10:00:00Z' |
| Time-of-day (scheduling) | HH:MM | '14:30' |
| IANA timezone identifiers | Region/City | 'America/New_York' |
Where Dates Appear
Tournament Record:
startDate/endDate— tournament date range (YYYY-MM-DD)localTimeZone— IANA timezone identifier for the tournament venueactiveDates— specific dates within the range when play occurs
Time Items:
itemDate— effective date (YYYY-MM-DD)createdAt— creation timestamp (ISO 8601 with timezone)itemValue— often contains timestamps for scheduling events
Publishing / Embargo:
embargo— ISO 8601 timestamp with timezone (Zor±HH:MMoffset required)
Scheduling:
scheduledDate— match date (YYYY-MM-DD)scheduledTime— match time (HH:MM)
Date & Time Utilities
The dateTime object on tools provides functions for working with dates and times:
import { tools } from 'tods-competition-factory';
Core Functions
| Function | Description |
|---|---|
dateTime.formatDate(date, separator?, order?) | Format a Date object to string |
dateTime.extractDate(dateTimeString) | Extract YYYY-MM-DD from a datetime string |
dateTime.extractTime(dateTimeString) | Extract HH:MM from a datetime string |
dateTime.addDays(date, days) | Add days to a date |
dateTime.addWeek(date) | Add 7 days to a date |
dateTime.addMinutesToTimeString(time, minutes) | Add minutes to an HH:MM string |
dateTime.sameDay(date1, date2) | Check if two dates are the same calendar day |
dateTime.isISODateString(value) | Validate ISO 8601 date/datetime string |
dateTime.isValidEmbargoDate(value) | Validate ISO 8601 datetime with timezone |
dateTime.isDate(value) | Check if value is a Date object |
dateTime.isTimeString(value) | Validate HH:MM format |
dateTime.timeStringMinutes(time) | Convert HH:MM to total minutes |
dateTime.offsetDate(dateString) | Create a Date from an ISO date string |
dateTime.offsetTime(isoString?) | Get timezone offset in milliseconds |
dateTime.getUTCdateString(dateOrString) | Get YYYY-MM-DD in UTC |
dateTime.timeUTC(dateOrString?) | Get UTC timestamp in milliseconds |
Date Range Generation
import { tools } from 'tods-competition-factory';
const dates = tools.generateDateRange('2024-06-10', '2024-06-15');
// ['2024-06-10', '2024-06-11', '2024-06-12', '2024-06-13', '2024-06-14', '2024-06-15']
Embargo Validation
isValidEmbargoDate ensures a value is a valid ISO 8601 datetime string that includes timezone context. This is enforced automatically by all publishing methods, but is also available for pre-validation:
tools.dateTime.isValidEmbargoDate('2024-06-15T10:00:00Z'); // true — UTC
tools.dateTime.isValidEmbargoDate('2024-06-15T10:00:00+05:30'); // true — offset
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
See Embargo and Scheduled Rounds for the full embargo documentation.
Timezone Utilities
The timeZone object on tools provides timezone-aware operations using Intl.DateTimeFormat. These functions handle DST transitions correctly and work in all modern JavaScript runtimes.
import { tools } from 'tods-competition-factory';
Validating Timezones
Tournament records can specify a localTimeZone (IANA timezone identifier). The factory validates this value on creation using isValidIANATimeZone:
tools.timeZone.isValidIANATimeZone('America/New_York'); // true
tools.timeZone.isValidIANATimeZone('Europe/London'); // true
tools.timeZone.isValidIANATimeZone('UTC'); // true
tools.timeZone.isValidIANATimeZone('EST'); // runtime-dependent
tools.timeZone.isValidIANATimeZone('Not/A/Zone'); // false
When creating a tournament record with an invalid localTimeZone, the factory returns an INVALID_TIME_ZONE error:
const result = engine.createTournamentRecord({
tournamentName: 'Example Open',
localTimeZone: 'Invalid/Zone',
});
// result.error === INVALID_TIME_ZONE
Getting UTC Offsets
getTimeZoneOffsetMinutes returns the UTC offset in minutes for a timezone at a specific instant, correctly accounting for DST:
// Winter: EST is UTC-5 (-300 minutes)
tools.timeZone.getTimeZoneOffsetMinutes('America/New_York', new Date('2024-01-15T12:00:00Z'));
// -300
// Summer: EDT is UTC-4 (-240 minutes)
tools.timeZone.getTimeZoneOffsetMinutes('America/New_York', new Date('2024-06-15T12:00:00Z'));
// -240
// Fixed offset (no DST)
tools.timeZone.getTimeZoneOffsetMinutes('Asia/Kolkata'); // 330 (UTC+5:30, always)
Converting Between Wall-Clock Time and UTC
These functions convert between "local time at a venue" and absolute UTC timestamps.
Wall-clock to UTC:
// "3:00 AM in New York on June 20" → UTC
tools.timeZone.wallClockToUTC('2024-06-20', '03:00', 'America/New_York');
// '2024-06-20T07:00:00.000Z' (EDT = UTC-4)
// Same time in winter → different UTC
tools.timeZone.wallClockToUTC('2024-01-15', '03:00', 'America/New_York');
// '2024-01-15T08:00:00.000Z' (EST = UTC-5)
UTC to wall-clock:
// UTC → "what time is it in New York?"
tools.timeZone.utcToWallClock('2024-06-20T07:00:00.000Z', 'America/New_York');
// { date: '2024-06-20', time: '03:00' }
// Day boundary: UTC June 21 03:00 → still June 20 in New York
tools.timeZone.utcToWallClock('2024-06-21T03:00:00.000Z', 'America/New_York');
// { date: '2024-06-20', time: '23:00' }
Creating Embargo Strings from Local Time
toEmbargoUTC is a convenience function that converts wall-clock time to a validated UTC embargo string, ready to pass to any publishing method:
const embargo = tools.timeZone.toEmbargoUTC('2024-06-15', '08:00', 'America/New_York');
// '2024-06-15T12:00:00.000Z'
// The result always passes isValidEmbargoDate
tools.dateTime.isValidEmbargoDate(embargo); // true
// Use directly
engine.publishOrderOfPlay({
scheduledDates: ['2024-06-15'],
embargo,
});
Practical Pattern: Tournament-Aware Embargo
Combine localTimeZone from the tournament record with toEmbargoUTC for a complete workflow:
// Get the tournament's timezone
const { tournamentInfo } = engine.getTournamentInfo();
const { localTimeZone } = tournamentInfo;
// e.g. 'America/New_York'
// Admin says "make the draw visible at 9 AM local time on June 15"
const embargo = tools.timeZone.toEmbargoUTC('2024-06-15', '09:00', localTimeZone);
engine.publishEvent({
eventId,
drawDetails: {
[drawId]: {
publishingDetail: { published: true, embargo },
},
},
});
// Later, display the embargo in local time for the admin
const { date, time } = tools.timeZone.utcToWallClock(embargo, localTimeZone);
console.log(`Embargo lifts at ${time} on ${date} (${localTimeZone})`);
// "Embargo lifts at 09:00 on 2024-06-15 (America/New_York)"
Date Handling Across the Factory
Tournament Dates
Tournament records track dates as YYYY-MM-DD strings. Validation is applied on creation and when modifying dates:
// startDate, endDate, and activeDates are validated as ISO date strings
engine.createTournamentRecord({
tournamentName: 'Summer Open',
startDate: '2024-06-10',
endDate: '2024-06-16',
activeDates: ['2024-06-10', '2024-06-12', '2024-06-14', '2024-06-16'], // optional
localTimeZone: 'America/New_York', // validated as IANA timezone
});
// Modifying dates validates and adjusts event dates accordingly
engine.setTournamentDates({
startDate: '2024-06-11',
endDate: '2024-06-15',
activeDates: ['2024-06-11', '2024-06-13', '2024-06-15'], // optional
});
Active Dates
activeDates is an optional array of YYYY-MM-DD date strings representing the specific dates within a tournament's (or event's) date range when play actually occurs. This is useful for tournaments that span a date range but don't play every day — for example, a week-long tournament with rest days.
- Omitted or
undefined: all dates in thestartDate–endDaterange are implicitly active - Non-empty array: only the listed dates are active play dates
- Validation: every date must be a valid date string and must fall within
startDate/endDate; returnsINVALID_DATEon failure - Derivation: when
createTournamentRecordis called withactiveDatesbut withoutstartDateand/orendDate, the missing boundaries are derived from the earliest and latest active dates. This ensures the tournament record always has validstartDate/endDatefor scheduling, court availability, and other subsystems that depend on them. - Falsy filtering: falsy values (empty strings,
null) are automatically filtered out before validation
activeDates can be set at creation time via createTournamentRecord or later via setTournamentDates. Events also support activeDates via setEventDates.
// Set activeDates at creation — startDate/endDate provided explicitly
engine.createTournamentRecord({
startDate: '2024-06-10',
endDate: '2024-06-16',
activeDates: ['2024-06-10', '2024-06-12', '2024-06-14', '2024-06-16'],
});
// Set activeDates at creation — startDate/endDate derived automatically
// startDate becomes '2024-06-10', endDate becomes '2024-06-16'
engine.createTournamentRecord({
activeDates: ['2024-06-10', '2024-06-12', '2024-06-14', '2024-06-16'],
});
// Update activeDates later
engine.setTournamentDates({
activeDates: ['2024-06-10', '2024-06-11', '2024-06-14', '2024-06-16'],
});
// Set event-level activeDates
engine.setEventDates({
eventId,
activeDates: ['2024-06-10', '2024-06-14'],
});
Scheduling
Match scheduling uses YYYY-MM-DD for dates and HH:MM for times. The scheduling system stores these as Time Items with full timestamps:
engine.addMatchUpScheduleItems({
matchUpId,
drawId,
schedule: {
scheduledDate: '2024-06-15',
scheduledTime: '14:30',
courtId: 'court-5',
},
});
Time Items
All temporal records use ISO 8601 format. createdAt is auto-generated with timezone context:
{
itemType: 'SCHEDULE.TIME.START',
itemValue: '2024-06-15T14:05:00Z', // Always includes timezone
itemDate: '2024-06-15', // YYYY-MM-DD
createdAt: '2024-06-15T14:05:00.000Z', // Auto-generated
}
See Time Items for the complete temporal data reference.
Future: Temporal API
The factory's zero-dependency approach to date/time handling is built on Date and Intl.DateTimeFormat — APIs available in every JavaScript runtime. The TC39 Temporal proposal is expected to reach Stage 4 and ship in major runtimes by 2027, providing first-class support for timezone-aware dates, wall-clock times, and duration arithmetic directly in the language.
When Temporal becomes widely available, the factory's timezone utilities will be updated to use Temporal.ZonedDateTime and Temporal.PlainDate internally, providing:
- More precise DST transition handling at boundary cases
- Native duration and calendar arithmetic
- Cleaner API surface without the quirks of the legacy
Dateobject
This transition will be non-breaking — the factory's public API (wallClockToUTC, utcToWallClock, toEmbargoUTC, etc.) will remain the same, with Temporal powering the implementation under the hood. The zero-dependency philosophy is maintained: Temporal is a language built-in, not a library.
Related Documentation
- Time Items — Temporal records on CODES document elements
- Embargo and Scheduled Rounds — Time-based visibility gates
- Scheduling Overview — Match scheduling concepts
- Tools API — Complete function reference for
dateTimeandtimeZone