All files / src/utils calendar.ts

100% Statements 148/148
90.9% Branches 30/33
100% Functions 10/10
100% Lines 148/148

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 1491x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 41x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 13x 13x 13x 13x 13x 13x 45x 45x 45x 45x 45x 13x 13x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 87x 87x 87x 87x 87x 32x 81x 4x 4x 51x 51x 8x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 20x 20x 20x 20x 20x 20x 20x 20x 20x 20x 20x 20x 20x 1080x 7560x 7560x 140x 7474x 7560x 122x 122x 7438x 7410x 7438x 133x 7438x 7305x 7305x 1080x 20x 20x 20x 20x 4x 1x 1x 1x 1x 1x 1x 1x 1x 10x 10x 10x 10x 10x 10x 16x 16x 10x 10x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 39x 10x 10x 10x  
import {
  getISODay,
  getISOWeek,
  getISOWeeksInYear,
  getYear,
  parseISO,
} from "date-fns";
 
import type {
  CalendarWeekData,
  CalendarYearData,
  CalendarYearsData,
  ValuesPerDay,
} from "~/types";
 
/** Creates a UTC date. */
export const createUtcDate = (year: number, month: number, day: number) =>
  new Date(Date.UTC(year, month - 1, day, 0, 0, 0, 0));
 
/**
 * Extracts the minimum and maximum years from the given `ValuesPerDay`
 * object.
 *
 * @param valuesPerDay - The `ValuesPerDay` object to extract the year
 * range from.
 * @returns An object containing the `minimumYear` and `maximumYear`.
 */
export const extractYearRange = (
  valuesPerDay: ValuesPerDay
): { minimumYear: number; maximumYear: number } => {
  let minimumYear = Infinity;
  let maximumYear = -Infinity;
 
  for (const dateString of valuesPerDay.keys()) {
    const year = getYear(parseISO(dateString));
 
    minimumYear = Math.min(minimumYear, year);
    maximumYear = Math.max(maximumYear, year);
  }
 
  return { minimumYear, maximumYear };
};
 
/**
 * Given a date, get its adjusted ISO week number based on the project
 * requirements:
 *
 * - If a date's month is January and its week number is greater than 50, it
 *   will be changed to week 0.
 * - If a date's month is December and its week number is less than 10, it
 *   will be changed to the number of ISO 8601 weeks in that year + 1.
 *
 * @param {Date} date - The date to get the adjusted ISO week number for.
 * @returns {number} - The adjusted ISO week number.
 */
export const getAdjustedISOWeek = (date: Date): number => {
  const month = date.getMonth();
  const isoWeek = getISOWeek(date);
  const isoWeeksInYear = getISOWeeksInYear(date);
 
  if (month === 0 && isoWeek > 50) {
    return 0;
  } else if (month === 11 && isoWeek < 10) {
    return isoWeeksInYear + 1;
  }
 
  return isoWeek;
};
 
/**
 * Initializes an empty `CalendarYearData`.
 *
 * If there are no values for that specific day, it will be `0`. If that day is
 * not part of the calendar year, it will be `-1`.
 *
 * @param {number} year - The year to initialize the data for.
 * @returns {CalendarYearData} a `CalendarYearData` with default values.
 */
export const initializeEmptyCalendarYearData = (
  year: number
): CalendarYearData => {
  const firstDateOfYear = createUtcDate(year, 1, 1);
  const lastDateOfYear = createUtcDate(year, 12, 31);
  const firstWeekOfYearAdjusted = getAdjustedISOWeek(firstDateOfYear);
  const lastWeekOfYearAdjusted = getAdjustedISOWeek(lastDateOfYear);
  const firstDayOfYear = getISODay(firstDateOfYear) - 1;
  const lastDayOfYear = getISODay(lastDateOfYear) - 1;
 
  const yearData: CalendarYearData = [
    ...(Array.from(
      { length: 54 },
      (_, weekIndex) =>
        Array.from({ length: 7 }, (_, dayIndex) => {
          if (
            (weekIndex === firstWeekOfYearAdjusted &&
              dayIndex < firstDayOfYear) ||
            (weekIndex === lastWeekOfYearAdjusted && dayIndex > lastDayOfYear)
          ) {
            return -1;
          } else if (
            weekIndex < firstWeekOfYearAdjusted ||
            weekIndex > lastWeekOfYearAdjusted
          ) {
            return -1;
          } else {
            return 0;
          }
        }) as CalendarWeekData
    ) as CalendarYearData),
  ];
 
  return yearData;
};
 
/**
 * Converts a `ValuesPerDay` object to a `CalendarYearsData` object.
 *
 * @param valuesPerDay - The `ValuesPerDay` object to convert.
 * @returns The converted `CalendarYearsData` object.
 */
export const convertToCalendarYearData = (
  valuesPerDay: ValuesPerDay
): CalendarYearsData => {
  const { minimumYear, maximumYear } = extractYearRange(valuesPerDay);
  const yearsData: CalendarYearsData = [];
 
  for (let year = minimumYear; year <= maximumYear; year++) {
    yearsData.push(initializeEmptyCalendarYearData(year));
  }
 
  for (const [dateString, value] of valuesPerDay) {
    const date = parseISO(dateString);
    const year = getYear(date);
 
    const yearIndex = year - minimumYear;
    const weekIndex = getAdjustedISOWeek(date);
    const dayIndex = getISODay(date) - 1;
 
    const yearData = yearsData[yearIndex];
    if (!yearData) continue;
 
    const weekData = yearData[weekIndex];
    if (!weekData || weekData[dayIndex] === -1) continue;
    weekData[dayIndex] = (weekData[dayIndex] ?? 0) + value;
  }
 
  return yearsData;
};