Search code examples
javascriptarraysstringsplitmapreduce

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.


Solution

    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; }