scdate 1.1.0 → 2.0.2

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
@@ -78,6 +78,11 @@ const weekday = getWeekdayFromDate(date1) // Get weekday (0-6)
78
78
  const nextTuesday = getNextDateByWeekday(date1, Weekday.Tue)
79
79
  const prevFriday = getPreviousDateByWeekday(date1, Weekday.Fri)
80
80
 
81
+ // Date calculations
82
+ const daysBetween = getDaysBetweenDates(date1, date2) // Days between dates (positive if date2 is later)
83
+ const utcMs = getUTCMillisecondsFromDate(date1, 'America/Puerto_Rico') // Convert to UTC milliseconds
84
+ const zonedDate = getTimeZonedDateFromDate(date1, 'America/Puerto_Rico') // Get timezone-adjusted Date
85
+
81
86
  // Date comparison
82
87
  const isEqual = isSameDate(date1, date2)
83
88
  const isBefore = isBeforeDate(date1, date2)
@@ -85,6 +90,10 @@ const isAfter = isAfterDate(date1, date2)
85
90
  const isSameOrBefore = isSameDateOrBefore(date1, date2)
86
91
  const isSameOrAfter = isSameDateOrAfter(date1, date2)
87
92
  const isToday = isDateToday(date1, 'America/Puerto_Rico')
93
+ const isSameMonth = areDatesInSameMonth(date1, date2) // Check if dates are in same month & year
94
+ const isSameYear = areDatesInSameYear(date1, date2) // Check if dates are in same year
95
+ const isCurrentMonth = isDateInCurrentMonth(date1, 'America/Puerto_Rico') // Check if in current month
96
+ const isCurrentYear = isDateInCurrentYear(date1, 'America/Puerto_Rico') // Check if in current year
88
97
 
89
98
  // Date formatting
90
99
  const fullDateStr = getFullDateString(date1, 'en-US')
