5

Eventhough I write in python I think the abstract concept is more interesting to me and others. So pseudocode please if you like :)

I have a list with items from one of my classes. Lets do it with strings and numbers here, it really doesn't matter. Its nested to any depth. (Its not really a list but a container class which is based on a list.)

Example: [1, 2, 3, ['a', 'b', 'c'] 4 ['d', 'e', [100, 200, 300]] 5, ['a', 'b', 'c'], 6]

Note that both ['a', 'b', 'c'] are really the same container. If you change one you change the other. The containers and items can be edited, items inserted and most important containers can be used multiple times. To avoid redundancy its not possible to flatten the list (I think!) because you loose the ability to insert items in one container and it automatically appears in all other containers.

The Problem: For the frontend (just commandline with the python "cmd" module) I want to navigate through this structure with a cursor which always points to the current item so it can be read or edited. The cursor can go left and right (users point of view) and should behave like the list is not a nested list but a flat one.

For a human this is super easy to do. You just pretend that in this list above the sublists don't exist and simply go from left to right and back.

For example if you are at the position of "3" in the list above and go right you get 'a' as next item, then 'b', 'c', and then "4" etc. Or if you go right from the "300" you get the "5" next.

And backwards: If you go left from "6" the next is 'c'. If you go left from "5" its "300".

So how do I do that in principle? I have one approach here but its wrong and the question is already so long that I fear most people will not read it :(. I can post it later.

P.S. Even if its hard to resist: The answer to this question is not "Why do you want to do this, why do you organize your data this way, why don't you [flatten the list| something out of my imagination] first? The problem is exactly what I've described here, nothing else. The data is structured by the nature of the problem this way.

General Grievance
  • 4,555
  • 31
  • 31
  • 45
nilshi
  • 504
  • 1
  • 6
  • 16
  • You're more likely to get responses if you post some code that doesn't work and ask more specific questions. – SteveMc Jul 01 '11 at 14:29
  • It became clear that the problem is not moving right but moving back left. In the above example going back from "5" should be 300, not to the start of the container, which is "d". – nilshi Jul 01 '11 at 15:43
  • @rolf, what problem do you have when moving left? I implemented a stack-based approach for the fun of it as well, but a tricky problem caused my `left` method to fail -- negative indices are still in range if you use exception testing, so it was iterating backwards over inner lists twice! (i.e. `l[2], l[1], l[0], l[-1], l[-2], l[-3]`). Perhaps that's your problem as well? – senderle Jul 01 '11 at 17:15
  • 1
    @rolf, in any case, I've pasted the working implementation [here](http://ideone.com/qvcqJ). – senderle Jul 01 '11 at 17:22

4 Answers4

3

One solution would be to store current index and/or depth information and use it to traverse the nested list. But that seems like a solution that would do a lot of complicated forking -- testing for ends of lists, and so on. Instead, I came up with a compromise. Instead of flattening the list of lists, I created a generator that creates a flat list of indices into the list of lists:

def enumerate_nested(nested, indices):
    for i, item in enumerate(nested):
        if isinstance(item, collections.Iterable) and not isinstance(item, basestring):
            for new_indices in enumerate_nested(item, indices + (i,)):
                yield new_indices
        else:
            yield indices + (i,)

Then a simple function that extracts an innermost item from the list of lists based on an index tuple:

def tuple_index(nested_list, index_tuple):
    for i in index_tuple:
        nested_list = nested_list[i]
    return nested_list

Now all you have to do is traverse the flat index list, in whatever way you like.

>>> indices = list(enumerate_nested(l, tuple()))
>>> print l
[1, 2, 3, ['a', 'b', 'c'], 4, ['d', 'e', [100, 200, 300]], 5, ['a', 'b', 'c'], 6]
>>> for i in indices:
...     print tuple_index(l, i),
... 
1 2 3 a b c 4 d e 100 200 300 5 a b c 6

Since this answer was accepted for the stack-based solution that I posted on ideone in the comments, and since it's preferable not to use external pastebins for answer code, please note that this answer also contains my stack-based solution.

Community
  • 1
  • 1
senderle
  • 145,869
  • 36
  • 209
  • 233
  • 1
    @rolf, I implemented a stack-based version as well, but rather than create a bloated answer, I've pasted it [here](http://ideone.com/qvcqJ). – senderle Jul 01 '11 at 17:21
  • @mac, seems I mixed you up with Cosmologicon, sorry -- thanks again :) – senderle Jul 01 '11 at 17:46
  • I just began to read the stack based code. Thanks already! What I don't get yet is how to move forward or backward only when the user wants it manually (in the interactive program mode). Or in other words, non-interactive: start the program, move one right, execute some other code, then move to the next position (or back) – nilshi Jul 01 '11 at 17:55
  • @rolf, `right()` moves the "cursor" one to the right, and `left()` moves it one to the left. They return True if the move is successful, False if not. Then you can call `get_item()` to get the item at the current location. Does that answer your question? – senderle Jul 01 '11 at 19:39
  • ah, get_item after that, of course. Yes, thank you very much. – nilshi Jul 01 '11 at 20:02
  • All thats left is deleting the item under the current cursor (and thus if its part of the same container thats in usage elsewhere deleting it there, too). And inserting after/before a new item after the cursor position. I think I am just to tired now... :) – nilshi Jul 01 '11 at 23:27
