src/period/helpers.js

Summary

Maintainability
A
1 hr
Test Coverage
import { isInteger } from '../lib/check'

export function formatAsISODate(date) {
    if (!(date instanceof Date)) {
        throw new Error('formatAsISODate(date) only accepts Date objects')
    }

    const y = date.getFullYear()
    const m = `0${date.getMonth() + 1}`.substr(-2)
    const d = `0${date.getDate()}`.substr(-2)
    return `${y}-${m}-${d}`
}

export function filterFuturePeriods(periods) {
    const array = []
    const now = new Date()

    for (let i = 0; i < periods.length; i++) {
        if (new Date(periods[i].startDate) <= now) {
            array.push(periods[i])
        }
    }

    return array
}

export function getYYYYMM(date) {
    const y = date.getFullYear()
    let m = `${date.getMonth() + 1}`
    m = `0${m}`.substr(-2)
    return y + m
}

export function getBiMonthlyId(date) {
    const y = date.getFullYear()
    const m = `0${Math.floor(date.getMonth() / 2) + 1}`.substr(-2)
    return `${y + m}B`
}

export function validateIfValueIsInteger(year) {
    if (!isInteger(year)) {
        throw new Error(
            'Generator should be called with an integer to identify the year.' +
                ' Perhaps you passed a Date object?'
        )
    }

    if (year < 0) {
        throw new Error(
            'Generator does not support generating year before the year 0.'
        )
    }
}

export function getCurrentYear() {
    return new Date().getFullYear()
}

export function is53WeekISOYear(year) {
    const p = (y) =>
        y + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400)

    return p(year) % 7 === 4 || p(year - 1) % 7 === 3
}

export function addDays(days, date) {
    const result = new Date(date)

    result.setDate(result.getDate() + days)

    return result
}

export function addMonths(months, date) {
    const result = new Date(date)

    result.setMonth(date.getMonth() + months)

    return result
}

export function subtractDays(days, date) {
    return addDays(-days, date)
}

export function getFirstDayInFirstISOWeekForYear(year) {
    // The first ISO week of the year always contains 4th January. We can use this as a pointer to start the first week.
    let startDate = new Date(year, 0, 4)

    // January 4th might not be at the start of the week. Therefore we rewind to the start of the week.
    if (startDate.getDay() === 0) {
        startDate = subtractDays(6, startDate)
        // If January 4th is on a Sunday we'll revert back 6 days
    } else {
        // We'll revert back the current day number of days - 1 (Due to the days being 0 indexed with 0 being Sunday)
        const daysAfterMonday = startDate.getDay() - 1
        startDate = subtractDays(daysAfterMonday, startDate)
    }

    return startDate
}

export function getLastDayOfTheWeekForFirstDayOfTheWeek(startDate) {
    const endDate = new Date(startDate)

    endDate.setDate(endDate.getDate() + 6)

    return endDate
}

export function getMonthNamesForLocale(locale) {
    const monthNames = []

    for (let i = 0; i < 12; i += 1) {
        const monthName = new Date(2000, i, 1).toLocaleDateString(locale, {
            month: 'long',
        })
        monthNames.push(monthName)
    }

    return monthNames
}

export function getLastDateOfMonth(year, month) {
    return new Date(new Date(year, month + 1).setDate(0))
}

export function getFirstDateOfQuarter(year, quarter) {
    const startMonth = (quarter - 1) * 3
    return new Date(year, startMonth)
}

export function getLastDateOfQuarter(year, quarter) {
    return new Date(getFirstDateOfQuarter(year, quarter + 1).setDate(0))
}

const ordTable = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]
const ordTableLeap = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335]

export function getFirstDateOfWeek(year, week) {
    const isLeapYear =
        new Date(new Date(year, 2, 1).setDate(0)).getDate() === 29
    const ordDiff = isLeapYear ? ordTableLeap : ordTable

    const correction = (new Date(year, 0, 4).getDay() || 7) + 3
    const ordDate = week * 7 + (1 - correction)
    if (ordDate < 0) {
        return new Date(year, 0, ordDate)
    }

    let month = 11
    while (ordDate < ordDiff[month]) {
        month-- // eslint-disable-line no-plusplus
    }

    return new Date(year, month, ordDate - ordDiff[month])
}

/**
 * This function takes some general instructions for a period and returns a precise period containing
 * a start date and end date. It corrects for week 1 starting in a previous year and for week 53 in a
 * 52 week year
 * It is used by the BiWeekly periodType and all flavors of Weekly PeriodTypes,
 * such as Weekly, weeklyWednessday, etc.
 * @param {Object} obj - An object
 * @param {Number} obj.year - Year
 * @param {Number} obj.week - Week number between 1-53
 * @param {String} [obj.locale=en] - The current locale
 * @param {Number} [obj.weekTypeDiff=0] - The difference between the starting day of the week
 * in a given periodType and the first day of the week. This option is being used for different
 * flavors of weekly period types, such as WeeklyWednesday, WeeklyThursday, etc.
 * @param {Number} [obj.periodLength=6] - The amount of days until the end date: 6 for weekly
 * and 13 for Bikweekly
 * @returns {Object} - An object containing various properties that can be used to contruct
 * the final parsed period
 * @example for default scenario
 * computeWeekBasedPeriod({ year: 2019, week: 12})
 * // returns:
 * // {
 * //     week: 12,
 * //     year: 2019,
 * //     startMonthName: 'June',
 * //     startDayNumber: 3,
 * //     endDayNumber: 9,
 * //     startDate: '2019-06-03',
 * //     endDate: '2019-06-09',
 * // }
 * @example for week 53 in 52 week year edge case
 * computeWeekBasedPeriod({ year: 2016, week: 53})
 * // returns:
 * // {
 * //     week: 1,
 * //     year: 2017,
 * //     startMonthName: 'January',
 * //     startDayNumber: 2,
 * //     endDayNumber: 8,
 * //     startDate: '2017-01-02',
 * //     endDate: '2017-01-08',
 * // }
 */
export const computeWeekBasedPeriod = ({
    year,
    week,
    locale = 'en',
    weekTypeDiff = 0,
    periodLength = 6,
}) => {
    const startDate = addDays(weekTypeDiff, getFirstDateOfWeek(year, week))
    const monthNames = getMonthNamesForLocale(locale)
    const startMonth = startDate.getMonth()
    const startYear = startDate.getFullYear()
    const startMonthName = monthNames[startMonth]
    const startDayNum = startDate.getDate()

    if (week === 53 && startYear !== year) {
        week = 1
        year = startYear
    }

    const endDate = addDays(periodLength, startDate)
    const endMonth = endDate.getMonth()
    const endDayNum = endDate.getDate()
    const endMonthName = monthNames[endMonth]

    return {
        week,
        year,
        startMonthName,
        startDayNum,
        endMonthName,
        endDayNum,
        startDate: formatAsISODate(startDate),
        endDate: formatAsISODate(endDate),
    }
}