@@ -104,10 +113,23 @@ const shortDateStr = getShortDateString(date1, 'America/Puerto_Rico', 'en-US', {
104
113
 
105
114
  - **`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
115
 
107
- - **`addMonthsToDate(date, months)`**: Handles month overflow correctly. For example, adding one month to January 31 will result in either February 28 or 29 (depending on leap year), not March 3.
116
+ - **`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.
117
+
118
+ Accepts an optional `options` object with:
119
+ - `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.
108
120
 
109
121
  - **`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.
110
122
 
123
+ - **`getDaysBetweenDates(date1, date2)`**: Returns the number of calendar days between two dates. The result is positive if date2 is after date1, negative if before. This accounts for calendar days rather than full 24-hour periods.
124
+
125
+ - **`getUTCMillisecondsFromDate(date, timeZone)`**: Converts a date to UTC milliseconds since the Unix epoch, accounting for the specified time zone offset.
126
+
127
+ - **`getTimeZonedDateFromDate(date, timeZone)`**: Returns a native Date object adjusted so that its local time matches the local time at the specified time zone.
128
+
129
+ - **`areDatesInSameMonth(date1, date2)` / `areDatesInSameYear(date1, date2)`**: Check if two dates fall within the same month/year. For months, both the month and year must match.
130
+
131
+ - **`isDateInCurrentMonth(date, timeZone)` / `isDateInCurrentYear(date, timeZone)`**: Check if a date falls within the current month or year in the specified time zone.
132
+
111
133
  ### Time Operations (`STime`)
112
134
 
113
135
  `STime` represents a time in the ISO 8601 format (`HH:MM`).
@@ -129,6 +151,8 @@ const totalMinutes = getTimeInMinutes(time1) // Get total minutes since midnight
129
151
 
130
152
  // Time formatting
131
153
  const timeString = get12HourTimeString(time1) // e.g., "2:30 PM"
154
+ const hoursString = get12HoursHoursStringFromTime(time1) // Get hours in 12-hour format (e.g., "2")
155
+ const minutesString = getMinutesStringFromTime(time1) // Get minutes as 2-digit string (e.g., "30")
132
156
 
133
157
  // Time comparison
134
158
  const isEqual = isSameTime(time1, time2)
@@ -147,6 +171,10 @@ const isPM = isTimePM(time1)
147
171
 
148
172
  - **`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.
149
173
 
174
+ - **`get12HoursHoursStringFromTime(time)`**: Returns the hours component in 12-hour format as a string (1-12).
175
+
176
+ - **`getMinutesStringFromTime(time)`**: Returns the minutes component as a zero-padded 2-digit string (00-59).
177
+
150
178
  ### Timestamp Operations (`STimestamp`)
151
179
 
152
180
  `STimestamp` combines a date and time in the ISO 8601 format (`YYYY-MM-DDTHH:MM`).
package/dist/sDate.d.ts CHANGED
@@ -191,11 +191,47 @@ export declare const addDaysToDate: (date: string | SDate, days: number) => SDat
191
191
  * number of months to the given date. Because it just adds to the month
192
192
  * component of the date, this operation is not affected by time zones.
193
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
+ *
194
199
  * @param date The date to add months to. It can be an SDate or a string in the
195
200
  * YYYY-MM-DD format.
196
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
+ * ```
197
231
  */
198
- 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;
199
235
  /**
200
236
  * Returns a new SDate instance with the date resulting from adding the given
201
237
  * number of years to the given date. Because this only adds to the year
package/dist/sDate.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { UTCDateMini } from '@date-fns/utc';
1
2
  import { getTimezoneOffset } from 'date-fns-tz';
2
3
  import { SDate } from './internal/SDate.js';
3
4
  import { DayToWeekday, DaysInWeek, MillisecondsInDay, } from './internal/constants.js';
@@ -289,14 +290,63 @@ export const addDaysToDate = (date, days) => {
289
290
  * number of months to the given date. Because it just adds to the month
290
291
  * component of the date, this operation is not affected by time zones.
291
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
+ *
292
298
  * @param date The date to add months to. It can be an SDate or a string in the
293
299
  * YYYY-MM-DD format.
294
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
+ * ```
295
330
  */
296
- export const addMonthsToDate = (date, months) => {
331
+ export const addMonthsToDate = (date, months, options) => {
297
332
  const sDateValue = sDate(date);
298
333
  const nativeDate = getDateAsUTCDateMini(sDateValue);
299
- 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
+ }
300
350
  return sDate(getISODateFromZonedDate(nativeDate));
301
351
  };
302
352
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scdate",
3
- "version": "1.1.0",
3
+ "version": "2.0.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./dist/index.js"
@@ -24,28 +24,28 @@
24
24
  "test:utc:watch": "TZ=Etc/Universal yarn test:watch",
25
25
  "smoke": "yarn build && yarn lint && yarn test:utc",
26
26
  "-- PRE-COMMIT HOOKS --": "",
27
- "localAfterInstall": "is-ci || husky || true",
27
+ "localAfterInstall": "husky || true",
28
28
  "prepublishOnly": "pinst --disable",
29
29
  "postpublish": "pinst --enable"
30
30
  },
31
31
  "dependencies": {
32
- "@date-fns/utc": "^2.1.0",
32
+ "@date-fns/utc": "^2.1.1",
33
33
  "date-fns": "^4.1.0",
34
34
  "date-fns-tz": "^3.2.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@eslint/js": "^9.25.0",
37
+ "@eslint/js": "^9.33.0",
38
38
  "@tsconfig/strictest": "^2.0.5",
39
- "@types/node": "^22.14.1",
40
- "eslint": "^9.25.0",
39
+ "@types/node": "^24.3.0",
40
+ "eslint": "^9.33.0",
41
41
  "husky": "^9.1.7",
42
- "lint-staged": "^15.5.1",
42
+ "lint-staged": "^16.1.5",
43
43
  "pinst": "^3.0.0",
44
- "prettier": "^3.5.3",
44
+ "prettier": "^3.6.2",
45
45
  "rimraf": "^6.0.1",
46
- "typescript": "^5.8.3",
47
- "typescript-eslint": "^8.30.1",
48
- "vitest": "^3.1.1"
46
+ "typescript": "^5.9.2",
47
+ "typescript-eslint": "^8.40.0",
48
+ "vitest": "^3.2.4"
49
49
  },
50
50
  "prettier": {
51
51
  "tabWidth": 2,
@@ -54,7 +54,7 @@
54
54
  },
55
55
  "repository": {
56
56
  "type": "git",
57
- "url": "https://github.com/ericvera/scdate"
57
+ "url": "git+https://github.com/ericvera/scdate.git"
58
58
  },
59
59
  "keywords": [
60
60
  "date",
@@ -70,5 +70,5 @@
70
70
  "*.{ts,tsx,mjs}": "eslint --cache",
71
71
  "*": "prettier --ignore-unknown --write"
72
72
  },
73
- "packageManager": "yarn@4.9.1"
73
+ "packageManager": "yarn@4.9.2"
74
74
  }