3

I'd let the cursor have a stack of the indices of the arrays.

Examples:

[1, 2, 3, ['a', 'b', 'c'], 4 ]

If the cursor is at the 1 (at index 0), the cursor's position is [0].

It the cursor is at the 2 (at index 1), the cursor's position is [1].

If the cursor is at the 'a' (at index 3 at the outmost level and index 0 at the second level), the cursor's position would be [3, 0].

If the cursor is at the 'b' (at index 3 at the outmost level and index 1 at the second level), the cursor's position would be [3, 1].

etc.

To move the cursor right, you just increase the rightmost index in the position. So when you go from 'b' to 'c' it will increase from [3, 1] to [3, 2]. If the index then becomes out of range, you pop the rightmost value from the index stack and increase the rightmost value. So if you go from 'c' to 4, you go from [3, 2] to [4].

When moving, if you encounter a position with a nested array, you enter it. So if you go right from 3 (at index [2]), you enter the first element in the array ['a','b','c'], which is at index [3, 0].

Moving left would just do the inverse of moving right.

Martin Vilcans
  • 5,428
  • 5
  • 22
  • 17
  • Moving left is not the inverse but if you move from "4" index:[4] one step left it is not [3,0] but [3,2]. How is the movement then? – nilshi Jul 01 '11 at 15:40
  • You do have to account for exiting lists like `[['d', 'e', [100, 200, 300]], 4]`. When moving into the 4, your index stack would go from `0, 2, 2` to `1`. So really, you should say "*While* the index is out of range, pop the rightmost value from the index stack and increase the rightmost value." – Ponkadoodle Jul 01 '11 at 15:41
  • @Martin How do you handle moving left into a nested list? – Judge Maygarden Jul 01 '11 at 15:45
  • @rolf -- If I understand what you're asking, here's some pseudo-python for moving left: `stack.top -= 1; while is_sequence(item[stack]): stack.push(len(item) - 1)` – senderle Jul 01 '11 at 15:51
1

Essentially I would base my own solution on recursion. I would extend the container class with the following:

  1. cursor_position - Property that stores the index of the highlighted element (or the element that contains the element that contains the highlighted element, or any level of recursion beyond that).
  2. repr_with_cursor - This method should return a printable version of the container's content, already highlighting the item currently selected.
  3. mov_right - This method should be invoked when the cursor move right. Returns the new index of the cursor within the element or None if the cursor falls "outside" the current container (if you move past hte last element in the container.
  4. mov_left - Idem, but towards the left.

The way the recursion should work, is that for each method, depending of the type of the highlighted method you should have two different behaviours:

  • if the cursor is on a container it should invoke the method of the "pointed" container.
  • if the cursor is on a non-container it should perform the 'real thing'.

EDIT I had a spare half an hour so I threw together an example class that implements my idea. It' not feature complete (for example it doesn't handle well when it reaches the either end of the largest container, and requires each instance of the class to be used only once in the largest sequence) but it works enough to demonstrate the concept. I shall repeat before people comments on that: this is proof-of-concept code, it's not in any way ready to be used!

#!/usr/bin/env python
# -*- coding: utf-8  -*-

