Skip to content

⏰ Flexible period and interval handling library for JavaScript and TypeScript, with support for precise time comparisons and custom intervals.

License

Notifications You must be signed in to change notification settings

hendurhance/chronos-ts

Repository files navigation

Chronos-ts ⏰

Chronos-ts from the name is inspired by the Greek god of time, Chronos. It is a comprehensive TypeScript package for handling time periods, intervals, and date-related operations.

Table of Contents

Installation

npm install chronos-ts

# or
yarn add chronos-ts

# or

pnpm add chronos-ts

Overview

This package provides an easy-to-use API for working with periods and intervals, along with precise handling of date and time logic. Inspired by the spatie/period package in PHP, this package provides support for both TypeScript and JavaScript environments, offering advanced time manipulation. It offers a user-friendly API for working with periods and intervals, providing precise date and time logic handling. Key features include:

  • Flexible period creation and manipulation with customizable precision (minute to year)
  • Interval generation for recurring events (e.g., "every 3 days")
  • Comprehensive period comparisons (overlaps, contains, adjacent)
  • Precise start/end date handling with various time units
  • Advanced operations like symmetric differences and unions between periods
  • Adjacent period detection for seamless time range management
  • Extensive utility functions for common date operations and formatting

Whether you're building scheduling systems, financial applications, or any project requiring sophisticated time calculations, Chronos simplifies complex time-based operations in both TypeScript and JavaScript environments.

Usage

Creating and Manipulating Periods

import { Period, Precision, Interval } from 'chronos-ts';

// Create a period for the year 2023
const year2023 = new Period('2023-01-01', '2023-12-31', Precision.DAY);

// Check if a date is within the period
const isInPeriod = year2023.contains('2023-06-15'); // true

// Create a period for Q2 2023
const q2_2023 = new Period('2023-04-01', '2023-06-30', Precision.DAY);

// Check if periods overlap
const periodsOverlap = year2023.overlapsWith(q2_2023); // true

// Get the overlapping period
const overlap = year2023.overlap(q2_2023);
console.log(overlap?.getStartDate(), overlap?.getEndDate()); // 2023-04-01, 2023-06-30

// Subtract a period
const remainingPeriods = year2023.subtract(q2_2023);
console.log(remainingPeriods.length); // 2
console.log(remainingPeriods[0].getStartDate(), remainingPeriods[0].getEndDate()); // 2023-01-01, 2023-03-31
console.log(remainingPeriods[1].getStartDate(), remainingPeriods[1].getEndDate()); // 2023-07-01, 2023-12-31

// Create a period with an interval
const weeklyPeriod = new Period('2023-01-01', '2023-12-31', Precision.WEEK, Interval.weeks(1));

// Get dates in the interval
const weeklyDates = weeklyPeriod.getDatesInInterval();
console.log(weeklyDates?.length); // 53 (number of weeks in 2023)

// Renew a period
const nextYear = year2023.renew();
console.log(nextYear.getStartDate(), nextYear.getEndDate()); // 2024-01-01, 2024-12-31

// Use fluent API
const customPeriod = new Period('2023-01-01', '2023-12-31')
  .setPrecision(Precision.MONTH)
  .setInterval(Interval.months(3));

console.log(customPeriod.getDatesInInterval()?.length); // 5 (Jan, Apr, Jul, Oct, Jan)

Working with Intervals

import { Interval } from 'chronos-ts';

const twoHours = Interval.hours(2);
console.log(twoHours.getMinutesInterval()); // 120

const threeWeeks = Interval.weeks(3);
console.log(threeWeeks.getMinutesInterval()); // 30240 (3 * 7 * 24 * 60)

const customInterval = new Interval(30, 2, 1, 0, 1); // 30 minutes, 2 hours, 1 day, 0 weeks, 1 month
console.log(customInterval.getMinutesInterval()); // 44310 (30 + 120 + 1440 + 43200)

Using Utility Functions

import { addToDate, subtractFromDate, formatDate, parseDate, getWeekNumber, isLeapYear, Precision } from 'chronos-ts';

const today = new Date();

// Add 3 months to today
const futureDate = addToDate(today, 3, Precision.MONTH);

