Skip to the code
cf.

Move an item in a list

Write moveItem(array, from, to). It returns a new array in which the item at index from has moved, and the items between from and to have shifted over by one to make room, the way a list reorders when you drag an item to a new place.

After the move, the item is at index to of the new array, whether it moved down the list (to greater than from) or up.

Negative indexes count back from the end, as they do with at: -1 is the last item. If from or to isn't an integer, or points outside the array, throw a RangeError.

array itself never changes, and the result is always a new array, even when from and to are the same.

const list = ["a", "b", "c", "d"];
moveItem(list, 0, 2);  // ["b", "c", "a", "d"]
moveItem(list, 3, 1);  // ["a", "d", "b", "c"]
moveItem(list, 0, -1); // ["b", "c", "d", "a"]
moveItem(list, 4, 0);  // throws a RangeError
list;                  // ["a", "b", "c", "d"]

What the tests check

  1. moves an item down the list
    expect(moveItem(["a", "b", "c", "d"], 0, 2)).toEqual(["b", "c", "a", "d"]);
    expect(moveItem(["a", "b", "c", "d"], 1, 3)).toEqual(["a", "c", "d", "b"]);
    
  2. moves an item up the list
    expect(moveItem(["a", "b", "c", "d"], 3, 1)).toEqual(["a", "d", "b", "c"]);
    expect(moveItem(["a", "b", "c", "d"], 2, 0)).toEqual(["c", "a", "b", "d"]);
    
  3. leaves the item at index to, for every pair of indexes
    const list = ["a", "b", "c", "d", "e"];
    for (let from = 0; from < list.length; from++) {
      for (let to = 0; to < list.length; to++) {
        const moved = moveItem(list, from, to);
        expect(moved).toHaveLength(5);
        expect(moved[to]).toBe(list[from]);
        expect(moved.filter((item) => item !== list[from])).toEqual(list.filter((item) => item !== list[from]));
      }
    }
    
  4. returns an equal copy when from and to are the same
    const list = ["a", "b", "c"];
    const moved = moveItem(list, 1, 1);
    expect(moved).toEqual(["a", "b", "c"]);
    expect(moved).not.toBe(list);
    
  5. counts negative indexes back from the end
    const list = ["a", "b", "c", "d"];
    expect(moveItem(list, -1, 0)).toEqual(["d", "a", "b", "c"]);
    expect(moveItem(list, 0, -1)).toEqual(["b", "c", "d", "a"]);
    expect(moveItem(list, 1, -2)).toEqual(["a", "c", "b", "d"]);
    
  6. throws a RangeError for an index outside the array
    const calls = [[["a", "b"], 2, 0], [["a", "b"], 0, 2], [["a", "b"], -3, 0], [["a", "b"], 0, -3], [[], 0, 0]];
    for (const [list, from, to] of calls) {
      let thrown;
      try {
        moveItem(list, from, to);
      } catch (error) {
        thrown = error;
      }
      // The page stops a loop that never ends with an InfiniteLoopError, which is a RangeError too, so report it here.
      if (thrown?.name === "InfiniteLoopError") throw new Error("moveItem(" + JSON.stringify(list) + ", " + from + ", " + to + ") never finished: " + thrown.message);
      expect(thrown).toBeInstanceOf(RangeError);
    }
    
  7. throws a RangeError for an index that isn't an integer
    const list = ["a", "b"];
    for (const [from, to] of [[0.5, 1], [0, NaN], ["1", 0]]) {
      let thrown;
      try {
        moveItem(list, from, to);
      } catch (error) {
        thrown = error;
      }
      // The page stops a loop that never ends with an InfiniteLoopError, which is a RangeError too, so report it here.
      if (thrown?.name === "InfiniteLoopError") throw new Error("moveItem(" + JSON.stringify(list) + ", " + JSON.stringify(from) + ", " + to + ") never finished: " + thrown.message);
      expect(thrown).toBeInstanceOf(RangeError);
    }
    
  8. leaves the original array alone
    const first = { id: 1 };
    const list = [first, { id: 2 }, { id: 3 }];
    const moved = moveItem(list, 0, 2);
    expect(list.map((item) => item.id)).toEqual([1, 2, 3]);
    expect(moved.map((item) => item.id)).toEqual([2, 3, 1]);
    expect(moved[2]).toBe(first);
    

Learn more

solution.jsSaved in this browser

Run tests runs every test on your code, here in your browser. The problem is solved when they all pass.

Ctrl + Enter runs the tests (⌘ + Enter on a Mac).