@stacksjs/datetime 0.75.408 → 0.75.417

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.
@@ -14,6 +14,8 @@ export declare function localDateRange(from: string, to: string, ...zones: unkno
14
14
  export declare function netHoursBetween(start: Date | string | null, end: Date | string | null, breakMinutes?: number): number;
15
15
  /** A location's clock interval as real instants; an earlier end time belongs to the next day. */
16
16
  export declare function localTimeSpan(date: string, start: string, end: string, ...zones: unknown[]): { start: Date, end: Date, timeZone: string };
17
+ /** Half-open instant intervals overlap only when they share positive elapsed time. */
18
+ export declare function timeSpansOverlap(first: { start: Date, end: Date }, second: { start: Date, end: Date }): boolean;
17
19
  export declare interface WeeklySchedule { weekdays: number[], start_time: string }
18
20
  export declare interface TimeWindow { start_time: string, end_time: string }
19
21
  export declare class ScheduleInputError extends RangeError {
package/dist/schedule.js CHANGED
@@ -1 +1 @@
1
- import{localDateTime}from"./local";import{zonedTimeToUtc}from"./zone";export class ScheduleInputError extends RangeError{status=422}const DAY=86400000;export function calendarDay(value){if(typeof value!=="string"||!/^\d{4}-\d{2}-\d{2}$/.test(value))throw new ScheduleInputError("Use yyyy-mm-dd dates");const date=new Date(`${value}T00:00:00Z`);if(!Number.isFinite(date.getTime())||date.toISOString().slice(0,10)!==value)throw new ScheduleInputError("Invalid calendar date");return date.getTime()/DAY}export function clockMinutes(value){if(typeof value!=="string"||!/^\d{2}:\d{2}$/.test(value))throw new ScheduleInputError("Use HH:MM times");const[hour,minute]=value.split(":").map(Number);if(hour>23||minute>59)throw new ScheduleInputError("Invalid clock time");return hour*60+minute}export function addClockMinutes(start,duration){if(!Number.isSafeInteger(duration)||duration<=0)throw new ScheduleInputError("Duration must be positive whole minutes");const end=clockMinutes(start)+duration;if(end>=1440)throw new ScheduleInputError("Session must end before midnight; shorten it or move its start time");return`${String(Math.floor(end/60)).padStart(2,"0")}:${String(end%60).padStart(2,"0")}`}export function parseWeeklySchedule(value){try{const data=typeof value==="string"?JSON.parse(value):value;if(!data||typeof data!=="object"||Array.isArray(data)||!Array.isArray(data.weekdays)||!data.weekdays.length)return null;const weekdays=data.weekdays.map((day)=>{if(typeof day!=="number"&&(typeof day!=="string"||!/^[0-6]$/.test(day.trim())))throw new ScheduleInputError("Invalid weekday");const number=Number(day);if(!Number.isInteger(number)||number<0||number>6)throw new ScheduleInputError("Invalid weekday");return number});clockMinutes(data.start_time);return{weekdays:[...new Set(weekdays)],start_time:data.start_time}}catch(error){if(error instanceof SyntaxError||error instanceof ScheduleInputError)return null;throw error}}export function expandWeeklySchedule(spec,from,to,duration,maxDays=800){const first=calendarDay(from),last=calendarDay(to);if(!Number.isSafeInteger(maxDays)||maxDays<1)throw new ScheduleInputError("Invalid date-range limit");if(last<first)throw new ScheduleInputError("from must be on or before to");if(last-first+1>maxDays)throw new ScheduleInputError(`Generate at most ${maxDays} days at a time`);const schedule=parseWeeklySchedule(spec);if(!schedule)throw new ScheduleInputError("Invalid recurrence");const end=addClockMinutes(schedule.start_time,duration),days=new Set(schedule.weekdays),slots=[];for(let day=first;day<=last;day++){const date=new Date(day*DAY);if(days.has(date.getUTCDay()))slots.push({session_date:date.toISOString().slice(0,10),start_time:schedule.start_time,end_time:end})}return slots}export function timeWindowsCover(windows,start,end){const first=clockMinutes(start),last=clockMinutes(end);if(first>=last)return!1;const spans=windows.map((window)=>({start:clockMinutes(window.start_time),end:clockMinutes(window.end_time)})).sort((a,b)=>a.start-b.start);let covered=first;for(const span of spans){if(span.end<=span.start)throw new ScheduleInputError("Availability must end after it starts");if(span.start>covered)break;covered=Math.max(covered,span.end);if(covered>=last)return!0}return!1}export function localDateRange(from,to,...zones){const first=calendarDay(from),last=calendarDay(to);if(last<first)throw new ScheduleInputError("from must be on or before to");if(Number(from.slice(0,4))<101||to>="9999-12-31")throw new ScheduleInputError("Local ranges support dates from 0101-01-01 through 9999-12-30");const timeZone=localDateTime(new Date(first*DAY),...zones).timeZone,next=new Date((last+1)*DAY).toISOString().slice(0,10);return{start:zonedTimeToUtc(`${from}T00:00:00`,timeZone),end:zonedTimeToUtc(`${next}T00:00:00`,timeZone),timeZone}}export function netHoursBetween(start,end,breakMinutes=0){if(start==null||end==null||!Number.isFinite(breakMinutes))return 0;const first=start instanceof Date?start.getTime():Date.parse(start),last=end instanceof Date?end.getTime():Date.parse(end);if(!Number.isFinite(first)||!Number.isFinite(last)||last<=first)return 0;return Math.max(0,(last-first)/3600000-Math.max(0,breakMinutes)/60)}export function localTimeSpan(date,start,end,...zones){const day=calendarDay(date),first=clockMinutes(start),last=clockMinutes(end);if(first===last)throw new ScheduleInputError("An interval must have distinct start and end times");if(Number(date.slice(0,4))<101||date>="9999-12-31")throw new ScheduleInputError("Unsupported local interval date");const timeZone=localDateTime(new Date(day*DAY),...zones).timeZone,endDate=last<first?new Date((day+1)*DAY).toISOString().slice(0,10):date,interval={start:zonedTimeToUtc(`${date}T${start}:00`,timeZone),end:zonedTimeToUtc(`${endDate}T${end}:00`,timeZone),timeZone};if(interval.end<=interval.start)throw new ScheduleInputError("The local interval has no positive duration in this timezone");return interval}
1
+ import{localDateTime}from"./local";import{zonedTimeToUtc}from"./zone";export class ScheduleInputError extends RangeError{status=422}const DAY=86400000;export function calendarDay(value){if(typeof value!=="string"||!/^\d{4}-\d{2}-\d{2}$/.test(value))throw new ScheduleInputError("Use yyyy-mm-dd dates");const date=new Date(`${value}T00:00:00Z`);if(!Number.isFinite(date.getTime())||date.toISOString().slice(0,10)!==value)throw new ScheduleInputError("Invalid calendar date");return date.getTime()/DAY}export function clockMinutes(value){if(typeof value!=="string"||!/^\d{2}:\d{2}$/.test(value))throw new ScheduleInputError("Use HH:MM times");const[hour,minute]=value.split(":").map(Number);if(hour>23||minute>59)throw new ScheduleInputError("Invalid clock time");return hour*60+minute}export function addClockMinutes(start,duration){if(!Number.isSafeInteger(duration)||duration<=0)throw new ScheduleInputError("Duration must be positive whole minutes");const end=clockMinutes(start)+duration;if(end>=1440)throw new ScheduleInputError("Session must end before midnight; shorten it or move its start time");return`${String(Math.floor(end/60)).padStart(2,"0")}:${String(end%60).padStart(2,"0")}`}export function parseWeeklySchedule(value){try{const data=typeof value==="string"?JSON.parse(value):value;if(!data||typeof data!=="object"||Array.isArray(data)||!Array.isArray(data.weekdays)||!data.weekdays.length)return null;const weekdays=data.weekdays.map((day)=>{if(typeof day!=="number"&&(typeof day!=="string"||!/^[0-6]$/.test(day.trim())))throw new ScheduleInputError("Invalid weekday");const number=Number(day);if(!Number.isInteger(number)||number<0||number>6)throw new ScheduleInputError("Invalid weekday");return number});clockMinutes(data.start_time);return{weekdays:[...new Set(weekdays)],start_time:data.start_time}}catch(error){if(error instanceof SyntaxError||error instanceof ScheduleInputError)return null;throw error}}export function expandWeeklySchedule(spec,from,to,duration,maxDays=800){const first=calendarDay(from),last=calendarDay(to);if(!Number.isSafeInteger(maxDays)||maxDays<1)throw new ScheduleInputError("Invalid date-range limit");if(last<first)throw new ScheduleInputError("from must be on or before to");if(last-first+1>maxDays)throw new ScheduleInputError(`Generate at most ${maxDays} days at a time`);const schedule=parseWeeklySchedule(spec);if(!schedule)throw new ScheduleInputError("Invalid recurrence");const end=addClockMinutes(schedule.start_time,duration),days=new Set(schedule.weekdays),slots=[];for(let day=first;day<=last;day++){const date=new Date(day*DAY);if(days.has(date.getUTCDay()))slots.push({session_date:date.toISOString().slice(0,10),start_time:schedule.start_time,end_time:end})}return slots}export function timeWindowsCover(windows,start,end){const first=clockMinutes(start),last=clockMinutes(end);if(first>=last)return!1;const spans=windows.map((window)=>({start:clockMinutes(window.start_time),end:clockMinutes(window.end_time)})).sort((a,b)=>a.start-b.start);let covered=first;for(const span of spans){if(span.end<=span.start)throw new ScheduleInputError("Availability must end after it starts");if(span.start>covered)break;covered=Math.max(covered,span.end);if(covered>=last)return!0}return!1}export function localDateRange(from,to,...zones){const first=calendarDay(from),last=calendarDay(to);if(last<first)throw new ScheduleInputError("from must be on or before to");if(Number(from.slice(0,4))<101||to>="9999-12-31")throw new ScheduleInputError("Local ranges support dates from 0101-01-01 through 9999-12-30");const timeZone=localDateTime(new Date(first*DAY),...zones).timeZone,next=new Date((last+1)*DAY).toISOString().slice(0,10);return{start:zonedTimeToUtc(`${from}T00:00:00`,timeZone),end:zonedTimeToUtc(`${next}T00:00:00`,timeZone),timeZone}}export function netHoursBetween(start,end,breakMinutes=0){if(start==null||end==null||!Number.isFinite(breakMinutes))return 0;const first=start instanceof Date?start.getTime():Date.parse(start),last=end instanceof Date?end.getTime():Date.parse(end);if(!Number.isFinite(first)||!Number.isFinite(last)||last<=first)return 0;return Math.max(0,(last-first)/3600000-Math.max(0,breakMinutes)/60)}export function localTimeSpan(date,start,end,...zones){const day=calendarDay(date),first=clockMinutes(start),last=clockMinutes(end);if(first===last)throw new ScheduleInputError("An interval must have distinct start and end times");if(Number(date.slice(0,4))<101||date>="9999-12-31")throw new ScheduleInputError("Unsupported local interval date");const timeZone=localDateTime(new Date(day*DAY),...zones).timeZone,endDate=last<first?new Date((day+1)*DAY).toISOString().slice(0,10):date,interval={start:zonedTimeToUtc(`${date}T${start}:00`,timeZone),end:zonedTimeToUtc(`${endDate}T${end}:00`,timeZone),timeZone};if(interval.end<=interval.start)throw new ScheduleInputError("The local interval has no positive duration in this timezone");return interval}export function timeSpansOverlap(first,second){for(const span of[first,second])if(!Number.isFinite(span.start.getTime())||!Number.isFinite(span.end.getTime())||span.end<=span.start)throw new ScheduleInputError("An interval must contain a positive elapsed duration");return first.start<second.end&&first.end>second.start}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stacksjs/datetime",
3
3
  "type": "module",
4
- "version": "0.75.408",
4
+ "version": "0.75.417",
5
5
  "description": "The Stacks datetime helpers.",
6
6
  "author": "Chris Breuer",
7
7
  "contributors": [
@@ -59,7 +59,7 @@
59
59
  },
60
60
  "devDependencies": {
61
61
  "better-dx": "^0.2.24",
62
- "@stacksjs/utils": "0.75.408"
62
+ "@stacksjs/utils": "0.75.417"
63
63
  },
64
64
  "sideEffects": false
65
65
  }