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+
110export const unitHierarchy = [
211 'years' ,
312 'months' ,
@@ -11,70 +20,153 @@ export const unitHierarchy = [
1120export type TimeUnit = ( typeof unitHierarchy ) [ number ] ;
1221export 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+
1452export 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+
50111export 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 ( / ^ G M T (?: ( [ + - ] \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
118215export 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