Отформатируйте несколько дат, чтобы они хорошо смотрелись в тексте, используя функциональный подход

У меня был фрагмент кода, который решил действительно простую проблему — он сформировал две даты, чтобы лучше выглядеть на веб-странице. Выглядело это так:

function formatDateRange(
  days: { firstDay: string, lastDay: string }
): string {

  let first, last;
  first = dayjs(days.firstDay); //dayjs parses string that formatted like this "YYYY-MM-DD" 
  last = dayjs(days.lastDay);   //and returns a wrapper for Date object
  if (first.isSame(last, "date")) {
    first = first.format("DD MMMM, YYYY")
    last = ""

  } else if (first.isSame(last, "month")) {
    first = first.format("DD") + " ... "
    last = last.format("DD, MMMM YYYY")

  } else if (first.isSame(last, "year")) {
    first = first.format("DD MMM") + " ... "
    last = last.format("DD MMM, YYYY")

  } else {
    first = first.format("DD MMM, YYYY") + " ... "
    last = last.format("DD MMM, YYYY")
  }
  return first + last
}

Затем я решил его реорганизовать и получил следующее:

//adapter that allows to use objects and arrays in function-composition
//EDIT: was called "callable"
function pluckFrom<T extends {}>(obj: T) {
  return (key: keyof T) => obj[key]
}

// a function for function-composition 
// (same as compose from redux but order is reversed)
function pipe(...funcs: Function[]) {
  return funcs.reduce((f1, f2) => (...args: any) => f2(f1(...args)))
}

//provides an interface to apply array of functions to array of data
function apply<A>(maps: Array<(a: A) => any>) {
  return {to: (arr: A[]) => arr.map((item, index) => maps[index](item))}
}

//takes array of dates, wrapped in Dayjs object and returns
//true if they're all the same in terms of unit of time 
function isSame([day1, ...rest]: Dayjs[]) {
  return (unit: UnitType) => rest.every(day => day.isSame(day1, unit))
}

interface DateRange {
  firstDay: string;
  lastDay: string;
}
//EDIT: removed redundant "same_" prefix
const case_to_format_map = {
  "date": ["DD MMMM, YYYY", " "],
  "month": ["DD ...", "DD, MMMM YYYY"],
  "year": ["DD MMM ... ", "DD MMM, YYYY"],
  " ": ["DD MMM, YYYY ... ", "DD MMM, YYYY"]
}

type Case = keyof typeof case_to_format_map

//EDIT: slightly refactored 
function formatDateRange({firstDay, lastDay}: DateRange): string {

  const
    getCase = (days: Dayjs[]) => (
      (["date", "month", "year", " "] as UnitType[])
        .filter(isSame(days))[0]
    ) as Case
    ,
    makeFormatter = (f: string) => (d: Dayjs) => d.format(f)
    ,
    getFormatters = pipe(
      getCase,
      pluckFrom(case_to_format_map),
      formats => formats.map(makeFormatter)
    )
    ,
    days = [firstDay, lastDay].map(dayjs)

  return apply(getFormatters(days)).to(days).join(" ")
}

В нем больше кода, но он мне нравится, потому что теперь у меня есть 4 дополнительные служебные функции, он более читабельный для меня. В остальном я не уверен, что мне стало лучше или хуже. Итак, у меня есть такие вопросы:

Вам это доступно для чтения?

Соответствует ли он функциональному стилю и лучшим практикам?

Я не уверен, что мне когда-нибудь понадобится расширять этот код, но как я могу сделать его более расширяемым?

1 ответ
1

Я лично предпочитаю первый, но это очень субъективно. От человека, впервые читающего код, я могу бегло просмотреть первую версию и получить хорошее общее представление о том, о чем этот код. Во второй версии я вижу кучу абстрактных функций, и мне требуется больше времени, чтобы увидеть, что на самом деле делает код.

Хотя у второй версии есть некоторые преимущества, я не думаю, что она того стоит в данном конкретном случае, если вы не знаете чего-то о будущем этого кода, чего я не знаю. Я думаю, что я пытаюсь сказать, что вам следует переформатировать свой код таким образом, только если у вас есть для этого веская причина. Например, вы хотите иметь возможность изменять некоторую часть поведения функции во время выполнения или когда она вызывается из разных частей кодовой базы.

    Добавить комментарий

    Ваш адрес email не будет опубликован. Обязательные поля помечены *