@neo-reckoning/core 0.1.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/time.js ADDED
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Internal time/date utilities for neo-reckoning.
3
+ * Uses Intl.DateTimeFormat for timezone conversion — no external dependencies.
4
+ */
5
+ /** Parse "HH:mm" into { hour, minute } */
6
+ export function parseTime(time) {
7
+ const [h, m] = time.split(':').map(Number);
8
+ return { hour: h, minute: m };
9
+ }
10
+ /** Format { hour, minute } into "HH:mm" */
11
+ export function formatTime(hour, minute) {
12
+ return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`;
13
+ }
14
+ /** Convert minutes since midnight to "HH:mm" */
15
+ export function minutesToTime(minutes) {
16
+ const h = Math.floor(minutes / 60);
17
+ const m = minutes % 60;
18
+ return formatTime(h, m);
19
+ }
20
+ /** Convert "HH:mm" to minutes since midnight */
21
+ export function timeToMinutes(time) {
22
+ const { hour, minute } = parseTime(time);
23
+ return hour * 60 + minute;
24
+ }
25
+ /** Add minutes to a time string, clamping at 24:00. Returns null if result >= 24:00. */
26
+ export function addMinutes(time, minutes) {
27
+ const total = timeToMinutes(time) + minutes;
28
+ if (total >= 1440)
29
+ return null; // past midnight
30
+ return minutesToTime(total);
31
+ }
32
+ /** Parse "YYYY-MM-DD" into { year, month, day } */
33
+ export function parseDate(date) {
34
+ const [y, m, d] = date.split('-').map(Number);
35
+ return { year: y, month: m - 1, day: d }; // month is 0-indexed like JS Date
36
+ }
37
+ /** Format a Date to "YYYY-MM-DD" */
38
+ export function formatDate(date) {
39
+ const y = date.getFullYear();
40
+ const m = String(date.getMonth() + 1).padStart(2, '0');
41
+ const d = String(date.getDate()).padStart(2, '0');
42
+ return `${y}-${m}-${d}`;
43
+ }
44
+ /** Get the day of week (0=Sunday) for a "YYYY-MM-DD" string */
45
+ export function getDayOfWeek(dateStr) {
46
+ const { year, month, day } = parseDate(dateStr);
47
+ return new Date(year, month, day).getDay();
48
+ }
49
+ /** Get the number of days in a given month (0-indexed month) */
50
+ export function daysInMonth(year, month) {
51
+ return new Date(year, month + 1, 0).getDate();
52
+ }
53
+ /** Compare two "YYYY-MM-DD" strings. Returns -1, 0, or 1. */
54
+ export function compareDates(a, b) {
55
+ if (a < b)
56
+ return -1;
57
+ if (a > b)
58
+ return 1;
59
+ return 0;
60
+ }
61
+ /** Generate all dates (as "YYYY-MM-DD") between from and to, inclusive. */
62
+ export function dateRange(from, to) {
63
+ const dates = [];
64
+ const { year: fy, month: fm, day: fd } = parseDate(from);
65
+ const { year: ty, month: tm, day: td } = parseDate(to);
66
+ const start = new Date(fy, fm, fd);
67
+ const end = new Date(ty, tm, td);
68
+ const current = new Date(start);
69
+ while (current <= end) {
70
+ dates.push(formatDate(current));
71
+ current.setDate(current.getDate() + 1);
72
+ }
73
+ return dates;
74
+ }
75
+ /** Get today's date as "YYYY-MM-DD" in a given timezone (or local if not specified). */
76
+ export function getToday(timezone) {
77
+ if (timezone) {
78
+ return formatDateInTimezone(new Date(), timezone);
79
+ }
80
+ return formatDate(new Date());
81
+ }
82
+ /**
83
+ * Format a Date as "YYYY-MM-DD" in a specific timezone using Intl.DateTimeFormat.
84
+ */
85
+ export function formatDateInTimezone(date, timezone) {
86
+ const formatter = new Intl.DateTimeFormat('en-CA', {
87
+ timeZone: timezone,
88
+ year: 'numeric',
89
+ month: '2-digit',
90
+ day: '2-digit',
91
+ });
92
+ // en-CA gives YYYY-MM-DD format
93
+ return formatter.format(date);
94
+ }
95
+ /**
96
+ * Get the hour and minute of a Date in a specific timezone.
97
+ */
98
+ export function getTimeInTimezone(date, timezone) {
99
+ const formatter = new Intl.DateTimeFormat('en-US', {
100
+ timeZone: timezone,
101
+ hour: 'numeric',
102
+ minute: 'numeric',
103
+ hour12: false,
104
+ });
105
+ const parts = formatter.formatToParts(date);
106
+ const hour = Number(parts.find(p => p.type === 'hour')?.value ?? 0);
107
+ const minute = Number(parts.find(p => p.type === 'minute')?.value ?? 0);
108
+ return { hour, minute };
109
+ }
110
+ /**
111
+ * Convert a time from one timezone to another on a given date.
112
+ * Returns the converted time as "HH:mm", or null if the time doesn't exist
113
+ * (e.g., spring-forward DST gap).
114
+ */
115
+ export function convertTime(dateStr, time, fromTimezone, toTimezone) {
116
+ if (fromTimezone === toTimezone)
117
+ return time;
118
+ const { year, month, day } = parseDate(dateStr);
119
+ const { hour, minute } = parseTime(time);
120
+ // Create a Date in the source timezone by using a known UTC offset approach.
121
+ // We build a UTC date then adjust based on the source timezone offset.
122
+ const utcDate = new Date(Date.UTC(year, month, day, hour, minute));
123
+ // Get the offset of the source timezone at this time
124
+ const sourceOffset = getTimezoneOffset(utcDate, fromTimezone);
125
+ // Adjust to actual UTC
126
+ const actualUtc = new Date(utcDate.getTime() + sourceOffset * 60000);
127
+ // Now get the time in the target timezone
128
+ const targetTime = getTimeInTimezone(actualUtc, toTimezone);
129
+ // Check if the source time actually exists (DST spring-forward gap)
130
+ const verifyTime = getTimeInTimezone(actualUtc, fromTimezone);
131
+ if (verifyTime.hour !== hour || verifyTime.minute !== minute) {
132
+ return null; // Time doesn't exist in source timezone (DST gap)
133
+ }
134
+ return formatTime(targetTime.hour, targetTime.minute);
135
+ }
136
+ /**
137
+ * Get the UTC offset of a timezone in minutes at a given instant.
138
+ * Positive means behind UTC (e.g., UTC-5 returns 300).
139
+ */
140
+ function getTimezoneOffset(date, timezone) {
141
+ const utcStr = date.toLocaleString('en-US', { timeZone: 'UTC' });
142
+ const tzStr = date.toLocaleString('en-US', { timeZone: timezone });
143
+ const utcDate = new Date(utcStr);
144
+ const tzDate = new Date(tzStr);
145
+ return (utcDate.getTime() - tzDate.getTime()) / 60000;
146
+ }
147
+ /**
148
+ * Build a Date object from a date string and time string in a given timezone.
149
+ * If timezone is null/undefined (floating), uses local time.
150
+ */
151
+ export function buildDate(dateStr, time, timezone) {
152
+ const { year, month, day } = parseDate(dateStr);
153
+ if (!time) {
154
+ return new Date(year, month, day);
155
+ }
156
+ const { hour, minute } = parseTime(time);
157
+ if (!timezone) {
158
+ // Floating — local time
159
+ return new Date(year, month, day, hour, minute);
160
+ }
161
+ if (timezone === 'UTC') {
162
+ return new Date(Date.UTC(year, month, day, hour, minute));
163
+ }
164
+ // Specific timezone — build via UTC offset
165
+ const utcGuess = new Date(Date.UTC(year, month, day, hour, minute));
166
+ const offset = getTimezoneOffset(utcGuess, timezone);
167
+ return new Date(utcGuess.getTime() + offset * 60000);
168
+ }
169
+ //# sourceMappingURL=time.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"time.js","sourceRoot":"","sources":["../src/time.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ import type { TimelineGridConfig, TimelineSlot, PositionedEvent, CalendarEvent } from './types.js';
2
+ /**
3
+ * TimelineGrid — produces the data structure for rendering hourly/timeline
4
+ * day views. Handles event positioning including overlap detection for
5
+ * side-by-side concurrent events.
6
+ */
7
+ export declare class TimelineGrid {
8
+ slots: TimelineSlot[];
9
+ private date;
10
+ private startHour;
11
+ private endHour;
12
+ private intervalMinutes;
13
+ private events;
14
+ constructor(config: TimelineGridConfig);
15
+ private generate;
16
+ private getEventStartMinutes;
17
+ private getEventEndMinutes;
18
+ }
19
+ /**
20
+ * Compute positioned events with column assignments for overlapping events.
21
+ * Uses a greedy column-assignment algorithm.
22
+ */
23
+ export declare function computeEventPositions(events: CalendarEvent[], startHour?: number, endHour?: number): PositionedEvent[];
24
+ //# sourceMappingURL=timeline.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeline.d.ts","sourceRoot":"","sources":["../src/timeline.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,kBAAkB,EAClB,YAAY,EACZ,eAAe,EACf,aAAa,EACd,MAAM,YAAY,CAAC;AAGpB;;;;GAIG;AACH,qBAAa,YAAY;IACvB,KAAK,EAAE,YAAY,EAAE,CAAC;IAEtB,OAAO,CAAC,IAAI,CAAS;IACrB,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,MAAM,CAAkB;gBAEpB,MAAM,EAAE,kBAAkB;IAStC,OAAO,CAAC,QAAQ;IAsChB,OAAO,CAAC,oBAAoB;IAI5B,OAAO,CAAC,kBAAkB;CAO3B;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,aAAa,EAAE,EACvB,SAAS,GAAE,MAAU,EACrB,OAAO,GAAE,MAAW,GACnB,eAAe,EAAE,CA8EnB"}
@@ -0,0 +1,158 @@
1
+ import { formatTime, timeToMinutes } from './time.js';
2
+ /**
3
+ * TimelineGrid — produces the data structure for rendering hourly/timeline
4
+ * day views. Handles event positioning including overlap detection for
5
+ * side-by-side concurrent events.
6
+ */
7
+ export class TimelineGrid {
8
+ slots;
9
+ date;
10
+ startHour;
11
+ endHour;
12
+ intervalMinutes;
13
+ events;
14
+ constructor(config) {
15
+ this.date = config.date;
16
+ this.startHour = config.startHour ?? 0;
17
+ this.endHour = config.endHour ?? 24;
18
+ this.intervalMinutes = config.intervalMinutes ?? 60;
19
+ this.events = config.events;
20
+ this.slots = this.generate();
21
+ }
22
+ generate() {
23
+ const slots = [];
24
+ const positioned = computeEventPositions(this.events, this.startHour, this.endHour);
25
+ const totalMinutes = (this.endHour - this.startHour) * 60;
26
+ let currentMinutes = this.startHour * 60;
27
+ const endMinutes = this.endHour * 60;
28
+ while (currentMinutes < endMinutes) {
29
+ const hour = Math.floor(currentMinutes / 60);
30
+ const minute = currentMinutes % 60;
31
+ const time = formatTime(hour, minute);
32
+ // Find events that overlap this slot
33
+ const slotEnd = currentMinutes + this.intervalMinutes;
34
+ const overlapping = positioned.filter(pe => {
35
+ const eventStart = this.getEventStartMinutes(pe.event);
36
+ const eventEnd = this.getEventEndMinutes(pe.event);
37
+ return eventStart < slotEnd && eventEnd > currentMinutes;
38
+ });
39
+ slots.push({
40
+ time,
41
+ hour,
42
+ minute,
43
+ events: overlapping,
44
+ });
45
+ currentMinutes += this.intervalMinutes;
46
+ }
47
+ return slots;
48
+ }
49
+ getEventStartMinutes(event) {
50
+ return event.start.getHours() * 60 + event.start.getMinutes();
51
+ }
52
+ getEventEndMinutes(event) {
53
+ if (event.end) {
54
+ return event.end.getHours() * 60 + event.end.getMinutes();
55
+ }
56
+ // Default to 30 minutes if no end time
57
+ return this.getEventStartMinutes(event) + 30;
58
+ }
59
+ }
60
+ /**
61
+ * Compute positioned events with column assignments for overlapping events.
62
+ * Uses a greedy column-assignment algorithm.
63
+ */
64
+ export function computeEventPositions(events, startHour = 0, endHour = 24) {
65
+ if (events.length === 0)
66
+ return [];
67
+ const totalMinutes = (endHour - startHour) * 60;
68
+ const timelineStart = startHour * 60;
69
+ // Sort by start time, then by duration (longer first)
70
+ const sorted = [...events].sort((a, b) => {
71
+ const aStart = a.start.getHours() * 60 + a.start.getMinutes();
72
+ const bStart = b.start.getHours() * 60 + b.start.getMinutes();
73
+ if (aStart !== bStart)
74
+ return aStart - bStart;
75
+ const aEnd = a.end
76
+ ? a.end.getHours() * 60 + a.end.getMinutes()
77
+ : aStart + 30;
78
+ const bEnd = b.end
79
+ ? b.end.getHours() * 60 + b.end.getMinutes()
80
+ : bStart + 30;
81
+ return (bEnd - bStart) - (aEnd - aStart); // Longer events first
82
+ });
83
+ // Assign columns using a greedy approach
84
+ // Track when each column becomes free
85
+ const columnEnds = [];
86
+ const assignments = [];
87
+ for (const event of sorted) {
88
+ const eventStart = event.start.getHours() * 60 + event.start.getMinutes();
89
+ const eventEnd = event.end
90
+ ? event.end.getHours() * 60 + event.end.getMinutes()
91
+ : eventStart + 30;
92
+ // Find the first available column
93
+ let assignedColumn = -1;
94
+ for (let col = 0; col < columnEnds.length; col++) {
95
+ if (columnEnds[col] <= eventStart) {
96
+ assignedColumn = col;
97
+ columnEnds[col] = eventEnd;
98
+ break;
99
+ }
100
+ }
101
+ if (assignedColumn === -1) {
102
+ assignedColumn = columnEnds.length;
103
+ columnEnds.push(eventEnd);
104
+ }
105
+ assignments.push({
106
+ event,
107
+ column: assignedColumn,
108
+ startMin: eventStart,
109
+ endMin: eventEnd,
110
+ });
111
+ }
112
+ // Compute total columns for each group of overlapping events
113
+ // We need to find connected components of overlapping events
114
+ const groups = findOverlapGroups(assignments);
115
+ const positioned = [];
116
+ for (const group of groups) {
117
+ const maxColumn = Math.max(...group.map(a => a.column)) + 1;
118
+ for (const assignment of group) {
119
+ const top = ((assignment.startMin - timelineStart) / totalMinutes) * 100;
120
+ const height = ((assignment.endMin - assignment.startMin) / totalMinutes) * 100;
121
+ positioned.push({
122
+ event: assignment.event,
123
+ top: Math.max(0, top),
124
+ height: Math.min(height, 100 - Math.max(0, top)),
125
+ column: assignment.column,
126
+ totalColumns: maxColumn,
127
+ });
128
+ }
129
+ }
130
+ return positioned;
131
+ }
132
+ /**
133
+ * Find groups of events that overlap with each other (connected components).
134
+ */
135
+ function findOverlapGroups(assignments) {
136
+ if (assignments.length === 0)
137
+ return [];
138
+ const sorted = [...assignments].sort((a, b) => a.startMin - b.startMin);
139
+ const groups = [];
140
+ let currentGroup = [sorted[0]];
141
+ let groupEnd = sorted[0].endMin;
142
+ for (let i = 1; i < sorted.length; i++) {
143
+ if (sorted[i].startMin < groupEnd) {
144
+ // Overlaps with current group
145
+ currentGroup.push(sorted[i]);
146
+ groupEnd = Math.max(groupEnd, sorted[i].endMin);
147
+ }
148
+ else {
149
+ // New group
150
+ groups.push(currentGroup);
151
+ currentGroup = [sorted[i]];
152
+ groupEnd = sorted[i].endMin;
153
+ }
154
+ }
155
+ groups.push(currentGroup);
156
+ return groups;
157
+ }
158
+ //# sourceMappingURL=timeline.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timeline.js","sourceRoot":"","sources":["../src/timeline.ts"],"names":[],"mappings":"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"}