1

I have an interval partitioned into “MECE” subintervals, and I want to check which subinterval a number lies in.

(MECE stands for “mutually exclusive & collectively exhaustive”, meaning the partitions do not overlap and they leave no gaps in between.)

First I simply used if .. else if:

;    if (0/6 <= h && h < 1/6) { rgb = [c, x, 0] }
else if (1/6 <= h && h < 2/6) { rgb = [x, c, 0] }
else if (2/6 <= h && h < 3/6) { rgb = [0, c, x] }
else if (3/6 <= h && h < 4/6) { rgb = [0, x, c] }
else if (4/6 <= h && h < 5/6) { rgb = [x, 0, c] }
else if (5/6 <= h && h < 6/6) { rgb = [c, 0, x] }

Then I tried using the ternary operator:

rgb =
    (0/6 <= h && h < 1/6) ? [c, x, 0] :
    (1/6 <= h && h < 2/6) ? [x, c, 0] :
    (2/6 <= h && h < 3/6) ? [0, c, x] :
    (3/6 <= h && h < 4/6) ? [0, x, c] :
    (4/6 <= h && h < 5/6) ? [x, 0, c] :
    (5/6 <= h && h < 6/6) ? [c, 0, x] : [c, x, 0]

I also read about how object lookups are a more performant alternative to switch statements. Since I’m not checking the exact value of h, I had to improvise:

rgb = ({
    [`${(0/6 <= h && h < 1/6)}`]: [c, x, 0],
    [`${(1/6 <= h && h < 2/6)}`]: [x, c, 0],
    [`${(2/6 <= h && h < 3/6)}`]: [0, c, x],
    [`${(3/6 <= h && h < 4/6)}`]: [0, x, c],
    [`${(4/6 <= h && h < 5/6)}`]: [x, 0, c],
    [`${(5/6 <= h && h < 6/6)}`]: [c, 0, x],
})['true']

The problem with object lookup is that there will be five keys of 'false', which is not good practice. So I wanted to use a similar lookup, but using ES6 native Map class:

new Map([
    [[0/6, 1/6], [c, x, 0]],
    [[1/6, 2/6], [x, c, 0]],
    [[2/6, 3/6], [0, c, x]],
    [[3/6, 4/6], [0, x, c]],
    [[4/6, 5/6], [x, 0, c]],
    [[5/6, 6/6], [c, 0, x]],
]).forEach((value, key) => {
    if (key[0] <= h && h < key[1]) rgb = value
})

Which is the most readable, maintainable, fastest-running way to achieve this? Are there any other options?

  • 3
    There's very little difference between these three. The performance differences are almost certainly inconsequential. Did you try measuring them to see which one performs the best? As a purely subjective opinion, I prefer the middle one. – Robert Harvey Mar 15 at 19:27
  • 3
    I would actually use Math.floor(h*6) as an index into some array, instead of these lengthy interval comparisons. – Doc Brown Mar 15 at 21:38
  • Readable depends upon domain & expertise of the reader. Know your audience. Cascaded if-else works, and is readable. – ChuckCottrill Mar 16 at 0:57
3

Beauty (readability) lies in the eye of the beholder.

Maintainability is inextricably linked to readability, but different.

Speed depends upon algorithmic complexity, and efficient implementation. Different languages have different underlying support.

Comments on your offered solutions,

  • cascaded if-else - 'multiple alternative selection structure', readable by the vast majority of developers, and works
  • cascaded ternary (if-else) - same as first solution, less readable to some, but brief so better for others
  • object lookups - less readable than a lookup table
  • map lookup - readable, but not as intuitive to some developers, but readable

Now, some alternatives,

  • partition hash-index function - as Doc Brown says, compose a function that calculates the partition, use the result as an index into a lookup table (array or hash/map). not very readable, and you need to compose the function. Not maintainable, as you need new function for every range change
  • partition array & scan - iteratively scan array of partition boundaries to lookup value, easy to read and maintain, half as many comparisons
  • decision tree - cascaded if-else by splitting ranges
  • decision tree array - decision tree in data and traversal function

More about these alternatives:

I'm not going to bother to present exact javascript syntax (this is SWE not SO), but this will give you alternatives.

partition hash-index function, essentially, compute k = h * 6 and lookup k in map

k = hash_function(h);
//hash_function(h) = ({ p=Math.floor(h*6)+1; p=(p<0 || p>6) ? 0 : p; })
partition_hash =
{ 0: [c, x, 0],
  1: [c, x, 0],
  2: [x, c, 0],
  3: [0, c, x],
  4: [0, x, c],
  5: [x, 0, c],
  6: [c, 0, x]
};
rgb = partition_hash[index];

partition array & scan, iterate over array looking for boundary 'cuts', assumes continuous range (no gaps, the CE in MECE)

boundary_lookup =
[ [0,   [c, x, 0]],
  [1/6, [c, x, 0]],
  [2/6, [x, c, 0]],
  [3/6, [0, c, x]],
  [4/6, [0, x, c]],
  [5/6, [x, 0, c]],
  [6/6, [c, 0, x]]
];
rgb = boundary_lookup[index][0]
for( index=0; index<7; ++index ) {
    if( h < boundary_lookup[index][0] ) rgb = boundary_lookup[index][1];
}

decision tree, less readable, but cuts O(n)+1 tests into O(log2(n))+1

if( h < 0 || h > 1 ) {
    rgb = [c, x, 0];
}
if( h < 3/6 ) {
    if( h < 2/6 ) {
        if( h < 1/6 ) { rgb = [x, c, 0] } // [1/6
        else { rgb = [0, c, x] } // 2/6
    }
    else { rgb = [0, c, x] } //3/6
}
else {
    if( h < 5/6 ) {// inrange [2/6 .. 3/6]
        if( h < 4/6 ) { rgb = [x, 0, c] } // 4/6
        else { rgb = [x, 0, c] } // 5/6
    }
    else { rgb = [0, c, x] } // 6/6
}

decision tree array, uses an array to express decision tree boundary points, easy to read the values, but hard to write the traversal (left as an exercise), but fast, and not as ugly as the decision tree code, and once the traversal is written, can be reused

[ [3/6, [ [1/6, [c, x, 0]],
          [2/6, [x, c, 0]],
          [3/6, [0, c, x]]
        ]
  ],
  [6/6, [ [4/6, [0, x, c]],
          [5/6, [x, 0, c]],
          [6/6, [c, 0, x]] ]
        ]
  ]
]

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.