Implement rangeRight([start=0], end, [step=1]) — it produces the same set of numbers as Lodash's _.range, but the array comes back in descending order. The mental shortcut: build the ascending range [start, end) stepped by step, then reverse it. The signature mirrors range exactly, including the one-argument shorthand where the lone value is the end and start defaults to 0.
// start: number — inclusive lower bound. Defaults to 0.
// With ONE argument, that argument is `end` and start is 0.
// end: number — exclusive upper bound. The range is half-open: [start, end).
// step: number — the increment between values. Defaults to 1, or to -1 when
// end < start (a descending source range).
// returns: number[] — the [start, end) values, stepped by step, REVERSED.
function rangeRight(start, end, step): number[];
rangeRight(4); // → [3, 2, 1, 0] (one arg: end=4, start=0)
rangeRight(1, 5); // → [4, 3, 2, 1] (start..end form)
rangeRight(0, 20, 5); // → [15, 10, 5, 0] (explicit step)
rangeRight(0, -4, -1); // → [-3, -2, -1, 0] (negative step descends, then reverses)
rangeRight(0); // → [] (empty range)
rangeRight(4) means start = 0, end = 4. Two arguments are start, end. Three add an explicit step.end is never included: rangeRight(1, 5) covers 1, 2, 3, 4 and stops before 5.end < start and no step is given, the step is -1 so the source range descends. Match this before reversing.0 as []. Lodash emits a run of start for a zero step; here, return an empty array instead and the tests check that choice.range reversed. Whatever range(start, end, step) would produce, rangeRight returns the same values in the opposite order.You'll generate the numbers from start up to (but not including) end, stepping by step, and then hand them back in reverse — largest first.
range is the function that fills an array with evenly spaced numbers: range(1, 5) gives you [1, 2, 3, 4]. rangeRight answers the same question — which numbers are in this range? — but lists them backwards: [4, 3, 2, 1]. Think of a countdown. To count down from 4 you don't invent a new sequence; you take the ordinary count-up 0, 1, 2, 3, 4 and read it from the other end. That is the whole job: figure out the right set of numbers, then flip the order.
The signature has one wrinkle worth saying out loud. With a single argument, that argument is the end, not the start — rangeRight(4) means "from 0 up to 4," so start quietly defaults to 0.
Hold three numbers in your head — start, end, and step — and remember the range is half-open: it includes start and excludes end. Produce the ascending sequence start, start + step, start + 2·step, … while you haven't reached end, then reverse. Everything tricky about this problem lives in two small decisions: what step to use when the caller didn't pass one, and how many elements the range actually contains.
The most direct version builds the ascending array with a for loop, then calls .reverse(). This is genuinely correct for the common cases, and it's a useful stepping stone:
function rangeRight(start, end, step) {
if (end === undefined) {
end = start;
start = 0;
}
if (step === undefined) {
step = 1;
}
const ascending = [];
for (let i = start; i < end; i += step) {
ascending.push(i);
}
return ascending.reverse();
}
It nails rangeRight(4) and rangeRight(0, 20, 5). But it breaks on descending source ranges. Call rangeRight(0, -4, -1): the loop condition is i < end, i.e. 0 < -4, which is already false, so the loop never runs and you get [] instead of [-3, -2, -1, 0]. The i < end comparison only makes sense when you're stepping up. A negative step needs i > end, and a defaulted step needs to become -1 when end < start in the first place. Patching the loop with a sign check works, but there's a cleaner path: compute how many elements the range has up front, then you never need a direction-sensitive comparison at all.
function rangeRight(start, end, step) {
// One-argument form: the lone value is `end`, and `start` defaults to 0.
if (end === undefined) {
end = start;
start = 0;
}
// No step given: ascend by +1, or descend by -1 when end < start.
if (step === undefined) {
step = start < end ? 1 : -1;
}
// A zero step can never move start toward end, so produce nothing.
if (step === 0) {
return [];
}
// Number of elements in the half-open [start, end) range stepped by `step`.
// Math.ceil of the signed span over the step; clamp negatives to 0.
const length = Math.max(Math.ceil((end - start) / step), 0);
// Build the ascending range, then reverse it in place for the "right" order.
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = start + i * step;
}
return result.reverse();
}
module.exports = { rangeRight };
The key shift from the naive version is computing length instead of looping until a comparison fails. (end - start) / step is the signed span — when both the span and the step point the same way, it's positive; when they disagree (an impossible range like start = 5, end = 1, step = 1), it's negative and Math.max(…, 0) clamps it to 0. Because we now index by i from 0 to length, the value start + i * step is correct whether step is positive or negative — no i < end vs i > end branching. The two remaining guards handle the spec's corners: defaulting step to -1 when end < start, and short-circuiting a 0 step to an empty array.
Trace rangeRight(0, -4, -1) end to end — the case the naive loop got wrong.
end is -4 (not undefined), so the one-argument branch is skipped: start = 0, end = -4. step is -1 (passed explicitly), so it's left alone and isn't zero.
Now the length: (end - start) / step is (-4 - 0) / -1 = 4, and Math.ceil(4) is 4, so length = 4. The loop fills four slots:
i = 0 → result[0] = 0 + 0 * -1 = 0
i = 1 → result[1] = 0 + 1 * -1 = -1
i = 2 → result[2] = 0 + 2 * -1 = -2
i = 3 → result[3] = 0 + 3 * -1 = -3
ascending = [0, -1, -2, -3]
That's the ascending source range — counting down from 0 because the step is negative. The final .reverse() flips it to [-3, -2, -1, 0], which is the answer. Notice the loop never compared i against end; it just ran length times, so the negative direction needed no special handling.
start. rangeRight(4) means end = 4, start = 0 — not start = 4. Detect the one-argument form by checking end === undefined and shift the values before anything else, or every later calculation is off.step to 1 unconditionally. If end < start and you leave step at 1, the signed span (end - start) / 1 is negative, clamps to 0, and you return [] when lodash would descend. When no step is passed, set it to start < end ? 1 : -1.i < end for a negative step. A direction-sensitive comparison silently produces [] for descending ranges, because 0 < -4 is false from the start. Compute the length once and loop i from 0 instead — the comparison disappears.Math.ceil((end - start) / step) can be negative (e.g. start = 5, end = 1, step = 1 gives -4). Passing a negative length to new Array(...) throws RangeError: Invalid array length. Math.max(…, 0) turns any impossible range into a clean empty array.(end - start) / 0 is Infinity (or NaN), and an infinite length would hang or throw. Guard step === 0 and return [] up front so the math never sees a zero divisor._.range — the ascending sibling. It's this exact function without the final .reverse(). Implementing one gives you the other for free: rangeRight(...args) is range(...args).reverse().range(0, 1, 0.25) → [0, 0.25, 0.5, 0.75]. The length formula already handles this, but start + i * step accumulates floating-point error for long ranges; production code multiplies by an index from a fresh integer rather than repeatedly adding step._.rangeStep patterns. The same length-first technique powers any "fill N evenly spaced values" helper — linspace in numerical libraries, tick generators in charting code. Once you compute the count up front, generating ascending or descending output is a one-line change.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
Implement rangeRight([start=0], end, [step=1]) — it produces the same set of numbers as Lodash's _.range, but the array comes back in descending order. The mental shortcut: build the ascending range [start, end) stepped by step, then reverse it. The signature mirrors range exactly, including the one-argument shorthand where the lone value is the end and start defaults to 0.
// start: number — inclusive lower bound. Defaults to 0.
// With ONE argument, that argument is `end` and start is 0.
// end: number — exclusive upper bound. The range is half-open: [start, end).
// step: number — the increment between values. Defaults to 1, or to -1 when
// end < start (a descending source range).
// returns: number[] — the [start, end) values, stepped by step, REVERSED.
function rangeRight(start, end, step): number[];
rangeRight(4); // → [3, 2, 1, 0] (one arg: end=4, start=0)
rangeRight(1, 5); // → [4, 3, 2, 1] (start..end form)
rangeRight(0, 20, 5); // → [15, 10, 5, 0] (explicit step)
rangeRight(0, -4, -1); // → [-3, -2, -1, 0] (negative step descends, then reverses)
rangeRight(0); // → [] (empty range)
rangeRight(4) means start = 0, end = 4. Two arguments are start, end. Three add an explicit step.end is never included: rangeRight(1, 5) covers 1, 2, 3, 4 and stops before 5.end < start and no step is given, the step is -1 so the source range descends. Match this before reversing.0 as []. Lodash emits a run of start for a zero step; here, return an empty array instead and the tests check that choice.range reversed. Whatever range(start, end, step) would produce, rangeRight returns the same values in the opposite order.You'll generate the numbers from start up to (but not including) end, stepping by step, and then hand them back in reverse — largest first.
range is the function that fills an array with evenly spaced numbers: range(1, 5) gives you [1, 2, 3, 4]. rangeRight answers the same question — which numbers are in this range? — but lists them backwards: [4, 3, 2, 1]. Think of a countdown. To count down from 4 you don't invent a new sequence; you take the ordinary count-up 0, 1, 2, 3, 4 and read it from the other end. That is the whole job: figure out the right set of numbers, then flip the order.
The signature has one wrinkle worth saying out loud. With a single argument, that argument is the end, not the start — rangeRight(4) means "from 0 up to 4," so start quietly defaults to 0.
Hold three numbers in your head — start, end, and step — and remember the range is half-open: it includes start and excludes end. Produce the ascending sequence start, start + step, start + 2·step, … while you haven't reached end, then reverse. Everything tricky about this problem lives in two small decisions: what step to use when the caller didn't pass one, and how many elements the range actually contains.
The most direct version builds the ascending array with a for loop, then calls .reverse(). This is genuinely correct for the common cases, and it's a useful stepping stone:
function rangeRight(start, end, step) {
if (end === undefined) {
end = start;
start = 0;
}
if (step === undefined) {
step = 1;
}
const ascending = [];
for (let i = start; i < end; i += step) {
ascending.push(i);
}
return ascending.reverse();
}
It nails rangeRight(4) and rangeRight(0, 20, 5). But it breaks on descending source ranges. Call rangeRight(0, -4, -1): the loop condition is i < end, i.e. 0 < -4, which is already false, so the loop never runs and you get [] instead of [-3, -2, -1, 0]. The i < end comparison only makes sense when you're stepping up. A negative step needs i > end, and a defaulted step needs to become -1 when end < start in the first place. Patching the loop with a sign check works, but there's a cleaner path: compute how many elements the range has up front, then you never need a direction-sensitive comparison at all.
function rangeRight(start, end, step) {
// One-argument form: the lone value is `end`, and `start` defaults to 0.
if (end === undefined) {
end = start;
start = 0;
}
// No step given: ascend by +1, or descend by -1 when end < start.
if (step === undefined) {
step = start < end ? 1 : -1;
}
// A zero step can never move start toward end, so produce nothing.
if (step === 0) {
return [];
}
// Number of elements in the half-open [start, end) range stepped by `step`.
// Math.ceil of the signed span over the step; clamp negatives to 0.
const length = Math.max(Math.ceil((end - start) / step), 0);
// Build the ascending range, then reverse it in place for the "right" order.
const result = new Array(length);
for (let i = 0; i < length; i++) {
result[i] = start + i * step;
}
return result.reverse();
}
module.exports = { rangeRight };
The key shift from the naive version is computing length instead of looping until a comparison fails. (end - start) / step is the signed span — when both the span and the step point the same way, it's positive; when they disagree (an impossible range like start = 5, end = 1, step = 1), it's negative and Math.max(…, 0) clamps it to 0. Because we now index by i from 0 to length, the value start + i * step is correct whether step is positive or negative — no i < end vs i > end branching. The two remaining guards handle the spec's corners: defaulting step to -1 when end < start, and short-circuiting a 0 step to an empty array.
Trace rangeRight(0, -4, -1) end to end — the case the naive loop got wrong.
end is -4 (not undefined), so the one-argument branch is skipped: start = 0, end = -4. step is -1 (passed explicitly), so it's left alone and isn't zero.
Now the length: (end - start) / step is (-4 - 0) / -1 = 4, and Math.ceil(4) is 4, so length = 4. The loop fills four slots:
i = 0 → result[0] = 0 + 0 * -1 = 0
i = 1 → result[1] = 0 + 1 * -1 = -1
i = 2 → result[2] = 0 + 2 * -1 = -2
i = 3 → result[3] = 0 + 3 * -1 = -3
ascending = [0, -1, -2, -3]
That's the ascending source range — counting down from 0 because the step is negative. The final .reverse() flips it to [-3, -2, -1, 0], which is the answer. Notice the loop never compared i against end; it just ran length times, so the negative direction needed no special handling.
start. rangeRight(4) means end = 4, start = 0 — not start = 4. Detect the one-argument form by checking end === undefined and shift the values before anything else, or every later calculation is off.step to 1 unconditionally. If end < start and you leave step at 1, the signed span (end - start) / 1 is negative, clamps to 0, and you return [] when lodash would descend. When no step is passed, set it to start < end ? 1 : -1.i < end for a negative step. A direction-sensitive comparison silently produces [] for descending ranges, because 0 < -4 is false from the start. Compute the length once and loop i from 0 instead — the comparison disappears.Math.ceil((end - start) / step) can be negative (e.g. start = 5, end = 1, step = 1 gives -4). Passing a negative length to new Array(...) throws RangeError: Invalid array length. Math.max(…, 0) turns any impossible range into a clean empty array.(end - start) / 0 is Infinity (or NaN), and an infinite length would hang or throw. Guard step === 0 and return [] up front so the math never sees a zero divisor._.range — the ascending sibling. It's this exact function without the final .reverse(). Implementing one gives you the other for free: rangeRight(...args) is range(...args).reverse().range(0, 1, 0.25) → [0, 0.25, 0.5, 0.75]. The length formula already handles this, but start + i * step accumulates floating-point error for long ranges; production code multiplies by an index from a fresh integer rather than repeatedly adding step._.rangeStep patterns. The same length-first technique powers any "fill N evenly spaced values" helper — linspace in numerical libraries, tick generators in charting code. Once you compute the count up front, generating ascending or descending output is a one-line change.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.