// Subtract 2 weeks from today
const pastDate = subtractFromDate(today, 2, Precision.WEEK);

// Format a date
const formattedDate = formatDate(today, 'YYYY-MM-DD HH:mm:ss');

// Parse a date string
const parsedDate = parseDate('2023-06-15 14:30:00', 'YYYY-MM-DD HH:mm:ss');

// Get the week number
const weekNumber = getWeekNumber(today);

// Check if it's a leap year
const isLeap = isLeapYear(2024); // true

API Reference

Classes

Period

The Period class represents a time period with a start date, end date, precision, and optional interval.

Constructor
constructor(start: string | Date, end: string | Date, precision: Precision = Precision.DAY, interval: Interval | null = null)
Methods
  • contains(date: string | Date): boolean: Checks if the given date is within the period.
  • overlapsWith(other: Period): boolean: Checks if this period overlaps with another period.
  • isAdjacentTo(other: Period): boolean: Checks if this period is adjacent to another period.
  • getDatesInInterval(): Date[] | null: Returns an array of dates within the period based on the interval.
  • getMinutesInInterval(): number: Returns the number of minutes in the period.
  • getHoursInInterval(): number: Returns the number of hours in the period.
  • getDaysInInterval(): number: Returns the number of days in the period.
  • getWeeksInInterval(): number: Returns the number of weeks in the period.
  • getMonthsInInterval(): number: Returns the number of months in the period.
  • getYearsInInterval(): number: Returns the number of years in the period.
  • length(): number: Returns the length of the period in the specified precision.
  • overlap(other: Period): Period | null: Returns the overlapping period with another period, if any.
  • subtract(other: Period): Period[]: Subtracts another period from this period.
  • gap(other: Period): Period | null: Returns the gap between this period and another period, if any.
  • symmetricDifference(other: Period): Period[]: Returns the symmetric difference between this period and another period.
  • renew(): Period: Creates a new period of the same length immediately following this period.
  • union(other: Period): Period[]: Returns the union of this period with another period.
Fluent API Methods

setStart(start: string | Date): this: Sets the start date of the period. setEnd(end: string | Date): this: Sets the end date of the period. setPrecision(precision: Precision): this: Sets the precision of the period. setInterval(interval: Interval): this: Sets the interval of the period.

Interval

The Interval class represents a time interval with minutes, hours, days, weeks, and months. Static Methods

  • minutes(minutes: number): Interval: Creates an interval with the specified number of minutes.
  • hours(hours: number): Interval: Creates an interval with the specified number of hours.
  • days(days: number): Interval: Creates an interval with the specified number of days.
  • weeks(weeks: number): Interval: Creates an interval with the specified number of weeks.
  • months(months: number): Interval: Creates an interval with the specified number of months.
Methods

getMinutesInterval(): number: Returns the total number of minutes in the interval.

Enums

Precision

The Precision enum represents different levels of time precision.

enum Precision {
    MINUTE = 'minute',
    HOUR = 'hour',
    DAY = 'day',
    WEEK = 'week',
    MONTH = 'month',
    YEAR = 'year',
}

Utility Function

The package includes various utility functions for working with dates:

  • getDatesWithInterval(start: Date, end: Date, interval: Interval): Date[] - Returns an array of dates within the specified interval.
  • getWeeksWithInterval(start: Date, end: Date, interval: Interval): Date[] - Returns an array of weeks within the specified interval.
  • getDaysWithInterval(start: Date, end: Date, interval: Interval): Date[] - Returns an array of days within the specified interval.
  • getHoursWithInterval(start: Date, end: Date, interval: Interval): Date[] - Returns an array of hours within the specified interval.
  • getMinutesWithInterval(start: Date, end: Date, interval: Interval): Date[] - Returns an array of minutes within the specified interval.
  • getMonthsWithInterval(start: Date, end: Date, interval: Interval): Date[] - Returns an array of months within the specified interval.
  • getYearsWithInterval(start: Date, end: Date, interval: Interval): Date[] - Returns an array of years within the specified interval.
  • addToDate(date: Date, amount: number, unit: Precision): Date - Adds the specified amount of time to a date.
  • subtractFromDate(date: Date, amount: number, unit: Precision): Date - Subtracts the specified amount of time from a date.
  • isSameDay(date1: Date, date2: Date): boolean - Checks if two dates are on the same day.
  • getWeekNumber(date: Date): number - Returns the week number of a date.
  • getQuarter(date: Date): number - Returns the quarter of a date.
  • isLeapYear(year: number): boolean - Checks if a year is a leap year.
  • getDaysInMonth(year: number, month: number): number - Returns the number of days in a month.
  • formatDate(date: Date, format: string): string
  • parseDate(dateString: string, format: string): Date - Parses a date string using the specified format.
  • range(start: number, end: number, step: number = 1): number[] - Returns an array of numbers within the specified range.