class C(list):

    def __init__(self, *args):
        self.cursor_position = None
        super(C, self).__init__(*args)

    def _pointed(self):
        '''Return currently pointed item'''
        if self.cursor_position == None:
            return None
        return self[self.cursor_position]

    def _recursable(self):
        '''Return True if pointed item is a container [C class]'''
        return (type(self._pointed()) == C)

    def init_pointer(self, end):
        '''
        Recursively set the pointers of containers in a way to point to the
        first non-container item of the nested hierarchy.
        '''
        assert end in ('left', 'right')
        val = 0 if end == 'left' else len(self)-1
        self.cursor_position = val
        if self._recursable():
            self.pointed._init_pointer(end)

    def repr_with_cursor(self):
        '''
        Return a representation of the container with highlighted item.
        '''
        composite = '['
        for i, elem in enumerate(self):
            if type(elem) == C:
                composite += elem.repr_with_cursor()
            else:
                if i != self.cursor_position:
                    composite += str(elem)
                else:
                    composite += '**' + str(elem) + '**'
            if i != len(self)-1:
                composite += ', '
        composite += ']'
        return composite

    def mov_right(self):
        '''
        Move pointer to the right.
        '''
        if self._recursable():
            if self._pointed().mov_right() == -1:
                if self.cursor_position != len(self)-1:
                    self.cursor_position += 1
        else:
            if self.cursor_position != len(self)-1:
                self.cursor_position += 1
                if self._recursable():
                    self._pointed().init_pointer('left')
            else:
                self.cursor_position = None
                return -1

    def mov_left(self):
        '''
        Move pointer to the left.
        '''
        if self._recursable():
            if self._pointed().mov_left() == -1:
                if self.cursor_position != 0:
                    self.cursor_position -= 1
        else:
            if self.cursor_position != 0:
                self.cursor_position -= 1
                if self._recursable():
                    self._pointed().init_pointer('right')
            else:
                self.cursor_position = None
                return -1

A simple test script:

# Create the nested structure
LevelOne = C(('I say',))
LevelTwo = C(('Hello', 'Bye', 'Ciao'))
LevelOne.append(LevelTwo)
LevelOne.append('!')
LevelOne.init_pointer('left')
# The container's content can be seen as both a regualar list or a
# special container.
print(LevelOne)
print(LevelOne.repr_with_cursor())
print('---')
# Showcase the effect of moving the cursor to right
for i in range(5):
    print(LevelOne.repr_with_cursor())
    LevelOne.mov_right()
print('---')
# Showcase the effect of moving the cursor to left
LevelOne.init_pointer('right')
for i in range(5):
    print(LevelOne.repr_with_cursor())
    LevelOne.mov_left()

It outputs:

['I say', ['Hello', 'Bye', 'Ciao'], '!']
[**I say**, [Hello, Bye, Ciao], !]
---
[**I say**, [Hello, Bye, Ciao], !]
[I say, [**Hello**, Bye, Ciao], !]
[I say, [Hello, **Bye**, Ciao], !]
[I say, [Hello, Bye, **Ciao**], !]
[I say, [Hello, Bye, Ciao], **!**]
---
[I say, [Hello, Bye, Ciao], **!**]
[I say, [Hello, Bye, **Ciao**], !]
[I say, [Hello, **Bye**, Ciao], !]
[I say, [**Hello**, Bye, Ciao], !]
[**I say**, [Hello, Bye, Ciao], !]

Fun problem! My favourite OS question of the day! :)

mac
  • 42,153
  • 26
  • 121
  • 131
1

While I like the idea of flattening the index list, that makes it impossible to modify the length of any sublist while iterating through the nested list. If this is not functionality you need, I would go with that.

Otherwise I would also implement a pointer into the list as a tuple of indices, and rely on recursion. To get you started, here's a class that implements right() and reading the value of the pointer via deref(). (I'm using None to represent a pointer beyond the end of a list.) I'll leave it as an exercise how to implement left() and assigning to elements. And you'll have to decide what behavior you want if you replace the element you're currently pointing at with another list. Good luck!

def islist(seq):
    return isinstance(seq, (list, tuple))

class nav:
    def __init__(self, seq):
        self.seq = seq
        self.ptr = self.first()
    def __nonzero__(self):
        return bool(self.ptr)
    def right(self):
        """Advance the nav to the next position"""
        self.ptr = self.next()
    def first(self, seq=None):
        """pointer to the first element of a (possibly empty) sequence"""
        if seq is None: seq = self.seq
        if not islist(seq): return ()
        return (0,) + self.first(seq[0]) if seq else None
    def next(self, ptr=None, seq=None):
        """Return the next pointer"""
        if ptr is None: ptr = self.ptr
        if seq is None: seq = self.seq
        subnext = None if len(ptr) == 1 else self.next(ptr[1:], seq[ptr[0]])
        if subnext is not None: return (ptr[0],) + subnext
        ind = ptr[0]+1
        return None if ind >= len(seq) else (ind,) + self.first(seq[ind])
    def deref(self, ptr=None, seq=None):
        """Dereference given pointer"""
        if ptr is None: ptr = self.ptr
        if seq is None: seq = self.seq
        if not ptr: return None
        subseq = seq[ptr[0]]
        return subseq if len(ptr) == 1 else self.deref(ptr[1:], subseq)

abc = ['a', 'b', 'c']
a = [1, 2, 3, abc, 4, ['d', 'e', [100, 200, 300]], 5, abc, 6]
n = nav(a)

while n:
    print n.ptr, n.deref()
    n.right()
Cosmologicon
  • 2,127
  • 1
  • 16
  • 18