Use array.reduce to join strings with comma, but avoid the extra comma

Viewed 1004

I have an array of objects to be transformed and merged into a single string with array.reduce.

const arr = [{id: 7, task: "foo"}, {id: 22, task: "bar"}]

The result should be 7. foo, 22. bar

If I write this code, it will work but produce , 7. foo, 22.bar:

arr.reduce((pre,cur)=> pre + `, ${cur.id}. ${cur.task}`, '')

How can I properly do this without the extra comma, preferably only in FP?

5 Answers

Is reduce a requirement? Map is easier to understand and read.

arr.map(o => `${o.id}. ${o.task}`).join(',')

You can resolve this easily by checking if pre is not a falsy value using ternary operator at start like:

`${pre ? ', ' : ''}`

const arr = [{id: 7, task: "foo"}, {id: 22, task: "bar"}]
const res = arr.reduce((pre,cur)=> pre + `${pre ? ', ' : ''}${cur.id}. ${cur.task}`, '')
console.log(res)

functional 101

The idiomatic JavaScript solution is to map-join. But Functional programming is about breaking the program into reusable modules and creating barriers of abstraction -

// Task.js

const empty =
  { id: 0, task: "" }

const task = (id = 0, task = "") =>
  ({ id, task })

const toString = (t = empty) =>
  `${t.id}. ${t.task}`

const toStringAll = ([ first, ...rest ]) =>
  rest.reduce   // <-- reduce
    ( (r, x) => r + ", " + toString(x)  
    , toString(first) 
    )

export { empty, task, toString, toStringAll } 

So there's a possible and sensible implementation using reduce. Readability of this program is good because each part of the module is small and does just one thing.

Now we close the imaginary lid on our module and forget all of the complexity within. What remains is a clean interface that clearly communicates the capabilities of the module -

// Main.js

import { task, toStringAll } from './Task'

const data =
  [ task(7, "foo")
  , task(22, "bar")
  , task(33, "qux")
  ]

console.log(toStringAll(data))
// 7. foo, 22. bar, 33. qux

console.log(toStringAll(data.slice(0,2)))
// 7. foo, 22. bar

console.log(toStringAll(data.slice(0,1)))
// 7. foo

console.log(toStringAll(data.slice(0,0)))
// 0.

Expand the snippet below to verify the result in your browser -

const empty =
  { id: 0, task: "" }
  
const task = (id = 0, task = "") =>
  ({ id, task })
  
const toString = (t = empty) =>
  `${t.id}. ${t.task}`
  
const toStringAll = ([ first, ...rest ]) =>
  rest.reduce
    ( (r, x) => r + ", " + toString(x)
    , toString(first) 
    )
    
const data =
  [ task(7, "foo")
  , task(22, "bar")
  , task(33, "qux")
  ]

console.log(toStringAll(data))
// 7. foo, 22. bar, 33. qux

console.log(toStringAll(data.slice(0,2)))
// 7. foo, 22. bar

console.log(toStringAll(data.slice(0,1)))
// 7. foo

console.log(toStringAll(data.slice(0,0)))
// 0.


the barrier of abstraction

Main.js is separated from Task by a barrier of abstraction. Now we can choose any implementation for toString and toStringAll without requiring Main to concern itself with how Task operates under the lid.

Let's practice making a change and update how an empty task is represented. In the program above we see 0. but we will make it say Empty. instead. And just for fun, let's try a new implementation of toStringAll -

// Task.js

const empty = //

const task = //

const toString = (t = empty) =>
  t === empty
    ? `Empty.`                // <-- custom representation for empty
    : `${t.id}. ${t.task}`

const toStringAll = ([ t = empty, ...more ]) =>
  more.length === 0
    ? toString(t)
    : toString(t) + ", " + toStringAll(more) // <-- recursive

//

export { empty, task, toString, toStringAll }

Main doesn't need to do anything differently -

// Main.js

import { task, toStringAll } from './Task'

const data = //

console.log(toStringAll(data))
// 7. foo, 22. bar, 33. qux

console.log(toStringAll(data.slice(0,2)))
// 7. foo, 22. bar

console.log(toStringAll(data.slice(0,1)))
// 7. foo

console.log(toStringAll(data.slice(0,0)))
// Empty.

Expand the snippet below to verify the result in your browser -

const empty =
  { id: 0, task: "" }
  
const task = (id = 0, task = "") =>
  ({ id, task })
  
const toString = (t = empty) =>
  t === empty
    ? `Empty.`
    : `${t.id}. ${t.task}`
  
const toStringAll = ([ t = empty, ...more ]) =>
  more.length === 0
    ? toString(t)
    : toString(t) + ", " + toStringAll(more)
    
const data =
  [ task(7, "foo")
  , task(22, "bar")
  , task(33, "qux")
  ]

console.log(toStringAll(data))
// 7. foo, 22. bar, 33. qux

console.log(toStringAll(data.slice(0,2)))
// 7. foo, 22. bar

console.log(toStringAll(data.slice(0,1)))
// 7. foo

console.log(toStringAll(data.slice(0,0)))
// Empty.

you may add a check for the first array element:

arr.reduce((pre, cur, index) => {
  if(index === 0) {
    return `${cur.id}. ${cur.task}`
  }; 
  return pre + `, ${cur.id}. ${cur.task}`}
, '');

You can use the index argument of the reducer:

const format = object => `${object.id}. ${object.task}`;
const result = arr.reduce((pre, object, index) => (index ? pre + ', ' : '') + format(object), '');
Related