Real-world Scenarios

1. Event Planning and Management The Period class can be used to represent events, while the Interval class can help manage recurring events.

import { Period, Interval, Precision } from 'chronos-ts';

// Create an event
const conference = new Period('2023-09-15 09:00', '2023-09-17 18:00', Precision.HOUR);

// Check if a specific time is during the conference
const isDuringConference = conference.contains('2023-09-16 14:30'); // true

// Create a recurring weekly meeting
const weeklyMeeting = new Period('2023-01-01 10:00', '2023-12-31 11:00', Precision.HOUR, Interval.weeks(1));

// Get all meeting dates
const meetingDates = weeklyMeeting.getDatesInInterval();

// Check for conflicts with the conference
const conflictingMeetings = meetingDates?.filter(date => conference.contains(date)) || [];
console.log(`There are ${conflictingMeetings.length} conflicting meetings during the conference.`);

2. Financial Reporting Periods Use the Period class to represent financial quarters and calculate year-to-date periods.

import { Period, Precision } from 'chronos-ts';

const q1_2023 = new Period('2023-01-01', '2023-03-31', Precision.DAY);
const q2_2023 = new Period('2023-04-01', '2023-06-30', Precision.DAY);
const q3_2023 = new Period('2023-07-01', '2023-09-30', Precision.DAY);
const q4_2023 = new Period('2023-10-01', '2023-12-31', Precision.DAY);

// Calculate Year-to-Date period
const ytd = (currentQuarter: Period): Period => {
  return new Period('2023-01-01', currentQuarter.endDate, Precision.DAY);
};

const q3YTD = ytd(q3_2023);
console.log(`Q3 YTD period: ${q3YTD.getStartDate().toDateString()} - ${q3YTD.getEndDate().toDateString()}`);

// Calculate quarter-over-quarter growth
const calculateQoQGrowth = (currentQuarter: number, previousQuarter: number): string => {
  const growth = (currentQuarter - previousQuarter) / previousQuarter * 100;
  return `${growth.toFixed(2)}%`;
};

const q2Revenue = 1000000;
const q3Revenue = 1200000;
console.log(`Q3 QoQ Growth: ${calculateQoQGrowth(q3Revenue, q2Revenue)}`);

3. Employee Leave Management Use the Period class to manage employee leave requests and calculate leave balances.

import { Period, Precision } from 'chronos-ts';

// Create a leave request period
const leaveRequest = new Period('2023-09-15', '2023-09-17', Precision.DAY);

// Check if the leave request overlaps with existing leave periods
const existingLeaves = [
  new Period('2023-09-10', '2023-09-14', Precision.DAY),
  new Period('2023-09-18', '2023-09-20', Precision.DAY),
];

const overlappingLeaves = existingLeaves.filter(leave => leaveRequest.overlapsWith(leave));
console.log(`There are ${overlappingLeaves.length} overlapping leave requests.`);

// Calculate remaining leave balance
const totalLeaveDays = 20;
const usedLeaveDays = existingLeaves.reduce((total, leave) => total + leave.length(), 0);
const remainingLeaveDays = totalLeaveDays - usedLeaveDays;
console.log(`Remaining leave days: ${remainingLeaveDays}`);

// Renew leave balance for the next year
const nextYearLeaveBalance = remainingLeaveDays > 0 ? new Period('2024-01-01', '2024-12-31') : null;
console.log(`Next year's leave balance: ${nextYearLeaveBalance ? nextYearLeaveBalance.length() : 0} days`);

4. Project Timeline Management Use the Period class to manage project timelines and track overlapping tasks.

import { Period, Precision } from 'chronos-ts';

const projectTimeline = new Period('2023-01-01', '2023-12-31', Precision.DAY);

const tasks = [
  new Period('2023-01-15', '2023-03-31', Precision.DAY),
  new Period('2023-03-01', '2023-05-15', Precision.DAY),
  new Period('2023-05-01', '2023-08-31', Precision.DAY),
  new Period('2023-08-15', '2023-11-30', Precision.DAY),
];

// Find overlapping tasks
const findOverlappingTasks = (tasks: Period[]): [Period, Period][] => {
  const overlaps: [Period, Period][] = [];
  for (let i = 0; i < tasks.length; i++) {
    for (let j = i + 1; j < tasks.length; j++) {
      if (tasks[i].overlapsWith(tasks[j])) {
        overlaps.push([tasks[i], tasks[j]]);
      }
    }
  }
  return overlaps;
};

const overlappingTasks = findOverlappingTasks(tasks);
console.log(`There are ${overlappingTasks.length} overlapping tasks in the project.`);

// Calculate project progress
const calculateProgress = (currentDate: Date): number => {
  const daysPassed = new Period(projectTimeline.getStartDate(), currentDate, Precision.DAY).getDaysInInterval();
  const totalDays = projectTimeline.getDaysInInterval();
  return (daysPassed / totalDays) * 100;
};

const currentProgress = calculateProgress(new Date('2023-06-15'));
console.log(`Project progress: ${currentProgress.toFixed(2)}%`);

5. Subscription Billing Cycle Management Use the Period class to manage subscription periods and calculate renewal dates.

import { Period, Precision, addToDate } from 'chronos-ts';

class Subscription {
  constructor(public startDate: Date, public plan: 'monthly' | 'annual') {}

  getCurrentPeriod(): Period {
    const endDate = addToDate(this.startDate, 1, this.plan === 'monthly' ? Precision.MONTH : Precision.YEAR);
    return new Period(this.startDate, endDate, Precision.DAY);
  }

  isActive(date: Date = new Date()): boolean {
    return this.getCurrentPeriod().contains(date);
  }

  getRenewalDate(): Date {
    return this.getCurrentPeriod().getEndDate();
  }

  renew(): void {
    this.startDate = this.getRenewalDate();
  }
}

const monthlySubscription = new Subscription(new Date('2023-01-01'), 'monthly');
console.log(`Monthly subscription active: ${monthlySubscription.isActive()}`);
console.log(`Monthly subscription renewal date: ${monthlySubscription.getRenewalDate().toDateString()}`);

const annualSubscription = new Subscription(new Date('2023-01-01'), 'annual');
console.log(`Annual subscription active: ${annualSubscription.isActive()}`);
console.log(`Annual subscription renewal date: ${annualSubscription.getRenewalDate().toDateString()}`);

// Check if a subscription will be active on a future date
const futureDate = new Date('2023-12-15');
console.log(`Monthly subscription active on ${futureDate.toDateString()}: ${monthlySubscription.isActive(futureDate)}`);
console.log(`Annual subscription active on ${futureDate.toDateString()}: ${annualSubscription.isActive(futureDate)}`);

// Renew a subscription
monthlySubscription.renew();
const renewedPeriod = monthlySubscription.getCurrentPeriod();
console.log(`Monthly subscription renewed. New period: ${renewedPeriod.getStartDate().toDateString()} - ${renewedPeriod.getEndDate().toDateString()}`);

6. Employee Shift Management

Use the Period and Interval classes to manage employee shifts and calculate overtime.

import { Period, Interval, Precision, addToDate } from 'chronos-ts';

class Shift extends Period {
  constructor(public employee: string, start: Date, end: Date) {
    super(start, end, Precision.MINUTE);
  }

  getDuration(): number {
    return this.getHoursInInterval();
  }

  isOvertime(): boolean {
    return this.getDuration() > 8;
  }

  getOvertimeHours(): number {
    return Math.max(0, this.getDuration() - 8);
  }
}

