scdate 1.0.1 → 2.0.0

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/README.md CHANGED
@@ -1,42 +1,277 @@
1
1
  # scDate
2
2
 
3
- **Date and time library for dealing with schedules.**
3
+ **Date and time library for working with schedules**
4
4
 
5
5
  [![github license](https://img.shields.io/github/license/ericvera/scdate.svg?style=flat-square)](https://github.com/ericvera/scdate/blob/master/LICENSE)
6
6
  [![npm version](https://img.shields.io/npm/v/scdate.svg?style=flat-square)](https://npmjs.org/package/scdate)
7
7
 
8
- Features:
8
+ ## Overview
9
9
 
10
- - Supports dates, times, time stamps, and active weekdays
11
- - Time zone required for operations only when relevant
12
- - Serializable to simple ISO formatted strings
10
+ scDate is a TypeScript library designed specifically for handling date and time operations in scheduling applications. It provides a set of immutable classes and utility functions that make working with dates, times, timestamps, and weekday patterns simple and predictable.
11
+
12
+ ## Features
13
+
14
+ - **Comprehensive date/time handling**: Work with dates (`SDate`), times (`STime`), timestamps (`STimestamp`), and weekday patterns (`SWeekdays`)
15
+ - **Time zone aware**: Time zone information is only required when performing operations where it's relevant
16
+ - **Serializable**: All objects serialize to simple ISO-formatted strings for easy storage and transmission
17
+ - **Immutable**: All operations return new instances, ensuring data consistency
18
+ - **Flexible inputs**: All methods accept either serialized strings or class instances, simplifying code and improving readability
19
+ - **Schedule-focused**: Built specifically for applications that need to handle schedules and recurring patterns
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install scdate
25
+ # or
26
+ yarn add scdate
27
+ ```
28
+
29
+ ## Basic Usage
30
+
31
+ ```typescript
32
+ import { sDate, sTime, sTimestamp, sWeekdays, Weekday } from 'scdate'
33
+
34
+ // Working with dates
35
+ const today = getDateToday('America/Puerto_Rico')
36
+ const nextMonday = getNextDateByWeekday(today, Weekday.Mon)
37
+
38
+ // Working with times
39
+ const currentTime = getTimeNow('America/Puerto_Rico')
40
+ const inOneHour = addMinutesToTime(currentTime, 60)
41
+
42
+ // Working with timestamps
43
+ const now = getTimestampNow('America/Puerto_Rico')
44
+ const meeting = getTimestampFromDateAndTime(nextMonday, sTime('14:30'))
45
+
46
+ // Working with weekday patterns
47
+ const weekendDays = getWeekdaysFromWeekdayFlags(Weekday.Sat | Weekday.Sun)
48
+ const businessDays = getWeekdaysFromWeekdayFlags(
49
+ Weekday.Mon | Weekday.Tue | Weekday.Wed | Weekday.Thu | Weekday.Fri,
50
+ )
51
+ ```
52
+
53
+ ## API Documentation
54
+
55
+ ### Date Operations (`SDate`)
56
+
57
+ `SDate` represents a date in the ISO 8601 format (`YYYY-MM-DD`).
58
+
59
+ ```typescript
60
+ // Creating dates
61
+ const date1 = sDate('2023-12-25') // From ISO string
62
+ const date2 = getDateToday('America/Puerto_Rico') // Current date in timezone
63
+ const date3 = getDateForFirstDayOfMonth(date1) // First day of month
64
+ const date4 = getDateForLastDayOfMonth(date1) // Last day of month
65
+
66
+ // Date arithmetic
67
+ const nextDay = addDaysToDate(date1, 1) // Add days
68
+ const nextMonth = addMonthsToDate(date1, 1) // Add months
69
+ const nextYear = addYearsToDate(date1, 1) // Add years
70
+
71
+ // Date information
72
+ const year = getYearFromDate(date1) // Get year (number)
73
+ const month = getMonthFromDate(date1) // Get month (0-11)
74
+ const day = getDateFromDate(date1) // Get day of month
75
+ const weekday = getWeekdayFromDate(date1) // Get weekday (0-6)
76
+
77
+ // Date navigation
78
+ const nextTuesday = getNextDateByWeekday(date1, Weekday.Tue)
79
+ const prevFriday = getPreviousDateByWeekday(date1, Weekday.Fri)
80
+
81
+ // Date comparison
82
+ const isEqual = isSameDate(date1, date2)
83
+ const isBefore = isBeforeDate(date1, date2)
84
+ const isAfter = isAfterDate(date1, date2)
85
+ const isSameOrBefore = isSameDateOrBefore(date1, date2)
86
+ const isSameOrAfter = isSameDateOrAfter(date1, date2)
87
+ const isToday = isDateToday(date1, 'America/Puerto_Rico')
88
+
89
+ // Date formatting
90
+ const fullDateStr = getFullDateString(date1, 'en-US')
91
+ const shortDateStr = getShortDateString(date1, 'America/Puerto_Rico', 'en-US', {
92
+ includeWeekday: true,
93
+ onTodayText: () => 'Today',
94
+ })
95
+ ```
96
+
97
+ #### Important Behaviors and Edge Cases
98
+
99
+ - **`getNextDateByWeekday(date, weekday)`**: Always returns a date _after_ the provided date. If the current date falls on the specified weekday, it will return the _next_ occurrence (7 days later), not the current date.
100
+
101
+ - **`getPreviousDateByWeekday(date, weekday)`**: Always returns a date _before_ the provided date. If the current date falls on the specified weekday, it will return the _previous_ occurrence (7 days earlier), not the current date.
102
+
103
+ - **`getDateForFirstDayOfMonth(date)`**: Returns a new date set to the first day of the month from the input date, preserving the year and month.
104
+
105
+ - **`getDateForLastDayOfMonth(date)`**: Returns a new date set to the last day of the month, which varies depending on the month and year (accounting for leap years).
106
+
107
+ - **`addMonthsToDate(date, months, options?)`**: Properly handles month boundaries by clamping to the last day of the target month. For example, adding one month to January 31 will result in February 28/29 (depending on leap year), adding 3 months will result in April 30, and adding 5 months will result in June 30. This ensures consistent and predictable date handling when crossing between months with different numbers of days.
108
+
109
+ Accepts an optional `options` object with:
110
+
111
+ - `capToCommonDate`: When set to `true`, dates greater than the 28th will always be capped to the 28th of the target month (the last date common to all months). For example, `addMonthsToDate('2023-01-31', 3, { capToCommonDate: true })` will result in `'2023-04-28'` rather than `'2023-04-30'`. This is useful for scheduling scenarios where you need consistent date behavior across all months.
112
+
113
+ - **`isDateToday(date, timeZone)`**: The comparison is time-zone aware, so a date that is "today" in one time zone might not be "today" in another time zone.
114
+
115
+ ### Time Operations (`STime`)
116
+
117
+ `STime` represents a time in the ISO 8601 format (`HH:MM`).
118
+
119
+ ```typescript
120
+ // Creating times
121
+ const time1 = sTime('14:30') // From ISO string
122
+ const time2 = getTimeNow('America/Puerto_Rico') // Current time in timezone
123
+ const time3 = getTimeAtMidnight() // 00:00
124
+ const time4 = getTimeFromMinutes(60) // 01:00 (60 minutes after midnight)
125
+
126
+ // Time arithmetic
127
+ const laterTime = addMinutesToTime(time1, 30) // Add minutes
128
+
129
+ // Time information
130
+ const hours = getHoursFromTime(time1) // Get hours (0-23)
131
+ const minutes = getMinutesFromTime(time1) // Get minutes (0-59)
132
+ const totalMinutes = getTimeInMinutes(time1) // Get total minutes since midnight
133
+
134
+ // Time formatting
135
+ const timeString = get12HourTimeString(time1) // e.g., "2:30 PM"
136
+
137
+ // Time comparison
138
+ const isEqual = isSameTime(time1, time2)
139
+ const isBefore = isBeforeTime(time1, time2)
140
+ const isAfter = isAfterTime(time1, time2)
141
+ const isSameOrBefore = isSameTimeOrBefore(time1, time2)
142
+ const isSameOrAfter = isSameTimeOrAfter(time1, time2)
143
+ const isPM = isTimePM(time1)
144
+ ```
145
+
146
+ #### Important Behaviors and Edge Cases
147
+
148
+ - **`addMinutesToTime(time, minutes)`**: When adding minutes, the time wraps around a 24-hour clock. For example, adding 30 minutes to 23:45 results in 00:15, not 24:15.
149
+
150
+ - **`getTimeInMinutes(time, midnightIs24)`**: By default, midnight (00:00) is represented as 0 minutes. If `midnightIs24` is set to `true`, midnight will be represented as 1440 minutes (24 hours).
151
+
152
+ - **`isTimePM(time)`**: Hours from 12:00 to 23:59 are considered PM, while 00:00 to 11:59 are AM. 12:00 is considered PM, not AM.
153
+
154
+ ### Timestamp Operations (`STimestamp`)
155
+
156
+ `STimestamp` combines a date and time in the ISO 8601 format (`YYYY-MM-DDTHH:MM`).
157
+
158
+ ```typescript
159
+ // Creating timestamps
160
+ const ts1 = sTimestamp('2023-12-25T14:30') // From ISO string
161
+ const ts2 = getTimestampNow('America/Puerto_Rico') // Current timestamp in timezone
162
+ const ts3 = getTimestampFromDateAndTime(date1, time1) // From date and time
163
+ const ts4 = getTimestampFromUTCMilliseconds(
164
+ 1640444400000,
165
+ 'America/Puerto_Rico',
166
+ ) // From UTC milliseconds
167
+
168
+ // Timestamp arithmetic
169
+ const tsNextDay = addDaysToTimestamp(ts1, 1)
170
+ const ts30MinLater = addMinutesToTimestamp(ts1, 30, 'America/Puerto_Rico')
171
+
172
+ // Timestamp breakdown
173
+ const tsDate = getDateFromTimestamp(ts1) // Extract date part
174
+ const tsTime = getTimeFromTimestamp(ts1) // Extract time part
175
+
176
+ // Timestamp conversion
177
+ const tsMilliseconds = getUTCMillisecondsFromTimestamp(
178
+ ts1,
179
+ 'America/Puerto_Rico',
180
+ )
181
+ const tsNativeDate = getTimeZonedDateFromTimestamp(ts1, 'America/Puerto_Rico')
182
+ const secondsToTs = getSecondsToTimestamp(ts1, 'America/Puerto_Rico')
183
+
184
+ // Timestamp formatting
185
+ const tsString = getShortTimestampString(ts1, 'America/Puerto_Rico', 'en-US', {
186
+ includeWeekday: true,
187
+ onTodayAtText: () => 'Today at',
188
+ })
189
+
190
+ // Timestamp comparison
191
+ const isEqual = isSameTimestamp(ts1, ts2)
192
+ const isBefore = isBeforeTimestamp(ts1, ts2)
193
+ const isAfter = isAfterTimestamp(ts1, ts2)
194
+ const isSameOrBefore = isSameTimestampOrBefore(ts1, ts2)
195
+ const isSameOrAfter = isSameTimestampOrAfter(ts1, ts2)
196
+ ```
197
+
198
+ #### Important Behaviors and Edge Cases
199
+
200
+ - **`getTimestampFromUTCMilliseconds(milliseconds, timeZone)`**: Converts UTC milliseconds to a timestamp in the specified time zone, accounting for time zone offsets.
201
+
202
+ - **`addMinutesToTimestamp(timestamp, minutes, timeZone)`**: Time zone awareness is critical for this operation, especially around Daylight Saving Time transitions. When a timestamp lands in the "missing hour" during a spring-forward transition, it's adjusted to the valid time.
203
+
204
+ - **`getSecondsToTimestamp(timestamp, timeZone)`**: Returns a positive value for future timestamps and negative for past timestamps. Handles DST transitions correctly but might produce unexpected results for timestamps that fall in skipped or repeated hours during DST transitions.
205
+
206
+ - **`getUTCMillisecondsFromTimestamp(timestamp, timeZone)`**: Converts a timestamp to UTC milliseconds, accounting for the time zone offset at that specific date and time (important for historical dates with different time zone rules).
207
+
208
+ ### Weekday Operations (`SWeekdays`)
209
+
210
+ `SWeekdays` represents a set of weekdays in the format `SMTWTFS`, where each position corresponds to a day of the week (Sunday to Saturday).
211
+
212
+ ```typescript
213
+ // Creating weekday patterns
214
+ const weekdays1 = sWeekdays('SMTWTFS') // All days
215
+ const weekdays2 = sWeekdays('SM----S') // Sunday, Monday, Saturday
216
+ const weekdays3 = getWeekdaysFromWeekdayFlags(Weekday.Mon | Weekday.Wed) // Monday, Wednesday
217
+ const weekdays4 = getWeekdaysWithAllIncluded() // All days
218
+ const weekdays5 = getWeekdaysWithNoneIncluded() // No days
219
+
220
+ // Weekday operations
221
+ const shiftedWeekdays = shiftWeekdaysForward(weekdays2) // Shift pattern one day forward
222
+ const filteredWeekdays = filterWeekdaysForDates(
223
+ // Filter to days in date range
224
+ weekdays1,
225
+ '2023-12-25',
226
+ '2023-12-31',
227
+ )
228
+ const updatedWeekdays = addWeekdayToWeekdays(weekdays5, Weekday.Fri) // Add Friday to pattern
229
+
230
+ // Weekday queries
231
+ const includesMonday = doesWeekdaysIncludeWeekday(weekdays2, Weekday.Mon)
232
+ const hasOverlap = doesWeekdaysHaveOverlapWithWeekdays(weekdays2, weekdays3)
233
+ ```
234
+
235
+ #### Important Behaviors and Edge Cases
236
+
237
+ - **`sWeekdays(pattern)`**: The pattern must be exactly 7 characters long, with each position representing a day from Sunday to Saturday. Valid characters are the first letter of the English weekday name (S, M, T, W, T, F, S) or a dash '-' for excluded days.
238
+
239
+ - **`shiftWeekdaysForward(weekdays)`**: Shifts the pattern forward by one day with circular wrapping. For example, 'SM----S' becomes 'SMT----'. The last character (Saturday) wraps around to become the first day (Sunday) position.
240
+
241
+ - **`filterWeekdaysForDates(weekdays, fromDate, toDate)`**: Returns a new weekday pattern that only includes the weekdays that fall within the given date range. If the date range spans less than a week, only the applicable days are included.
242
+
243
+ - **`getWeekdaysFromWeekdayFlags(flags)`**: Uses bitwise operations to combine multiple weekday flags. Each weekday is represented by a power of 2, allowing for efficient combination and checking.
13
244
 
14
245
  ## Dependencies
15
246
 
16
247
  This package has the following dependencies:
17
248
 
18
- - `date-fns-tz`: used for time zone calculations
19
- - `date-fns`: it is a peer dependency of `date-fns-tz`
20
- - `@date-fns/utc`: used for its `UTCDateMini` implementation that simplifies some of the time calculations
249
+ - `date-fns-tz`: Used for time zone calculations
250
+ - `date-fns`: Peer dependency of `date-fns-tz`
251
+ - `@date-fns/utc`: Used for its `UTCDateMini` implementation that simplifies time calculations
21
252
 
22
253
  ## Design Decisions
23
254
 
24
- ### ISO formatted values
255
+ ### ISO 8601 Format
256
+
257
+ scDate uses a subset of the [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) standard for all date and time representations. This format was chosen for several key benefits:
25
258
 
26
- A subset of [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) is used as the valid format for SDate, STime, and STimestamp. This was done because:
259
+ - **Human readability**: Dates and times are easy to read and interpret
260
+ - **String sortability**: ISO-formatted strings can be sorted chronologically using simple string comparison
261
+ - **Direct comparison**: Values can be compared directly as strings without conversion
27
262
 
28
- - the format is human readable
29
- - the values are easily sortable as strings
30
- - the values are easily comparable as strings
263
+ ### Minute-Level Precision
31
264
 
32
- ### No seconds in time components
265
+ scDate intentionally omits seconds and milliseconds from time representations. This design choice reflects the library's focus on scheduling applications, where:
33
266
 
34
- The library was designed with schedules in mind that do not require second or smaller granularity as such STime and STimestamp only provide minute granularity.
267
+ - Minute-level granularity is typically sufficient for most scheduling needs
268
+ - Simpler time representations lead to more intuitive API and usage patterns
269
+ - Performance is optimized by avoiding unnecessary precision
35
270
 
36
271
  ## Time zones
37
272
 
38
273
  For a list of valid time zones run `Intl.supportedValuesOf('timeZone')` in your environment.
39
274
 
40
- ## API Reference
275
+ ## License
41
276
 
42
- See [docs](docs/README.md)
277
+ MIT
@@ -1,7 +1,3 @@
1
- import { SDate } from './SDate.js';
2
- import { STimestamp } from './STimestamp.js';
3
- export declare const getMillisecondsInUTCFromTimestamp: (timestamp: STimestamp, timeZone: string) => number;
4
- export declare const getMillisecondsInUTCFromDate: (date: SDate, timeZone: string) => number;
5
1
  /**
6
2
  * @param timeZone For a list of valid time zones run
7
3
  * `Intl.supportedValuesOf('timeZone')` on your environment.
@@ -1,23 +1,4 @@
1
- import { getTimezoneOffset, toZonedTime } from 'date-fns-tz';
2
- import { getDateAsUTCDateMini } from './date.js';
3
- import { getTimestampAsUTCDateMini } from './timestamp.js';
4
- const getValidatedTimeZoneOffset = (timeZone, utcDate) => {
5
- const timeZoneOffset = getTimezoneOffset(timeZone, utcDate);
6
- if (isNaN(timeZoneOffset)) {
7
- throw new Error(`Invalid time zone. Time zone: '${timeZone}'`);
8
- }
9
- return timeZoneOffset;
10
- };
11
- export const getMillisecondsInUTCFromTimestamp = (timestamp, timeZone) => {
12
- const utcDate = getTimestampAsUTCDateMini(timestamp);
13
- const timeZoneOffset = getValidatedTimeZoneOffset(timeZone, utcDate);
14
- return utcDate.getTime() - timeZoneOffset;
15
- };
16
- export const getMillisecondsInUTCFromDate = (date, timeZone) => {
17
- const utcDate = getDateAsUTCDateMini(date);
18
- const timeZoneOffset = getValidatedTimeZoneOffset(timeZone, utcDate);
19
- return utcDate.getTime() - timeZoneOffset;
20
- };
1
+ import { toZonedTime } from 'date-fns-tz';
21
2
  /**
22
3
  * @param timeZone For a list of valid time zones run
23
4
  * `Intl.supportedValuesOf('timeZone')` on your environment.
package/dist/sDate.d.ts CHANGED
@@ -90,6 +90,16 @@ export declare const getDateFromDate: (date: string | SDate) => number;
90
90
  * in the YYYY-MM-DD format.
91
91
  */
92
92
  export declare const getWeekdayFromDate: (date: string | SDate) => number;
93
+ /**
94
+ * Returns the number of milliseconds since the Unix epoch (January 1, 1970, 00:00:00 UTC)
95
+ * for the given date in the specified time zone.
96
+ *
97
+ * @param date The date to convert to UTC milliseconds. It can be an SDate or a string
98
+ * in the YYYY-MM-DD format.
99
+ * @param timeZone The time zone to use when converting the date. See
100
+ * `Intl.supportedValuesOf('timeZone')` for a list of valid time zones.
101
+ */
102
+ export declare const getUTCMillisecondsFromDate: (date: string | SDate, timeZone: string) => number;
93
103
  /**
94
104
  * Returns a native Date adjusted so that the local time of that date matches
95
105
  * the local time at the specified time zone.
@@ -181,11 +191,47 @@ export declare const addDaysToDate: (date: string | SDate, days: number) => SDat
181
191
  * number of months to the given date. Because it just adds to the month
182
192
  * component of the date, this operation is not affected by time zones.
183
193
  *
194
+ * When the original date's day exceeds the number of days in the target month,
195
+ * the function will automatically clamp to the last day of the target month
196
+ * rather than rolling over to the next month. For example, adding 1 month to
197
+ * January 31 will result in February 28/29 (depending on leap year), not March 3.
198
+ *
184
199
  * @param date The date to add months to. It can be an SDate or a string in the
185
200
  * YYYY-MM-DD format.
186
201
  * @param months The number of months to add to the date.
202
+ * @param options Additional options for controlling the behavior of the function.
203
+ * @param options.capToCommonDate When true, if the original date is the 29th,
204
+ * 30th, or 31st, the result will be capped to the 28th of the month (the last
205
+ * date common to all months) rather than the last day of the target month.
206
+ * This ensures consistent date handling across all months.
207
+ *
208
+ * @example
209
+ * ```ts
210
+ * addMonthsToDate('2023-01-31', 1)
211
+ * //=> '2023-02-28' (February has fewer days than January)
212
+ * ```
213
+ *
214
+ * @example
215
+ * ```ts
216
+ * addMonthsToDate('2023-01-31', 3)
217
+ * //=> '2023-04-30' (April has 30 days)
218
+ * ```
219
+ *
220
+ * @example
221
+ * ```ts
222
+ * addMonthsToDate('2024-01-31', 1)
223
+ * //=> '2024-02-29' (February in leap year)
224
+ * ```
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * addMonthsToDate('2023-01-31', 3, { capToCommonDate: true })
229
+ * //=> '2023-04-28' (capped to the 28th regardless of month length)
230
+ * ```
187
231
  */
188
- export declare const addMonthsToDate: (date: string | SDate, months: number) => SDate;
232
+ export declare const addMonthsToDate: (date: string | SDate, months: number, options?: {
233
+ capToCommonDate?: boolean;
234
+ }) => SDate;
189
235
  /**
190
236
  * Returns a new SDate instance with the date resulting from adding the given
191
237
  * number of years to the given date. Because this only adds to the year
package/dist/sDate.js CHANGED
@@ -1,9 +1,11 @@
1
+ import { UTCDateMini } from '@date-fns/utc';
2
+ import { getTimezoneOffset } from 'date-fns-tz';
1
3
  import { SDate } from './internal/SDate.js';
2
4
  import { DayToWeekday, DaysInWeek, MillisecondsInDay, } from './internal/constants.js';
3
5
  import { getDateAsUTCDateMini, getISODateFromISODate, getISODateFromZonedDate, getISOMonthFromISODate, getISOYearFromISODate, } from './internal/date.js';
4
6
  import { getAtIndex } from './internal/utils.js';
5
7
  import { getIndexForWeekday } from './internal/weekdays.js';
6
- import { getMillisecondsInUTCFromDate, getTimeZonedDate, } from './internal/zoned.js';
8
+ import { getTimeZonedDate } from './internal/zoned.js';
7
9
  /**
8
10
  * --- Factory ---
9
11
  */
@@ -142,6 +144,24 @@ export const getWeekdayFromDate = (date) => {
142
144
  const nativeDate = getDateAsUTCDateMini(sDateValue);
143
145
  return getAtIndex(DayToWeekday, nativeDate.getDay());
144
146
  };
147
+ /**
148
+ * Returns the number of milliseconds since the Unix epoch (January 1, 1970, 00:00:00 UTC)
149
+ * for the given date in the specified time zone.
150
+ *
151
+ * @param date The date to convert to UTC milliseconds. It can be an SDate or a string
152
+ * in the YYYY-MM-DD format.
153
+ * @param timeZone The time zone to use when converting the date. See
154
+ * `Intl.supportedValuesOf('timeZone')` for a list of valid time zones.
155
+ */
156
+ export const getUTCMillisecondsFromDate = (date, timeZone) => {
157
+ const sDateValue = sDate(date);
158
+ const utcDate = getDateAsUTCDateMini(sDateValue);
159
+ const timeZoneOffset = getTimezoneOffset(timeZone, utcDate);
160
+ if (isNaN(timeZoneOffset)) {
161
+ throw new Error(`Invalid time zone. Time zone: '${timeZone}'`);
162
+ }
163
+ return utcDate.getTime() - timeZoneOffset;
164
+ };
145
165
  /**
146
166
  * Returns a native Date adjusted so that the local time of that date matches
147
167
  * the local time at the specified time zone.
@@ -153,7 +173,7 @@ export const getWeekdayFromDate = (date) => {
153
173
  */
154
174
  export const getTimeZonedDateFromDate = (date, timeZone) => {
155
175
  const sDateValue = sDate(date);
156
- const milliseconds = getMillisecondsInUTCFromDate(sDateValue, timeZone);
176
+ const milliseconds = getUTCMillisecondsFromDate(sDateValue, timeZone);
157
177
  const zonedTime = getTimeZonedDate(milliseconds, timeZone);
158
178
  return zonedTime;
159
179
  };
@@ -270,14 +290,63 @@ export const addDaysToDate = (date, days) => {
270
290
  * number of months to the given date. Because it just adds to the month
271
291
  * component of the date, this operation is not affected by time zones.
272
292
  *
293
+ * When the original date's day exceeds the number of days in the target month,
294
+ * the function will automatically clamp to the last day of the target month
295
+ * rather than rolling over to the next month. For example, adding 1 month to
296
+ * January 31 will result in February 28/29 (depending on leap year), not March 3.
297
+ *
273
298
  * @param date The date to add months to. It can be an SDate or a string in the
274
299
  * YYYY-MM-DD format.
275
300
  * @param months The number of months to add to the date.
301
+ * @param options Additional options for controlling the behavior of the function.
302
+ * @param options.capToCommonDate When true, if the original date is the 29th,
303
+ * 30th, or 31st, the result will be capped to the 28th of the month (the last
304
+ * date common to all months) rather than the last day of the target month.
305
+ * This ensures consistent date handling across all months.
306
+ *
307
+ * @example
308
+ * ```ts
309
+ * addMonthsToDate('2023-01-31', 1)
310
+ * //=> '2023-02-28' (February has fewer days than January)
311
+ * ```
312
+ *
313
+ * @example
314
+ * ```ts
315
+ * addMonthsToDate('2023-01-31', 3)
316
+ * //=> '2023-04-30' (April has 30 days)
317
+ * ```
318
+ *
319
+ * @example
320
+ * ```ts
321
+ * addMonthsToDate('2024-01-31', 1)
322
+ * //=> '2024-02-29' (February in leap year)
323
+ * ```
324
+ *
325
+ * @example
326
+ * ```ts
327
+ * addMonthsToDate('2023-01-31', 3, { capToCommonDate: true })
328
+ * //=> '2023-04-28' (capped to the 28th regardless of month length)
329
+ * ```
276
330
  */
277
- export const addMonthsToDate = (date, months) => {
331
+ export const addMonthsToDate = (date, months, options) => {
278
332
  const sDateValue = sDate(date);
279
333
  const nativeDate = getDateAsUTCDateMini(sDateValue);
280
- nativeDate.setMonth(nativeDate.getMonth() + months);
334
+ const currentDay = nativeDate.getDate();
335
+ const currentMonth = nativeDate.getMonth();
336
+ // First set day to 1 to avoid month overflow
337
+ nativeDate.setDate(1);
338
+ // Then set the new month
339
+ nativeDate.setMonth(currentMonth + months);
340
+ // If capToCommonDate is true and the original day is greater than 28, cap to 28
341
+ if (options?.capToCommonDate && currentDay > 28) {
342
+ nativeDate.setDate(28);
343
+ }
344
+ else {
345
+ // Get the last day of the target month
346
+ const lastDayOfMonth = new UTCDateMini(nativeDate.getFullYear(), nativeDate.getMonth() + 1, 0).getDate();
347
+ // Set the day, clamping to the last day of the month if necessary
348
+ nativeDate.setDate(Math.min(currentDay, lastDayOfMonth));
349
+ }
281
350
  return sDate(getISODateFromZonedDate(nativeDate));
282
351
  };
283
352
  /**
@@ -49,6 +49,16 @@ export declare const getTimestampFromDateAndTime: (date: string | SDate, time: s
49
49
  /**
50
50
  * --- Getters ---
51
51
  */
52
+ /**
53
+ * Returns the number of milliseconds since the Unix epoch (January 1, 1970, 00:00:00 UTC)
54
+ * for the given timestamp in the specified time zone.
55
+ *
56
+ * @param timestamp The timestamp to convert to UTC milliseconds. It can be an STimestamp
57
+ * or a string in the YYYY-MM-DDTHH:MM format.
58
+ * @param timeZone The time zone to use when converting the timestamp. See
59
+ * `Intl.supportedValuesOf('timeZone')` for a list of valid time zones.
60
+ */
61
+ export declare const getUTCMillisecondsFromTimestamp: (timestamp: string | STimestamp, timeZone: string) => number;
52
62
  /**
53
63
  * Returns a native Date adjusted so that the local time of that date matches
54
64
  * the local timestamp at the specified time zone.
@@ -197,10 +207,10 @@ export declare const addDaysToTimestamp: (timestamp: string | STimestamp, days:
197
207
  *
198
208
  * Time is converted from the given time zone to
199
209
  * UTC before the minutes are added, and then converted back to the specified
200
- * time zone. This results in the resulting time being adjusted for daylight saving time
201
- * changes. (e.g. Adding 60 minutes to 2024-03-10T01:59 in America/New_York will
202
- * result in 2024-03-10T03:59 as time move forward one hour for daylight saving
203
- * time at 2024-03-10T02:00.)
210
+ * time zone. This results in the resulting time being adjusted for daylight
211
+ * saving time changes. (e.g. Adding 60 minutes to 2024-03-10T01:59 in
212
+ * America/New_York will result in 2024-03-10T03:59 as time move forward one
213
+ * hour for daylight saving time at 2024-03-10T02:00.)
204
214
  *
205
215
  * For example, adding one minute to 2024-03-10T01:59 will result in
206
216
  * 2024-03-10T03:00 as expected. However, trying to add one minute to
@@ -1,7 +1,8 @@
1
+ import { getTimezoneOffset } from 'date-fns-tz';
1
2
  import { STimestamp } from './internal/STimestamp.js';
2
3
  import { MillisecondsInMinute, MillisecondsInSecond, } from './internal/constants.js';
3
4
  import { getISODateFromISOTimestamp, getISOTimeFromISOTimestamp, getISOTimestampFromZonedDate, getTimestampAsUTCDateMini, } from './internal/timestamp.js';
4
- import { getMillisecondsInUTCFromTimestamp, getTimeZonedDate, } from './internal/zoned.js';
5
+ import { getTimeZonedDate } from './internal/zoned.js';
5
6
  import { getShortDateString, sDate } from './sDate.js';
6
7
  import { get12HourTimeString, sTime } from './sTime.js';
7
8
  /**
@@ -62,6 +63,24 @@ export const getTimestampFromDateAndTime = (date, time) => {
62
63
  /**
63
64
  * --- Getters ---
64
65
  */
66
+ /**
67
+ * Returns the number of milliseconds since the Unix epoch (January 1, 1970, 00:00:00 UTC)
68
+ * for the given timestamp in the specified time zone.
69
+ *
70
+ * @param timestamp The timestamp to convert to UTC milliseconds. It can be an STimestamp
71
+ * or a string in the YYYY-MM-DDTHH:MM format.
72
+ * @param timeZone The time zone to use when converting the timestamp. See
73
+ * `Intl.supportedValuesOf('timeZone')` for a list of valid time zones.
74
+ */
75
+ export const getUTCMillisecondsFromTimestamp = (timestamp, timeZone) => {
76
+ const sTimestampValue = sTimestamp(timestamp);
77
+ const utcDate = getTimestampAsUTCDateMini(sTimestampValue);
78
+ const timeZoneOffset = getTimezoneOffset(timeZone, utcDate);
79
+ if (isNaN(timeZoneOffset)) {
80
+ throw new Error(`Invalid time zone. Time zone: '${timeZone}'`);
81
+ }
82
+ return utcDate.getTime() - timeZoneOffset;
83
+ };
65
84
  /**
66
85
  * Returns a native Date adjusted so that the local time of that date matches
67
86
  * the local timestamp at the specified time zone.
@@ -73,7 +92,7 @@ export const getTimestampFromDateAndTime = (date, time) => {
73
92
  */
74
93
  export const getTimeZonedDateFromTimestamp = (timestamp, timeZone) => {
75
94
  const sTimestampValue = sTimestamp(timestamp);
76
- const dateInUTCMilliseconds = getMillisecondsInUTCFromTimestamp(sTimestampValue, timeZone);
95
+ const dateInUTCMilliseconds = getUTCMillisecondsFromTimestamp(sTimestampValue, timeZone);
77
96
  return getTimeZonedDate(dateInUTCMilliseconds, timeZone);
78
97
  };
79
98
  /**
@@ -130,7 +149,7 @@ export const getTimeZonedDateFromTimestamp = (timestamp, timeZone) => {
130
149
  export const getSecondsToTimestamp = (timestamp, timeZone) => {
131
150
  const sTimestampValue = sTimestamp(timestamp);
132
151
  const millisecondsNow = Date.now();
133
- const millisecondsAtTimestamp = getMillisecondsInUTCFromTimestamp(sTimestampValue, timeZone);
152
+ const millisecondsAtTimestamp = getUTCMillisecondsFromTimestamp(sTimestampValue, timeZone);
134
153
  return Math.floor((millisecondsAtTimestamp - millisecondsNow) / MillisecondsInSecond);
135
154
  };
136
155
  /**
@@ -240,10 +259,10 @@ export const addDaysToTimestamp = (timestamp, days) => {
240
259
  *
241
260
  * Time is converted from the given time zone to
242
261
  * UTC before the minutes are added, and then converted back to the specified
243
- * time zone. This results in the resulting time being adjusted for daylight saving time
244
- * changes. (e.g. Adding 60 minutes to 2024-03-10T01:59 in America/New_York will
245
- * result in 2024-03-10T03:59 as time move forward one hour for daylight saving
246
- * time at 2024-03-10T02:00.)
262
+ * time zone. This results in the resulting time being adjusted for daylight
263
+ * saving time changes. (e.g. Adding 60 minutes to 2024-03-10T01:59 in
264
+ * America/New_York will result in 2024-03-10T03:59 as time move forward one
265
+ * hour for daylight saving time at 2024-03-10T02:00.)
247
266
  *
248
267
  * For example, adding one minute to 2024-03-10T01:59 will result in
249
268
  * 2024-03-10T03:00 as expected. However, trying to add one minute to
@@ -278,7 +297,7 @@ export const addDaysToTimestamp = (timestamp, days) => {
278
297
  */
279
298
  export const addMinutesToTimestamp = (timestamp, minutes, timeZone) => {
280
299
  const sTimestampValue = sTimestamp(timestamp);
281
- const newMillisecondsInUTC = getMillisecondsInUTCFromTimestamp(sTimestampValue, timeZone) +
300
+ const newMillisecondsInUTC = getUTCMillisecondsFromTimestamp(sTimestampValue, timeZone) +
282
301
  minutes * MillisecondsInMinute;
283
302
  const newTimestamp = getTimestampFromUTCMilliseconds(newMillisecondsInUTC, timeZone);
284
303
  return newTimestamp;
package/dist/sWeekdays.js CHANGED
@@ -62,7 +62,7 @@ export const sWeekdays = (weekdays) => {
62
62
  * ```
63
63
  */
64
64
  export const getWeekdaysFromWeekdayFlags = (weekdays) => {
65
- const newWeekdays = [...AllWeekdaysIncludedMask];
65
+ const newWeekdays = Array.from(AllWeekdaysIncludedMask);
66
66
  for (let i = 0; i < DayToWeekday.length; i++) {
67
67
  const weekday = getAtIndex(DayToWeekday, i);
68
68
  if (!hasFlag(weekdays, weekday)) {
@@ -102,7 +102,7 @@ export const getWeekdaysWithNoneIncluded = () => {
102
102
  */
103
103
  export const shiftWeekdaysForward = (weekdays) => {
104
104
  const sWeekdaysInstance = sWeekdays(weekdays);
105
- const after = [...NoWeekdaysIncluded];
105
+ const after = Array.from(NoWeekdaysIncluded);
106
106
  const DayShift = 1;
107
107
  for (let i = 0; i < WeekdaysCount; i++) {
108
108
  const prevDayIndex = (WeekdaysCount - DayShift + i) % WeekdaysCount;
@@ -163,7 +163,7 @@ export const filterWeekdaysForDates = (weekdays, fromDate, toDate) => {
163
163
  */
164
164
  export const addWeekdayToWeekdays = (weekdays, weekdayToAdd) => {
165
165
  const sWeekdaysInstance = sWeekdays(weekdays);
166
- const newWeekdays = [...sWeekdaysInstance.weekdays];
166
+ const newWeekdays = Array.from(sWeekdaysInstance.weekdays);
167
167
  const weekdayIndex = getIndexForWeekday(weekdayToAdd);
168
168
  newWeekdays[weekdayIndex] = getAtIndex(AllWeekdaysIncludedMask, weekdayIndex);
169
169
  return sWeekdays(newWeekdays.join(''));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scdate",
3
- "version": "1.0.1",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -23,9 +23,8 @@
23
23
  "test:watch": "vitest",
24
24
  "test:utc:watch": "TZ=Etc/Universal yarn test:watch",
25
25
  "smoke": "yarn build && yarn lint && yarn test:utc",
26
- "docs": "typedoc && prettier --ignore-unknown --write docs/",
27
26
  "-- PRE-COMMIT HOOKS --": "",
28
- "localAfterInstall": "is-ci || husky || true",
27
+ "localAfterInstall": "husky || true",
29
28
  "prepublishOnly": "pinst --disable",
30
29
  "postpublish": "pinst --enable"
31
30
  },
@@ -35,21 +34,18 @@
35
34
  "date-fns-tz": "^3.2.0"
36
35
  },
37
36
  "devDependencies": {
38
- "@eslint/js": "^9.11.1",
37
+ "@eslint/js": "^9.25.0",
39
38
  "@tsconfig/strictest": "^2.0.5",
40
- "@types/node": "^22.7.4",
41
- "eslint": "^9.11.1",
42
- "husky": "^9.1.6",
43
- "is-ci": "^3.0.1",
44
- "lint-staged": "^15.2.10",
39
+ "@types/node": "^22.14.1",
40
+ "eslint": "^9.25.0",
41
+ "husky": "^9.1.7",
42
+ "lint-staged": "^15.5.1",
45
43
  "pinst": "^3.0.0",
46
- "prettier": "^3.3.3",
44
+ "prettier": "^3.5.3",
47
45
  "rimraf": "^6.0.1",
48
- "typedoc": "^0.26.7",
49
- "typedoc-plugin-markdown": "^4.2.8",
50
- "typescript": "^5.6.2",
51
- "typescript-eslint": "^8.8.0",
52
- "vitest": "^2.1.1"
46
+ "typescript": "^5.8.3",
47
+ "typescript-eslint": "^8.30.1",
48
+ "vitest": "^3.1.2"
53
49
  },
54
50
  "prettier": {
55
51
  "tabWidth": 2,
@@ -74,5 +70,5 @@
74
70
  "*.{ts,tsx,mjs}": "eslint --cache",
75
71
  "*": "prettier --ignore-unknown --write"
76
72
  },
77
- "packageManager": "yarn@4.3.0"
73
+ "packageManager": "yarn@4.9.1"
78
74
  }