1
\$\begingroup\$

I had a piece of code that solved really simple problem - it formated two dates to look better on a webpage. It looked like this:

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
}

Than I decided to refactor it, and I got this:

//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(" ")
}

It's more code, but I like it because now I have 4 additional utility functions, it's more readable for me. Other than that, I'm not sure if I made better or worse. And so I have these questions:

Is it readable to you?

Is it aligned with functional style and best practices?

I'm not sure if I will ever need to extend this code but how can I make it more extendable?

\$\endgroup\$
4
  • \$\begingroup\$ I think it's a neat solution, it can be modular if callable, pipe and apply are candidates to be called in other tasks. However in fact it's more code than what you had previously. Appart from that the second code is quite cryptical so hm well, it's quite hard to tell. \$\endgroup\$ May 13 '21 at 0:43
  • \$\begingroup\$ Concatenating strings to determine which function to call by name really smells to me. I feel like the more functional solution would be to iterate through an array of tuples that contain the test and formatting function together and just iterate over that in a loop. \$\endgroup\$
    – Turksarama
    May 13 '21 at 5:23
  • \$\begingroup\$ Please follow out guidelines: codereview.stackexchange.com/help/how-to-ask \$\endgroup\$
    – BCdotWEB
    May 13 '21 at 5:56
  • \$\begingroup\$ Here catchts.com/FP-style#compose you can find an example of typing compose function \$\endgroup\$ Jun 1 '21 at 7:17
0
\$\begingroup\$

I personally prefer the first one, but this is very subjective. From a person reading the code for the first time, I can skim over the first version and have a good general idea of what the code is about. For the second version, I see a bunch of abstract functions and it takes me longer to see what the code actually does.

While the second version has some advantages, I don't believe it's worth it in this particular case unless you know something about the future of this code that I don't. I guess the point I'm trying to make is that, you should only reformat your code like that if you have a good reason for it. For example, you want to be able to change some part of the function behavior at runtime or when it's called from different parts of the codebase.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.