Skip to content

Instantly share code, notes, and snippets.

@psenger
Created January 22, 2025 21:21
Show Gist options
  • Save psenger/1a4132560bacf821f3404bd3f9f3efcd to your computer and use it in GitHub Desktop.
Save psenger/1a4132560bacf821f3404bd3f9f3efcd to your computer and use it in GitHub Desktop.
[Date Builder] #JavaScript #Date

DateBuilder 🗓️ - The Lazy Developer's Date Manipulation Tool

What is this? 🤔

DateBuilder is a super simple, zero-dependency class that makes manipulating JavaScript dates a breeze! No more headaches with date calculations - just chain methods and go!

Features ✨

  • 🔄 Chainable methods - write clean, readable code
  • ➕ Add or subtract time units (days, months, years, hours, minutes, seconds)
  • 0️⃣ Zero dependencies - just pure JavaScript!
  • 🎯 Simple and intuitive API
  • 🔨 Builder pattern - construct your dates step by step

Installation 📦

Just copy and paste! That's right - no npm, no yarn, no pnpm, no dependencies! We know you're lazy (I am too 😉), so I made it super easy.

Usage Example 🚀

const nextWeekNextYear = new DateBuilder()
    .addDays(7)
    .addYears(1)
    .build();

const lastMonthMinusTwoHours = new DateBuilder()
    .subtractMonths(1)
    .subtractHours(2)
    .build();

Why Use This? 💡

  • 🎨 Clean, fluent interface
  • 🧩 Method chaining for complex date calculations
  • 🎯 Immutable output (build() returns a new Date)
  • 🛠️ Perfect for lazy developers who just want things to work

License 📄

This is free and unencumbered software released into the public domain. Copy, modify, and use as you wish!

Remember: Time is an illusion, lunchtime doubly so. 🕰️

