Skip to content

Commit 1b2fcdb

Browse files
authored
Merge pull request #94 from TheLukasHenry/#84-fix-dates
#84 fix dates
2 parents fe5dffa + 2f1be19 commit 1b2fcdb

File tree

4 files changed

+241
-71
lines changed

4 files changed

+241
-71
lines changed

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"@types/omggif": "^1.0.5",
4242
"browser-image-compression": "^2.0.2",
4343
"color": "^4.2.3",
44+
"dayjs": "^1.11.13",
4445
"formik": "^2.4.6",
4546
"jimp": "^0.22.12",
4647
"lint-staged": "^15.4.3",
Lines changed: 155 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
import dayjs from 'dayjs';
2+
import utc from 'dayjs/plugin/utc';
3+
import timezone from 'dayjs/plugin/timezone';
4+
import duration from 'dayjs/plugin/duration';
5+
6+
dayjs.extend(utc);
7+
dayjs.extend(timezone);
8+
dayjs.extend(duration);
9+
110
export const unitHierarchy = [
211
'years',
312
'months',
@@ -11,70 +20,153 @@ export const unitHierarchy = [
1120
export type TimeUnit = (typeof unitHierarchy)[number];
1221
export type TimeDifference = Record<TimeUnit, number>;
1322

23+
// Mapping common abbreviations to IANA time zone names
24+
export const tzMap: { [abbr: string]: string } = {
25+
EST: 'America/New_York',
26+
EDT: 'America/New_York',
27+
CST: 'America/Chicago',
28+
CDT: 'America/Chicago',
29+
MST: 'America/Denver',
30+
MDT: 'America/Denver',
31+
PST: 'America/Los_Angeles',
32+
PDT: 'America/Los_Angeles',
33+
GMT: 'Etc/GMT',
34+
UTC: 'Etc/UTC'
35+
// add more mappings as needed
36+
};
37+
38+
// Parse a date string with a time zone abbreviation,
39+
// e.g. "02/02/2024 14:55 EST"
40+
export const parseWithTZ = (dateTimeStr: string): dayjs.Dayjs => {
41+
const parts = dateTimeStr.trim().split(' ');
42+
const tzAbbr = parts.pop()!; // extract the timezone part (e.g., EST)
43+
const dateTimePart = parts.join(' ');
44+
const tzName = tzMap[tzAbbr];
45+
if (!tzName) {
46+
throw new Error(`Timezone abbreviation ${tzAbbr} not supported`);
47+
}
48+
// Parse using the format "MM/DD/YYYY HH:mm" in the given time zone
49+
return dayjs.tz(dateTimePart, 'MM/DD/YYYY HH:mm', tzName);
50+
};
51+
1452
export const calculateTimeBetweenDates = (
1553
startDate: Date,
1654
endDate: Date
1755
): TimeDifference => {
18-
if (endDate < startDate) {
19-
const temp = startDate;
20-
startDate = endDate;
21-
endDate = temp;
56+
let start = dayjs(startDate);
57+
let end = dayjs(endDate);
58+
59+
// Swap dates if start is after end
60+
if (end.isBefore(start)) {
61+
[start, end] = [end, start];
2262
}
2363

24-
const milliseconds = endDate.getTime() - startDate.getTime();
25-
const seconds = Math.floor(milliseconds / 1000);
26-
const minutes = Math.floor(seconds / 60);
27-
const hours = Math.floor(minutes / 60);
28-
const days = Math.floor(hours / 24);
64+
// Calculate each unit incrementally so that the remainder is applied for subsequent units.
65+
const years = end.diff(start, 'year');
66+
const startPlusYears = start.add(years, 'year');
67+
68+
const months = end.diff(startPlusYears, 'month');
69+
const startPlusMonths = startPlusYears.add(months, 'month');
70+
71+
const days = end.diff(startPlusMonths, 'day');
72+
const startPlusDays = startPlusMonths.add(days, 'day');
2973

30-
// Approximate months and years
31-
const startYear = startDate.getFullYear();
32-
const startMonth = startDate.getMonth();
33-
const endYear = endDate.getFullYear();
34-
const endMonth = endDate.getMonth();
74+
const hours = end.diff(startPlusDays, 'hour');
75+
const startPlusHours = startPlusDays.add(hours, 'hour');
3576

36-
const months = (endYear - startYear) * 12 + (endMonth - startMonth);
37-
const years = Math.floor(months / 12);
77+
const minutes = end.diff(startPlusHours, 'minute');
78+
const startPlusMinutes = startPlusHours.add(minutes, 'minute');
79+
80+
const seconds = end.diff(startPlusMinutes, 'second');
81+
const startPlusSeconds = startPlusMinutes.add(seconds, 'second');
82+
83+
const milliseconds = end.diff(startPlusSeconds, 'millisecond');
3884

3985
return {
40-
milliseconds,
41-
seconds,
42-
minutes,
43-
hours,
44-
days,
86+
years,
4587
months,
46-
years
88+
days,
89+
hours,
90+
minutes,
91+
seconds,
92+
milliseconds
4793
};
4894
};
4995

96+
// Calculate duration between two date strings with timezone abbreviations
97+
export const getDuration = (
98+
startStr: string,
99+
endStr: string
100+
): TimeDifference => {
101+
const start = parseWithTZ(startStr);
102+
const end = parseWithTZ(endStr);
103+
104+
if (end.isBefore(start)) {
105+
throw new Error('End date must be after start date');
106+
}
107+
108+
return calculateTimeBetweenDates(start.toDate(), end.toDate());
109+
};
110+
50111
export const formatTimeDifference = (
51112
difference: TimeDifference,
52-
includeUnits: TimeUnit[] = unitHierarchy.slice(0, -1)
113+
includeUnits: TimeUnit[] = unitHierarchy.slice(0, -2)
53114
): string => {
54-
const timeUnits: { key: TimeUnit; value: number; divisor?: number }[] = [
55-
{ key: 'years', value: difference.years },
56-
{ key: 'months', value: difference.months, divisor: 12 },
57-
{ key: 'days', value: difference.days, divisor: 30 },
58-
{ key: 'hours', value: difference.hours, divisor: 24 },
59-
{ key: 'minutes', value: difference.minutes, divisor: 60 },
60-
{ key: 'seconds', value: difference.seconds, divisor: 60 }
115+
// First normalize the values (convert 24 hours to 1 day, etc.)
116+
const normalized = { ...difference };
117+
118+
// Convert milliseconds to seconds
119+
if (normalized.milliseconds >= 1000) {
120+
const additionalSeconds = Math.floor(normalized.milliseconds / 1000);
121+
normalized.seconds += additionalSeconds;
122+
normalized.milliseconds %= 1000;
123+
}
124+
125+
// Convert seconds to minutes
126+
if (normalized.seconds >= 60) {
127+
const additionalMinutes = Math.floor(normalized.seconds / 60);
128+
normalized.minutes += additionalMinutes;
129+
normalized.seconds %= 60;
130+
}
131+
132+
// Convert minutes to hours
133+
if (normalized.minutes >= 60) {
134+
const additionalHours = Math.floor(normalized.minutes / 60);
135+
normalized.hours += additionalHours;
136+
normalized.minutes %= 60;
137+
}
138+
139+
// Convert hours to days if 24 or more
140+
if (normalized.hours >= 24) {
141+
const additionalDays = Math.floor(normalized.hours / 24);
142+
normalized.days += additionalDays;
143+
normalized.hours %= 24;
144+
}
145+
146+
const timeUnits: { key: TimeUnit; value: number; label: string }[] = [
147+
{ key: 'years', value: normalized.years, label: 'year' },
148+
{ key: 'months', value: normalized.months, label: 'month' },
149+
{ key: 'days', value: normalized.days, label: 'day' },
150+
{ key: 'hours', value: normalized.hours, label: 'hour' },
151+
{ key: 'minutes', value: normalized.minutes, label: 'minute' },
152+
{ key: 'seconds', value: normalized.seconds, label: 'second' },
153+
{
154+
key: 'milliseconds',
155+
value: normalized.milliseconds,
156+
label: 'millisecond'
157+
}
61158
];
62159

63160
const parts = timeUnits
64161
.filter(({ key }) => includeUnits.includes(key))
65-
.map(({ key, value, divisor }) => {
66-
const remaining = divisor ? value % divisor : value;
67-
return remaining > 0 ? `${remaining} ${key}` : '';
162+
.map(({ value, label }) => {
163+
if (value === 0) return '';
164+
return `${value} ${label}${value === 1 ? '' : 's'}`;
68165
})
69166
.filter(Boolean);
70167

71168
if (parts.length === 0) {
72-
if (includeUnits.includes('milliseconds')) {
73-
return `${difference.milliseconds} millisecond${
74-
difference.milliseconds === 1 ? '' : 's'
75-
}`;
76-
}
77-
return '0 seconds';
169+
return '0 minutes';
78170
}
79171

80172
return parts.join(', ');
@@ -85,45 +177,49 @@ export const getTimeWithTimezone = (
85177
timeString: string,
86178
timezone: string
87179
): Date => {
88-
// Combine date and time
89-
const dateTimeString = `${dateString}T${timeString}Z`; // Append 'Z' to enforce UTC parsing
90-
const utcDate = new Date(dateTimeString);
91-
92-
if (isNaN(utcDate.getTime())) {
93-
throw new Error('Invalid date or time format');
94-
}
95-
96180
// If timezone is "local", return the local date
97181
if (timezone === 'local') {
98-
return utcDate;
182+
const dateTimeString = `${dateString}T${timeString}`;
183+
return dayjs(dateTimeString).toDate();
99184
}
100185

101-
// Extract offset from timezone (e.g., "GMT+5:30" or "GMT-4")
186+
// Check if the timezone is a known abbreviation
187+
if (tzMap[timezone]) {
188+
const dateTimeString = `${dateString} ${timeString}`;
189+
return dayjs
190+
.tz(dateTimeString, 'YYYY-MM-DD HH:mm', tzMap[timezone])
191+
.toDate();
192+
}
193+
194+
// Handle GMT+/- format
102195
const match = timezone.match(/^GMT(?:([+-]\d{1,2})(?::(\d{2}))?)?$/);
103196
if (!match) {
104197
throw new Error('Invalid timezone format');
105198
}
106199

200+
const dateTimeString = `${dateString}T${timeString}Z`;
201+
const utcDate = dayjs.utc(dateTimeString);
202+
203+
if (!utcDate.isValid()) {
204+
throw new Error('Invalid date or time format');
205+
}
206+
107207
const offsetHours = match[1] ? parseInt(match[1], 10) : 0;
108208
const offsetMinutes = match[2] ? parseInt(match[2], 10) : 0;
109-
110209
const totalOffsetMinutes =
111210
offsetHours * 60 + (offsetHours < 0 ? -offsetMinutes : offsetMinutes);
112211

113-
// Adjust the UTC date by the timezone offset
114-
return new Date(utcDate.getTime() - totalOffsetMinutes * 60 * 1000);
212+
return utcDate.subtract(totalOffsetMinutes, 'minute').toDate();
115213
};
116214

117-
// Helper function to format time based on largest unit
118215
export const formatTimeWithLargestUnit = (
119216
difference: TimeDifference,
120217
largestUnit: TimeUnit
121218
): string => {
122219
const largestUnitIndex = unitHierarchy.indexOf(largestUnit);
123-
const unitsToInclude = unitHierarchy.slice(largestUnitIndex);
124-
125-
// Preserve only whole values, do not apply fractional conversions
126-
const adjustedDifference: TimeDifference = { ...difference };
127-
128-
return formatTimeDifference(adjustedDifference, unitsToInclude);
220+
const unitsToInclude = unitHierarchy.slice(
221+
largestUnitIndex,
222+
unitHierarchy.length // Include milliseconds if it's the largest unit requested
223+
);
224+
return formatTimeDifference(difference, unitsToInclude);
129225
};

0 commit comments

Comments
 (0)