The Lift

Another weekly challenge from CodeWars, The Lift. This time slightly more challenging, and took some time to get it right.

class Dinglemouse(object):

    def __init__(self, queues, capacity):
        self.queues = [list(x) for x in queues]
        self.capacity = capacity

    def theLift(self):
        stops = [0]
        elevator = []

        people_waiting = True

        while people_waiting:
            people_waiting = False

            # Going up
            for floor in range(0, len(self.queues)):
                stop_at_floor = False

                for person in elevator[:]:
                    if person == floor:
                        elevator.remove(person)
                        stop_at_floor = True

                for person in self.queues[floor][:]:
                    if person > floor:
                        stop_at_floor = True
                        if self.capacity > len(elevator):
                            elevator.append(person)
                            self.queues[floor].remove(person)
                        else:
                            people_waiting = True

                if stop_at_floor and not stops[-1] == floor:
                    stops.append(floor)

            # Going down
            for floor in range(len(self.queues) -1, -1, -1):
                stop_at_floor = False

                for person in elevator[:]:
                    if person == floor:
                        elevator.remove(person)
                        stop_at_floor = True

                for person in self.queues[floor][:]:
                    if person < floor:
                        stop_at_floor = True

                        if self.capacity > len(elevator):
                            elevator.append(person)
                            self.queues[floor].remove(person)
                        else:
                            people_waiting = True

                if stop_at_floor and not stops[-1] == floor:
                    stops.append(floor)

        if stops[-1] != 0:
            stops.append(0)

        return stops
# Floors:    G     1      2        3     4      5      6         Answers:
tests = [[ ( (),   (),    (5,5,5), (),   (),    (),    () ),     [0, 2, 5, 0]          ],
         [ ( (),   (),    (1,1),   (),   (),    (),    () ),     [0, 2, 1, 0]          ],
         [ ( (),   (3,),  (4,),    (),   (5,),  (),    () ),     [0, 1, 2, 3, 4, 5, 0] ],
         [ ( (),   (0,),  (),      (),   (2,),  (3,),  () ),     [0, 5, 4, 3, 2, 1, 0] ]]

assert(Dinglemouse(tests[0][0], 5).theLift() == tests[0][1])
assert(Dinglemouse(tests[1][0], 5).theLift() == tests[1][1])
assert(Dinglemouse(tests[2][0], 5).theLift() == tests[2][1])
assert(Dinglemouse(tests[3][0], 5).theLift() == tests[3][1])

Comparing Solution to others

My initial solution felt like spaghetti code, but after looking at others solutions I think it’s quite ok. What I feel would improve the code a lot would be to refactor it a bit to break out the nested conditionals into semantic operations, and in order to reduce code duplication.

One neat thing in order to be able to remove items from a list while iterating through it is to create an anonymous copy in order to avoid unexpected behaviour;

a_list = [1, 2, 3, 4, 5]

for item in a_list:
    a_list.remove(item)

print(a_list)
[2, 4]

I am not entirely sure what’s going on here, luckily it’s easily avoided;

a_list = [1, 2, 3, 4, 5]

for item in a_list[:]:
    a_list.remove(item)

print(a_list)
[]

Of course, for single line statements like this a list comprehension would be a better solution to filter out the items to keep, but in a more complex function like in the assignment, this trick helped.

class Dinglemouse_v2(object):

    def __init__(self, queues, capacity):
        self.queues = [list(x) for x in queues]
        self.capacity = capacity
        self.elevator = []
        self.people_waiting = True

    def theLift(self):
        """Main function called to execute the elevator."""

        stops = [0]

        while self.people_waiting:
            self.people_waiting = False

            stops = self._run_elevator(stops, 'up')
            stops = self._run_elevator(stops, 'down')

        if stops[-1] != 0:
            stops.append(0)

        return stops

    def _run_elevator(self, stops, direction):
        """Runs the elevator in one direction and returns updated list of all stops."""

        floor_order = range(len(self.queues) -1, -1, -1) if direction == 'down' else range(0, len(self.queues))

        for floor in floor_order:
            stop_to_let_people_off = self._people_getting_off(floor)
            stop_to_let_people_on = self._people_getting_on(floor, direction)

            if stop_to_let_people_off or stop_to_let_people_on and not stops[-1] == floor:
                stops.append(floor)

        return stops

    def _people_getting_off(self, floor):
        """Removes people waiting in elevator."""

        stop_at_floor = False

        for person in self.elevator[:]:
            if person == floor:
                self.elevator.remove(person)
                stop_at_floor = True

        return stop_at_floor

    def _people_getting_on(self, floor, direction):
        """Let new people onto the elevator."""

        stop_at_floor = False

        for person in self.queues[floor][:]:
            person_is_getting_on = person < floor if direction == 'down' else person > floor

            if person_is_getting_on:
                stop_at_floor = True

                if self.capacity > len(self.elevator):
                    self.elevator.append(person)
                    self.queues[floor].remove(person)
                else:
                    self.people_waiting = True

        return stop_at_floor
assert(Dinglemouse_v2(tests[0][0], 5).theLift() == tests[0][1])
assert(Dinglemouse_v2(tests[1][0], 5).theLift() == tests[1][1])
assert(Dinglemouse_v2(tests[2][0], 5).theLift() == tests[2][1])
assert(Dinglemouse_v2(tests[3][0], 5).theLift() == tests[3][1])

Final Thoughts

  • Still room for a lot of improvements. Especially I don’t like passing the historic stops to the run_elevator functions everytime, but it’s currently needed for it to have access to the last floor stopped on during last run up/down. The alternative would be to add the historic stops as an object variable, but since it should be reset everytime the elevator runs that doesn’t feel very clean either.
  • There are still nested conditionals that could be broken out into smaller functions, but unless there’s a reason to do it, I think that would be starting to verge on making the code too atomic.

Comments