/**
* A builder class for manipulating JavaScript Date objects in a chainable way.
* @class
* @example
* const builder = new DateBuilder();
* const nextWeek = builder
* .addDays(7)
* .build();
*
* @example
* const customDate = new DateBuilder('2024-01-01')
* .addMonths(1)
* .subtractDays(5)
* .build();
*/
class DateBuilder {
/**
* Creates a new DateBuilder instance
* @param {Date|string|number} [date=new Date()] - Initial date. Defaults to current date/time
* @throws {TypeError} If the date parameter cannot be converted to a valid Date
*/
constructor(date = new Date()) {
this.date = new Date(date);
if (isNaN(this.date.getTime())) {
throw new TypeError('Invalid date provided to DateBuilder constructor');
}
}
/**
* Adds the specified number of days to the current date
* @param {number} days - Number of days to add (can be negative)
* @returns {DateBuilder} The current DateBuilder instance for chaining
* @example
* const threeDaysFromNow = new DateBuilder()
* .addDays(3)
* .build();
*/
addDays(days) {
this.date.setDate(this.date.getDate() + days);
return this;
}
/**
* Adds the specified number of months to the current date
* @param {number} months - Number of months to add (can be negative)
* @returns {DateBuilder} The current DateBuilder instance for chaining
* @example
* const twoMonthsFromNow = new DateBuilder()
* .addMonths(2)
* .build();
*/
addMonths(months) {
this.date.setMonth(this.date.getMonth() + months);
return this;
}
/**
* Adds the specified number of years to the current date
* @param {number} years - Number of years to add (can be negative)
* @returns {DateBuilder} The current DateBuilder instance for chaining
* @example
* const nextYear = new DateBuilder()
* .addYears(1)
* .build();
*/
addYears(years) {
this.date.setFullYear(this.date.getFullYear() + years);
return this;
}
/**
* Adds the specified number of hours to the current date
* @param {number} hours - Number of hours to add (can be negative)
* @returns {DateBuilder} The current DateBuilder instance for chaining
* @example
* const threeHoursFromNow = new DateBuilder()
* .addHours(3)
* .build();
*/
addHours(hours) {
this.date.setHours(this.date.getHours() + hours);
return this;
}
/**
* Adds the specified number of minutes to the current date
* @param {number} minutes - Number of minutes to add (can be negative)
* @returns {DateBuilder} The current DateBuilder instance for chaining
* @example
* const thirtyMinutesFromNow = new DateBuilder()
* .addMinutes(30)
* .build();
*/
addMinutes(minutes) {
this.date.setMinutes(this.date.getMinutes() + minutes);
return this;
}
/**
* Adds the specified number of seconds to the current date
* @param {number} seconds - Number of seconds to add (can be negative)
* @returns {DateBuilder} The current DateBuilder instance for chaining
* @example
* const tenSecondsFromNow = new DateBuilder()
* .addSeconds(10)
* .build();
*/
addSeconds(seconds) {
this.date.setSeconds(this.date.getSeconds() + seconds);
return this;
}
/**
* Subtracts the specified number of days from the current date
* @param {number} days - Number of days to subtract
* @returns {DateBuilder} The current DateBuilder instance for chaining
* @example
* const threeDaysAgo = new DateBuilder()
* .subtractDays(3)
* .build();
*/
subtractDays(days) {
return this.addDays(-days);
}
/**
* Subtracts the specified number of months from the current date
* @param {number} months - Number of months to subtract
* @returns {DateBuilder} The current DateBuilder instance for chaining
* @example
* const twoMonthsAgo = new DateBuilder()
* .subtractMonths(2)
* .build();
*/
subtractMonths(months) {
return this.addMonths(-months);
}
/**
* Subtracts the specified number of years from the current date
* @param {number} years - Number of years to subtract
* @returns {DateBuilder} The current DateBuilder instance for chaining
* @example
* const lastYear = new DateBuilder()
* .subtractYears(1)
* .build();
*/
subtractYears(years) {
return this.addYears(-years);
}
/**
* Subtracts the specified number of hours from the current date
* @param {number} hours - Number of hours to subtract
* @returns {DateBuilder} The current DateBuilder instance for chaining
* @example
* const threeHoursAgo = new DateBuilder()
* .subtractHours(3)
* .build();
*/
subtractHours(hours) {
return this.addHours(-hours);
}
/**
* Subtracts the specified number of minutes from the current date
* @param {number} minutes - Number of minutes to subtract
* @returns {DateBuilder} The current DateBuilder instance for chaining
* @example
* const thirtyMinutesAgo = new DateBuilder()
* .subtractMinutes(30)
* .build();
*/
subtractMinutes(minutes) {
return this.addMinutes(-minutes);
}
/**
* Subtracts the specified number of seconds from the current date
* @param {number} seconds - Number of seconds to subtract
* @returns {DateBuilder} The current DateBuilder instance for chaining
* @example
* const tenSecondsAgo = new DateBuilder()
* .subtractSeconds(10)
* .build();
*/
subtractSeconds(seconds) {
return this.addSeconds(-seconds);
}
/**
* Creates a new Date instance with the current state
* @returns {Date} A new Date instance
* @example
* const finalDate = new DateBuilder()
* .addDays(1)
* .addHours(2)
* .build();
*/
build() {
return new Date(this.date);
}
}
describe('DateBuilder', () => {
let fixedDate;
beforeEach(() => {
// Fix the date to ensure consistent testing
fixedDate = new Date('2024-01-01T00:00:00.000Z');
jest.useFakeTimers();
jest.setSystemTime(fixedDate);
});
afterEach(() => {
jest.useRealTimers();
});
describe('constructor', () => {
it('should initialize with current date when no arguments provided', () => {
const builder = new DateBuilder();
expect(builder.build().toISOString()).toBe(fixedDate.toISOString());
});
it('should initialize with a provided Date object', () => {
const customDate = new Date('2023-12-25T00:00:00.000Z');
const builder = new DateBuilder(customDate);
expect(builder.build().toISOString()).toBe(customDate.toISOString());
});
it('should initialize with a date string', () => {
const builder = new DateBuilder('2023-12-25');
expect(builder.build().toISOString()).startsWith('2023-12-25');
});
it('should throw TypeError for invalid date input', () => {
expect(() => new DateBuilder('invalid-date')).toThrow(TypeError);
expect(() => new DateBuilder('2024-13-45')).toThrow(TypeError);
});
});
describe('add operations', () => {
let builder;
beforeEach(() => {
builder = new DateBuilder(fixedDate);
});
it('should add days correctly', () => {
const result = builder.addDays(5).build();
expect(result.getDate()).toBe(6); // Jan 1 + 5 days = Jan 6
});
it('should handle month rollover when adding days', () => {
const result = builder.addDays(31).build();
expect(result.getMonth()).toBe(1); // February
expect(result.getDate()).toBe(1);
});
it('should add months correctly', () => {
const result = builder.addMonths(3).build();
expect(result.getMonth()).toBe(3); // April
});
it('should handle year rollover when adding months', () => {
const result = builder.addMonths(12).build();
expect(result.getFullYear()).toBe(2025);
expect(result.getMonth()).toBe(0); // January
});
it('should add years correctly', () => {
const result = builder.addYears(5).build();
expect(result.getFullYear()).toBe(2029);
});
it('should add hours correctly', () => {
const result = builder.addHours(25).build();
expect(result.getDate()).toBe(2); // Next day
expect(result.getHours()).toBe(1);
});
it('should add minutes correctly', () => {
const result = builder.addMinutes(150).build();
expect(result.getHours()).toBe(2);
expect(result.getMinutes()).toBe(30);
});
it('should add seconds correctly', () => {
const result = builder.addSeconds(3665).build(); // 1 hour + 1 minute + 5 seconds
expect(result.getHours()).toBe(1);
expect(result.getMinutes()).toBe(1);
expect(result.getSeconds()).toBe(5);
});
});
describe('subtract operations', () => {
let builder;
beforeEach(() => {
builder = new DateBuilder(fixedDate);
});
it('should subtract days correctly', () => {
const result = builder.subtractDays(5).build();
expect(result.getMonth()).toBe(11); // December
expect(result.getDate()).toBe(27);
});
it('should subtract months correctly', () => {
const result = builder.subtractMonths(2).build();
expect(result.getMonth()).toBe(10); // November
});
it('should subtract years correctly', () => {
const result = builder.subtractYears(3).build();
expect(result.getFullYear()).toBe(2021);
});
it('should subtract hours correctly', () => {
const result = builder.subtractHours(25).build();
expect(result.getDate()).toBe(30); // Previous day
expect(result.getHours()).toBe(23);
});
it('should subtract minutes correctly', () => {
const result = builder.subtractMinutes(150).build();
expect(result.getHours()).toBe(21);
expect(result.getMinutes()).toBe(30);
expect(result.getDate()).toBe(31); // Previous day
});
it('should subtract seconds correctly', () => {
const result = builder.subtractSeconds(3665).build();
expect(result.getHours()).toBe(22);
expect(result.getMinutes()).toBe(58);
expect(result.getSeconds()).toBe(55);
expect(result.getDate()).toBe(31); // Previous day
});
});
describe('method chaining', () => {
it('should support multiple add operations', () => {
const result = new DateBuilder(fixedDate)
.addDays(1)
.addMonths(1)
.addYears(1)
.addHours(2)
.addMinutes(30)
.addSeconds(15)
.build();
expect(result.getFullYear()).toBe(2025);
expect(result.getMonth()).toBe(1); // February
expect(result.getDate()).toBe(2);
expect(result.getHours()).toBe(2);
expect(result.getMinutes()).toBe(30);
expect(result.getSeconds()).toBe(15);
});
it('should support multiple subtract operations', () => {
const result = new DateBuilder(fixedDate)
.subtractDays(1)
.subtractMonths(1)
.subtractYears(1)
.subtractHours(2)
.subtractMinutes(30)
.subtractSeconds(15)
.build();
expect(result.getFullYear()).toBe(2022);
expect(result.getMonth()).toBe(11); // December
expect(result.getDate()).toBe(30);
expect(result.getHours()).toBe(21);
expect(result.getMinutes()).toBe(29);
expect(result.getSeconds()).toBe(45);
});
it('should support mixing add and subtract operations', () => {
const result = new DateBuilder(fixedDate)
.addYears(1)
.subtractMonths(6)
.addDays(15)
.subtractHours(12)
.build();
expect(result.getFullYear()).toBe(2024);
expect(result.getMonth()).toBe(6); // July
expect(result.getDate()).toBe(16);
expect(result.getHours()).toBe(12);
});
});
describe('build method', () => {
it('should return a new Date instance', () => {
const builder = new DateBuilder(fixedDate);
const result = builder.build();
expect(result).toBeInstanceOf(Date);
expect(result).not.toBe(builder.date); // Should be a new instance
});
it('should not modify original date when building multiple times', () => {
const builder = new DateBuilder(fixedDate);
const result1 = builder.build();
const result2 = builder.build();
expect(result1.getTime()).toBe(result2.getTime());
expect(result1).not.toBe(result2); // Should be different instances
});
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment