teamdigitale/italia-app

View on GitHub
ts/utils/dates.ts

Summary

Maintainability
A
0 mins
Test Coverage
import { format as dateFnsFormat } from "date-fns";
import dfns_de from "date-fns/locale/de";
import dfns_en from "date-fns/locale/en";
import dfns_it from "date-fns/locale/it";
import * as E from "fp-ts/lib/Either";
import { pipe } from "fp-ts/lib/function";
import * as O from "fp-ts/lib/Option";
import * as t from "io-ts";
import { Errors } from "io-ts";
import { Locales } from "../../locales/locales";
import I18n from "../i18n";
import { CreditCardExpirationMonth, CreditCardExpirationYear } from "./input";
import { getLocalePrimary, localeDateFormat } from "./locale";
import { NumberFromString } from "./number";

type DateFnsLocale = typeof import("date-fns/locale/it");

type DFNSLocales = Record<Locales, DateFnsLocale>;

const locales: DFNSLocales = { it: dfns_it, en: dfns_en, de: dfns_de };

export const pad = (n: number) => n.toString().padStart(2, "0");

/*
 * This function is specific for the fiscal code birthday rendering.
 * The birthday is an ISO8601 format for midnight.
 * It returns the date in short format.
 *
 * i.e. 1977-05-22T00:00:00.000Z -> 22/05/1977
 */
export const formatFiscalCodeBirthdayAsShortFormat = (
  date: Date | undefined
): string =>
  pipe(
    date,
    O.fromNullable,
    O.chain(O.fromPredicate(d => !isNaN(d.getTime()))),
    O.fold(
      () => I18n.t("global.date.invalid"),
      d => {
        const year = d.getUTCFullYear();
        const month = pad(d.getUTCMonth() + 1);
        const day = pad(d.getUTCDate());
        return `${day}/${month}/${year}`;
      }
    )
  );

// return a string representing the date dd/MM/YYYY (ex: 1 Jan 1970 -> 01/01/1970)
export const formatDateAsShortFormat = (date: Date): string =>
  isNaN(date.getTime())
    ? I18n.t("global.date.invalid")
    : I18n.strftime(date, I18n.t("global.dateFormats.shortFormat"));

export function formatDateAsMonth(date: Date): ReturnType<typeof format> {
  return format(date, "MMM");
}

export function formatDateAsDay(date: Date): ReturnType<typeof format> {
  return format(date, "DD");
}

export function formatDateAsReminder(
  date: Date
): ReturnType<typeof dateFnsFormat> {
  return dateFnsFormat(date, "YYYY-MM-DDTHH:mm:ss.SSS[Z]");
}

/**
 *
 * It provides the format of the date depending on the system locale (DD/MM or MM/DD as default)
 * @param date
 * @param includeYear: true if the year should be included (DD/MM/YY or MM/DD/YY)
 * @param extendedYear
 */
export function formatDateAsLocal(
  date: Date,
  includeYear: boolean = false,
  extendedYear: boolean = false
): ReturnType<typeof dateFnsFormat> {
  const dateFormat = I18n.t("global.dateFormats.dayMonth");
  return extendedYear
    ? format(date, dateFormat) + "/" + format(date, "YYYY")
    : includeYear
    ? format(date, dateFormat) + "/" + format(date, "YY")
    : format(date, dateFormat);
}

export function format(
  date: string | number | Date,
  dateFormat?: string
): ReturnType<typeof dateFnsFormat> {
  const localePrimary = getLocalePrimary(I18n.currentLocale());
  return dateFnsFormat(
    date,
    dateFormat,
    pipe(
      localePrimary,
      O.chainNullableK(lp => locales[lp as Locales]), // becomes empty if locales[lp] is undefined
      O.map(locale => ({ locale })),
      O.toUndefined // if some returns the value, if empty return undefined)
    )
  );
}

/**
 * Try to parse month and validate as month
 * @param month
 */
export const decodeCreditCardMonth = (
  month: string | number | undefined
): E.Either<Error | Errors, number> => {
  // convert month to string (if it is a number) and
  // ensure it is left padded: 2 -> 02
  const monthStr = (month ?? "").toString().trim().padStart(2, "0");
  // check that month matches the pattern (01-12)
  if (!CreditCardExpirationMonth.is(monthStr)) {
    return E.left(
      new Error("month doesn't follow CreditCardExpirationMonth pattern")
    );
  }
  return NumberFromString.decode(monthStr);
};

/**
 * Try to parse year and validate as year
 * @param year
 */
export const decodeCreditCardYear = (
  year: string | number | undefined
): E.Either<Error | Errors, number> => {
  const yearStr = (year ?? "").toString().trim();
  // if the year is 2 digits, convert it to 4 digits: 21 -> 2021
  if (yearStr.length === 2) {
    // check that year is included matches the pattern (00-99)
    if (!CreditCardExpirationYear.is(yearStr)) {
      return E.left(
        new Error("year doesn't follow CreditCardExpirationYear pattern")
      );
    }
    const now = new Date();
    return NumberFromString.decode(
      now.getFullYear().toString().substring(0, 2) + yearStr.toString()
    );
  }
  return NumberFromString.decode(yearStr);
};

/**
 * ⚠️ Beware, the Date that this method returns is partially correct since is created only from year and month.
 * Eg: month: "03" year: "2022" will return -> 2022-02-28T23:00:00.000Z
 * The date thus returned is therefore ambiguous since it may not correspond to the intended semantics
 * (for example the date returned is not applicable to credit cards that includes the last day of the month)
 * Using the date thus generated to make comparisons could lead to unexpected behaviour
 * @param month
 * @param year
 * @deprecated
 */
