Skip to content

How does one reduce and mutate/change string entries of an array based on common substring patterns?

I have an array of string items …

[
  'Mon  : 9:00AM - 7:00PM',
  'Tue  : 9:00AM - 10:00PM',
  'Wed  : Closed',
  'Thu  : 9:00AM - 7:00PM',
  'Fri  : 9:00AM - 7:00PM',
  'Sat  :  Closed',
  'Sun  :  Closed',
]

… and I want to achieve a result like the one below …

[
  'Mon: 9:00AM - 7:00PM',
  'Tue: 9:00AM - 10:00PM',
  'Wed: Closed',
  'Thu-Fri: 9:00AM - 7:00PM',
  'Sat-Sun:  Closed',
]

Any help is really appreciated.

Answer

  1. Firstly one needs to separate the day value from the hours value part of a single opening hours string.

    • This either can be achieved via indexOf, substring and trim

      function splitOpeningHoursEntry(entry) {
        // e.g.: 'Mon  : 9:00AM - 7:00PM'
        const indexOfColon = entry.indexOf(':'); // e.g. 5
      
        // entry.substring(0, 5) ... e.g.: 'Mon  '
        const day = entry.substring(0, indexOfColon);
        // entry.substring(6) ... e.g.: ' 9:00AM - 7:00PM'
        const hours = entry.substring(indexOfColon + 1);
      
        // e.g.: ['Mon', '9:00AM - 7:00PM']
        return [day.trim(), hours.trim()];
      }
      
    • … or it can be done by split-ting with a Regular Expression like … /(^[^:s]+)s*:s*/ … and slice-ing the results array …

      function regexSplitOpeningHoursEntry(entry) {
        // [https://regex101.com/r/vGRck7/3]
        // entry.split(':')                 // ["Mon  ", " 9", "00AM - 7", "00PM"]
      
        // [https://regex101.com/r/vGRck7/2]
        // entry.split(/s*:s*/)           // ["Mon", "9", "00AM - 7", "00PM"]
      
        // [https://regex101.com/r/vGRck7/1]
        // entry.split(/(^[^:s]+)s*:s*/) // ["", "Mon", "9:00AM - 7:00PM"];
      
        return entry.split(/(^[^:s]+)s*:s*/).slice(1);
      }
      
  2. Then one has to map an entire array of opening hours strings into an array of arrays, where each array-item contains the day value as first and the hours value as second array item … either like this …

    sampleList.map(splitOpeningHoursEntry);
    

    … or like that …

    sampleList.map(regexSplitOpeningHoursEntry);
    
  3. On top one needs to reduce this array of splitted [<day>, <hours>] entries into its compact form …

  4. Finally one has to map each splitted [<day>, <hours>] entry with a concatenation task back into its human readable string form….

const sampleList = [
  'Mon  : 9:00AM - 7:00PM',
  'Tue  : 9:00AM - 10:00PM',
  'Wed  : Closed',
  'Thu  : 9:00AM - 7:00PM',
  'Fri  : 9:00AM - 7:00PM',
  'Sat  :  Closed',
  'Sun  :  Closed',
];

function splitOpeningHoursEntry(entry) {
  // e.g.: 'Mon  : 9:00AM - 7:00PM'
  const indexOfColon = entry.indexOf(':'); // e.g. 5

  // entry.substring(0, 5) ... e.g.: 'Mon  '
  const day = entry.substring(0, indexOfColon);
  // entry.substring(6) ... e.g.: ' 9:00AM - 7:00PM'
  const hours = entry.substring(indexOfColon + 1);

  // e.g.: ['Mon', '9:00AM - 7:00PM']
  return [day.trim(), hours.trim()];
}
function regexSplitOpeningHoursEntry(entry) {
  // [https://regex101.com/r/vGRck7/3]
  // entry.split(':')                 // ["Mon  ", " 9", "00AM - 7", "00PM"]

  // [https://regex101.com/r/vGRck7/2]
  // entry.split(/s*:s*/)           // ["Mon", "9", "00AM - 7", "00PM"]

  // [https://regex101.com/r/vGRck7/1]
  // entry.split(/(^[^:s]+)s*:s*/) // ["", "Mon", "9:00AM - 7:00PM"];

  return entry.split(/(^[^:s]+)s*:s*/).slice(1);
}

