mwilson
mwilson

Reputation: 12900

Grouping array of years into a range

I'm trying to take an array like this:

[1999, 2000, 2001, 2002, 2003, 2004, 2006, 2008, 2009, 2010]

and make it output:

1999 - 2004, 2006, 2008 - 2010

It appeared to be a simple task, but can't seem to get it right. I'm assuming Array.reduce is the right tool for this:

const years = [1999, 2000, 2001, 2002, 2003, 2004, 2006, 2008, 2009, 2010];
const str = years.reduce((acc, curr, idx) => {
  const prevYear = years[idx - 1];
  const nextYear = years[idx + 1];
  if (!prevYear) {
    acc.push(curr.toString());
    return acc;
  }
  if (prevYear === curr - 1 && nextYear === curr + 1) {
    if (acc[acc.length - 1] !== '-') {
      acc.push('-');
    }
    return acc;
  } else {
    acc.push(curr.toString());
    if (nextYear) {
      acc.push(',');
    }
    return acc;
  }
  return acc;
}, []);
console.log(str.join(''))

I'm almost there but I'm trying to make it look as clean as possible without a bunch of if statements.

I'm aware I can take the poor-mans route and just run a str.replace(',-', '-') to fix my last little hurdle, but I don't like that.

Upvotes: 3

Views: 102

Answers (4)

Kwame Opare Asiedu
Kwame Opare Asiedu

Reputation: 2355

Using a traditional for loop, you could write:

const years = [1999, 2000, 2001, 2002, 2003, 2004, 2006, 2008, 2009, 2010];
const sliceInterval = 5;
const summary = [];
let sliceCounter = 0;
let segment = "";

for (var i = 0; i < years.length; i++) {
    const currentYear = years[i];
    if (sliceCounter === 0) segment = currentYear + " - ";
    if (sliceCounter !== 0 && (i === years.length - 1 || sliceCounter % sliceInterval === 0)) {
        segment += currentYear;
        summary.push(segment);
        sliceCounter = 0;
        segment = "";
    } else sliceCounter++;
}

console.log(summary.join(", "));

Upvotes: 0

Dawid Kisielewski
Dawid Kisielewski

Reputation: 829

Should be something like this

const years = [1999, 2001, 2000, 2002, 2003, 2004, 2006, 2008, 2009, 2010]
const getGroups = (years) => {
  const groups = []
  let lastYear = null;
  let currentGroup = {
    from: null,
    to: null
  }

  years.sort().forEach(year => {
    if (currentGroup.from === null) {
      currentGroup.from = year
       currentGroup.to = year
    } else if (lastYear && lastYear + 1 === year) {
      currentGroup.to = year
    } else {
      if (currentGroup.to === null) {
        currentGroup.to = lastYear
      }

      groups.push(currentGroup)

      currentGroup = {
        from: year,
        to: null
      }
    }
    lastYear = year
  })

  groups.push(currentGroup)
  return groups
}

const printGroups = (groups) => {
  const groupsString = groups.map(group => {
    if(group.from === group.to)
      return `${group.from}`
    else
      return `${group.from} - ${group.to}`
  }).join(', ')
  console.log(groupsString)
}

const groups = getGroups(years)
printGroups(groups)

Upvotes: 0

CertainPerformance
CertainPerformance

Reputation: 370699

An alternative (which I think is a bit easier to conceptualize) is to iterate over the range rather than over the items in the array. When finding an item, track forward until no more found years in a row exist. If more than one year in a row was found, add that range, else just add the one year:

const getRanges = (arr) => {
  const set = new Set(arr); // for less computational complexity later
  const min = Math.min(...arr);
  const max = Math.max(...arr);
  const output = [];
  for (let i = min; i <= max; i++) {
    if (!set.has(i)) {
      continue;
    }
    const thisMin = i;
    while(set.has(i)) {
      i++;
    }
    output.push(i === thisMin + 1 ? thisMin : `${thisMin} - ${i - 1}`);
  }
  return output;
};
console.log(getRanges([1999, 2000, 2001, 2002, 2003, 2004, 2006, 2008, 2009, 2010]));

An issue with using reduce here is that you need to keep track of additional state (the number of items that are in the so-far-consecutive range, and their values) other than the acc output array, which gets really messy if you're trying to encapsulate everything inside the reduce callback. It's possible to do, but I wouldn't recommend it.

Upvotes: 6

Saransh Kataria
Saransh Kataria

Reputation: 1497

years.reduce((acc, cur, idx, src) => {
          if ((idx > 0) && ((cur - src[idx - 1]) === 1))
            acc[acc.length - 1][1] = cur;
          else acc.push([cur]);
          return acc;
        }, []).map(range => range.join(' - '));

I created an array of values and added indexes as a 2d array and then joined them using the separator.

Upvotes: 2

Related Questions