export const dateFromMonthAndYear = (
  month: string | number | undefined,
  year: string | number | undefined
): O.Option<Date> => {
  const maybeMonth = decodeCreditCardMonth(month);
  const maybeYear = decodeCreditCardYear(year);
  if (E.isLeft(maybeMonth) || E.isLeft(maybeYear)) {
    return O.none;
  }
  return O.some(new Date(maybeYear.right, maybeMonth.right - 1));
};

/**
 * if expireMonth and expireYear are defined, and they represent a valid date then
 * return some, with 'true' if the given date is expired compared with now.
 * return none if the input is not valid
 * {@expireYear could be 2 or 4 digits}
 * @param expireMonth
 * @param expireYear
 */
export const isExpired = (
  expireMonth: string | number | undefined,
  expireYear: string | number | undefined
): E.Either<Error, boolean> => {
  const maybeMonth = decodeCreditCardMonth(expireMonth);
  const maybeYear = decodeCreditCardYear(expireYear);
  return pipe(
    maybeYear,
    E.chain(year =>
      pipe(
        maybeMonth,
        E.map(month => {
          const now = new Date();
          const nowYearMonth = new Date(now.getFullYear(), now.getMonth() + 1);
          const cardExpirationDate = new Date(year, month);
          return nowYearMonth > cardExpirationDate;
        })
      )
    ),
    E.mapLeft(_ => new Error("invalid input"))
  );
};

/**
 * This function returns true or false is the provided expiryDate is expired or not
 * @param expiryDate
 */
export const isExpiredDate = (expiryDate: Date): boolean => {
  const now = new Date();
  const nowYearMonth = new Date(now.getFullYear(), now.getMonth());
  return nowYearMonth > expiryDate;
};

export type ExpireStatus = "VALID" | "EXPIRING" | "EXPIRED";

/**
 * A function to check if the given date is in the past or in the future.
 * It returns:
 * -VALID, if the date is in the future
 * -EXPIRING, if the date is within the next 7 days
 * -EXPIRED, if the date is in the past
 * @param date Date
 */
export const getExpireStatus = (date: Date): ExpireStatus => {
  const remainingMilliseconds = date.getTime() - Date.now();
  return remainingMilliseconds > 1000 * 60 * 60 * 24 * 7
    ? "VALID"
    : remainingMilliseconds > 0
    ? "EXPIRING"
    : "EXPIRED";
};

/*
 * this code is a copy from gcanti repository https://github.com/gcanti/io-ts-types/blob/06b29a2e74c64b21ee2f2477cabf98616a7af35f/src/Date/DateFromISOString.ts
 * this because to avoid node modules conflicts given from using io-ts-types
 * DateFromISOStringType is a codec to encode (date -> string) and decode (string -> date) a date in iso format
 */
export class DateFromISOStringType extends t.Type<Date, string, unknown> {
  constructor() {
    super(
      "DateFromISOString",
      (u): u is Date => u instanceof Date,
      (u, c) => {
        const validation = t.string.validate(u, c);
        if (E.isLeft(validation)) {
          return validation as any;
        } else {
          const s = validation.right;
          const d = new Date(s);
          return isNaN(d.getTime()) ? t.failure(s, c) : t.success(d);
        }
      },
      a => a.toISOString()
    );
  }
}

export const DateFromISOString: DateFromISOStringType =
  new DateFromISOStringType();

/**
 *
 * It provides, given 2 strings that represent the year and the month, a single string in the format
 * specified by the locales (IT: MM/YY, EN: MM/YY) or undefined if one of the inputs is not provided
 * @param fullYear
 * @param month
 */
export const getTranslatedShortNumericMonthYear = (
  fullYear?: string,
  month?: string
): string | undefined => {
  if (!fullYear || !month) {
    return undefined;
  }
  const year = parseInt(fullYear, 10);
  const indexedMonth = parseInt(month, 10);
  if (isNaN(year) || isNaN(indexedMonth)) {
    return undefined;
  }
  return localeDateFormat(
    new Date(year, indexedMonth - 1),
    I18n.t("global.dateFormats.shortNumericMonthYear")
  );
};

/**
 * Generates a locale formatted timestamp,
 * used to force the refresh of the Image component cache for Android devices
 * every 24 hours.
 * @returns the actual locale date short format without slashes.
 */
export const toAndroidCacheTimestamp = () =>
  localeDateFormat(
    new Date(),
    I18n.t("global.dateFormats.shortFormat").replace(/\//g, "")
  );

/**
 * This function returns a Date object from a string in format "YYYYMM"
 * @param expiryDate
 */
export const getDateFromExpiryDate = (expiryDate: string): Date | undefined => {
  try {
    const year = +expiryDate.slice(0, 4);
    const month = +expiryDate.slice(4, 6);
    const date = new Date(year, month - 1);
    return isNaN(date.getDate()) ? undefined : date;
  } catch {
    return undefined;
  }
};

/**
 * Remove timezone from a date.
 * @param date - the date to remove timezone from
 * @returns a new date with the timezone removed
 */
export const removeTimezoneFromDate = (date: Date) => {
  if (isNaN(date.getTime())) {
    throw new Error("Invalid date");
  }
  const userTimezoneOffset = date.getTimezoneOffset() * 60000;
  return new Date(date.getTime() + userTimezoneOffset);
};