function compactOpeningHoursEntries(compactEntries, splitEntry, idx, arr) {
  // get the predecessor item of the currently
  // processed `splitEntry` item or default to [].
  const prevSplitEntry = arr[idx - 1] || [];

  // get the successor item of the currently
  // processed `splitEntry` item or default to [].
  const nextSplitEntry = arr[idx + 1] || [];

  if (prevSplitEntry[1] !== splitEntry[1]) {
    // in case the previous and current `hours` values do not match ...

    // ... push the current entry of splitted `day` and `hours`
    // values into `compactEntries` which is the accumulating
    // array of the compacted form of all opening hours entries.
    compactEntries.push(splitEntry);

  } else if (nextSplitEntry[1] !== splitEntry[1]) {
    // ... or in case the next and current `hours` values do not match ...

    const lastCompactEntry = compactEntries[compactEntries.length - 1];

    // ...retrieve the first and the last day value
    // of a compactly written day-range format...
    const firstDayInRange = lastCompactEntry[0];
    const lastDayInRange = splitEntry[0];

    // ...and create and rewrite its compact form
    // as the compacted entry's final day value.
    lastCompactEntry[0] = firstDayInRange + '-' + lastDayInRange;
  }
  return compactEntries;
}

function concatOpeningHoursEntry([day, hours]) {
  return `${ day }: ${ hours }`;
}

// First one needs to separate the `day` from the
// `hours` part of a single opening hours string
console.log(
  "splitOpeningHoursEntry('Mon  : 9:00AM - 7:00PM') ...",
  splitOpeningHoursEntry('Mon  : 9:00AM - 7:00PM')
);
console.log(
  "regexSplitOpeningHoursEntry('Mon  : 9:00AM - 7:00PM') ...",
  regexSplitOpeningHoursEntry('Mon  : 9:00AM - 7:00PM')
);

// Then one does map an entire array of opening hours strings
// into an array of arrays, where each array item contains the
// `day` value as first and the `hours` value as second array item.
console.log(
  '... list item `split` mapping ... ',
  sampleList
    .map(splitOpeningHoursEntry)
  //.map(regexSplitOpeningHoursEntry)
)

// On top one has to `reduce` this array of splitted
// `[<day>, <hours>]` entries into its compact form.
console.log(
  '... list item `split` mapping and split entry reducing ... ',
  sampleList
    .map(splitOpeningHoursEntry)
    .reduce(compactOpeningHoursEntries, [])
);

// Finally one needs to `map` each splitted `[<day>, <hours>]` entry
// with a concatenation task back into its human readable string form.
console.log(
  '... list item `split` mapping, reducing and a final concatenation mapping ... ',
  sampleList
    .map(splitOpeningHoursEntry)
    .reduce(compactOpeningHoursEntries, [])
    .map(concatOpeningHoursEntry)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }

Another less talkative proof of concept …

function splitOpeningHoursEntry(entry) {
  return entry.split(/(^[^:s]+)s*:s*/).slice(1);
}
function concatOpeningHoursEntry([day, hours]) {
  return `${ day }: ${ hours }`;
}

function compactOpeningHoursEntries(compactEntries, splitEntry, idx, arr) {
  const prevSplitEntry = arr[idx - 1] || [];
  const nextSplitEntry = arr[idx + 1] || [];

  if (prevSplitEntry[1] !== splitEntry[1]) {

    compactEntries.push(splitEntry);

  } else if (nextSplitEntry[1] !== splitEntry[1]) {
    const lastCompactEntry = compactEntries[compactEntries.length - 1];

    const firstDayInRange = lastCompactEntry[0];
    const lastDayInRange = splitEntry[0];

    lastCompactEntry[0] = firstDayInRange + '-' + lastDayInRange;
  }
  return compactEntries;
}
console.log([
    'Mon  : 08:00AM - 17:00PM',
    'Tue  : 08:00AM - 17:00PM',
    'Wed  : 08:00AM - 17:00PM',
    'Thu  : 10:00AM - 14:00PM',
    'Fri  : 10:00AM - 14:00PM',
    'Sat  :  Closed',
    'Sun  :  Closed',
  ], '=>', [
    'Mon  : 08:00AM - 17:00PM',
    'Tue  : 08:00AM - 17:00PM',
    'Wed  : 08:00AM - 17:00PM',
    'Thu  : 10:00AM - 14:00PM',
    'Fri  : 10:00AM - 14:00PM',
    'Sat  :  Closed',
    'Sun  :  Closed',
  ]
  .map(splitOpeningHoursEntry)
  .reduce(compactOpeningHoursEntries, [])
  .map(concatOpeningHoursEntry)
);
.as-console-wrapper { min-height: 100%!important; top: 0; }