aboutsummaryrefslogtreecommitdiff
path: root/lib/unitsToRanges.js
blob: ecd3c968a01411ad349ba3a9db47285dc1db6853 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
/**
 * Convert a list of unit numbers into an addr:flats list. Examples:
 * 1,2,3,5 => 1-3;5
 * 1a,2a,3a,5 => 1a-3a;5
 * 1,2,3-5 => 1-5
 * 1,2,3a-5a => 1-2;3a-5a
 *
 * @param {Array} units
 * @param {Array} [sourceAddresses] - the source addresses where these units came from, used for debugging
 *
 * @returns {string} addr:flats list
 */
module.exports = (units, sourceAddresses) => {
  const regexp = /^(?<pre>\D*)(?<num>\d*)(?<suf>\D*)$/

  // expand out any existing ranges which may be mixed into the input
  const expandedUnits = units
    .slice()
    .reduce((acc, cur) => {
      const rangeParts = cur.split('-')
      if (rangeParts.length === 2) {
        // was a range, pull out prefix and suffix
        const fromMatch = rangeParts[0].match(regexp)
        const toMatch = rangeParts[1].match(regexp)

        // matching prefix and suffix
        if (fromMatch.groups.pre === toMatch.groups.pre && fromMatch.groups.suf === toMatch.groups.suf) {
          for (let i = fromMatch.groups.num; i <= toMatch.groups.num; i++) {
            acc.push(`${fromMatch.groups.pre}${i}${fromMatch.groups.suf}`)
          }
        } else {
          // prefix/suffix don't match in the from-to, so just pass as is
          console.log(`passed a range with different prefix/suffix: ${rangeParts[0]}-${rangeParts[1]}`)
          if (sourceAddresses) {
            console.log(JSON.stringify(sourceAddresses, null, 2))
          }
          acc.push(cur)
        }
      } else if (rangeParts.length > 2) {
        // 1-2-3 not sure if this ever occurs, but just pass as is
        console.log(`Unsupported range ${cur}`)
        if (sourceAddresses) {
          console.log(JSON.stringify(sourceAddresses, null, 2))
        }
        acc.push(cur)
      } else {
        // was not a range
        acc.push(cur)
      }
      return acc
    }, [])

  // combine individual unit values into ranges
  const existingRanges = []

  // adapted from https://stackoverflow.com/a/54973116/6702659
  const formedRanges = [...new Set(expandedUnits)]
    .slice()
    .map(unit => {
      if (unit.split('-').length > 1) {
        existingRanges.push(unit)
        return []
      } else {
        return [unit]
      }
    })
    .flat()
    .sort(sortNumbers)
    .reduce((acc, cur, idx, src) => {
      const curParts = cur.match(regexp)
      const prevParts = idx > 0 ? src[idx - 1].match(regexp) : null

      if (!curParts) {
        console.log(`"${cur}" didn't match regexp for prefix number suffix`)
        if (sourceAddresses) {
          console.log(JSON.stringify(sourceAddresses, null, 2))
        }
        acc.push([cur])
        return acc
      }

      const curNum = curParts.groups.num
      const prevNum = prevParts ? prevParts.groups.num : null

      if ((idx > 0) && ((curNum - prevNum) === 1)) {
        if (prevParts ? (curParts.groups.pre === prevParts.groups.pre && curParts.groups.suf === prevParts.groups.suf) : true) {
          acc[acc.length - 1][1] = cur
        } else {
          acc.push([cur])
        }
      } else {
        acc.push([cur])
      }
      return acc
    }, [])
    .map(range => range.join('-'))

  const unitRanges = [...formedRanges, ...existingRanges]

  return unitRanges.length ? unitRanges.join(';') : null
}

/* custom sort function where 2 goes before 1A and 1A goes before 1B */
function sortNumbers(a, b) {
  if (Number.isInteger(Number(a)) && Number.isInteger(Number(b))) {
    // both are integers
    return a - b
  } else if (Number.isInteger(Number(a)) && !Number.isInteger(Number(b))) {
    // a is integer but b isn't, so a goes before b
    return  -1
  } else if (!Number.isInteger(Number(a)) && Number.isInteger(Number(b))) {
    // a isn't integer but b is, so a goes after b
    return  1
  } else {
    // neither are integers
    return a.localeCompare(b)
  }
}