// Create a week's worth of shifts for an employee
const createWeekShifts = (employee: string, startDate: Date): Shift[] => {
  const shifts: Shift[] = [];
  for (let i = 0; i < 5; i++) { // Assuming 5-day work week
    const shiftStart = addToDate(startDate, i, Precision.DAY);
    shiftStart.setHours(9, 0, 0, 0); // 9 AM start
    const shiftEnd = new Date(shiftStart);
    shiftEnd.setHours(17, 0, 0, 0); // 5 PM end
    shifts.push(new Shift(employee, shiftStart, shiftEnd));
  }
  return shifts;
};

const employeeShifts = createWeekShifts('John Doe', new Date('2023-06-19'));

// Calculate total hours worked and overtime
const totalHours = employeeShifts.reduce((sum, shift) => sum + shift.getDuration(), 0);
const overtimeHours = employeeShifts.reduce((sum, shift) => sum + shift.getOvertimeHours(), 0);

console.log(`Total hours worked: ${totalHours}`);
console.log(`Overtime hours: ${overtimeHours}`);

// Check for conflicting shifts
const hasConflict = (shifts: Shift[]): boolean => {
  for (let i = 0; i < shifts.length; i++) {
    for (let j = i + 1; j < shifts.length; j++) {
      if (shifts[i].overlapsWith(shifts[j])) {
        return true;
      }
    }
  }
  return false;
};

console.log(`Shifts have conflicts: ${hasConflict(employeeShifts)}`);

7. Travel Itinerary Planning Use the Period class to manage travel itineraries and check for scheduling conflicts.

import { Period, Precision, addToDate } from 'chronos-ts';

class TravelEvent extends Period {
  constructor(public description: string, start: Date, end: Date) {
    super(start, end, Precision.MINUTE);
  }
}

class Itinerary {
  events: TravelEvent[] = [];

  addEvent(event: TravelEvent): void {
    if (this.hasConflict(event)) {
      throw new Error('Event conflicts with existing events');
    }
    this.events.push(event);
  }

  hasConflict(newEvent: TravelEvent): boolean {
    return this.events.some(event => event.overlapsWith(newEvent));
  }

  getDuration(): number {
    if (this.events.length === 0) return 0;
    const start = this.events.reduce((min, e) => e.getStartDate() < min ? e.getStartDate() : min, this.events[0].getStartDate());
    const end = this.events.reduce((max, e) => e.getEndDate() > max ? e.getEndDate() : max, this.events[0].getEndDate());
    return new Period(start, end, Precision.HOUR).getDaysInInterval();
  }
}

// Create a travel itinerary
const itinerary = new Itinerary();

// Add events to the itinerary
try {
  itinerary.addEvent(new TravelEvent('Flight to Paris', new Date('2023-07-01 10:00'), new Date('2023-07-01 12:00')));
  itinerary.addEvent(new TravelEvent('Hotel Check-in', new Date('2023-07-01 14:00'), new Date('2023-07-01 15:00')));
  itinerary.addEvent(new TravelEvent('Eiffel Tower Visit', new Date('2023-07-02 10:00'), new Date('2023-07-02 13:00')));
  itinerary.addEvent(new TravelEvent('Louvre Museum', new Date('2023-07-03 09:00'), new Date('2023-07-03 12:00')));
  itinerary.addEvent(new TravelEvent('Flight to Rome', new Date('2023-07-04 15:00'), new Date('2023-07-04 17:00')));
} catch (error) {
  console.error('Error creating itinerary:', error.message);
}

console.log(`Itinerary duration: ${itinerary.getDuration()} days`);

// Try to add a conflicting event
try {
  itinerary.addEvent(new TravelEvent('Conflicting Event', new Date('2023-07-01 11:00'), new Date('2023-07-01 13:00')));
} catch (error) {
  console.log('Caught conflicting event:', error.message);
}

Contributing 🤝

Contributions, issues and feature requests are welcome. After cloning & setting up project locally, you can just submit a PR to this repo and it will be deployed once it's accepted. There is a list of TODOs in the TODO.md file. The project is still in its early stages, so there are plenty of opportunities to contribute.

License 📝

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgements 🙏