Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

mapping list of string into hierarchical structure of objects

This is not a homework problem. This questions was asked to one of my friend in an interview test.

I have a list of lines read from a file as input. Each line has a identifier such as (A,B,NN,C,DD) at the start of line. Depending upon the identifier, I need to map the list of records into a single object A which contains a hierarchy structure of objects.

enter image description here

Description of Hierarchy : Each A can have zero or more B types. Each B identifier can have zero or more NN and C as child. Similarly each C segment can have zero or more NN and DD child. Abd each DD can have zero or more NN as child.

Mapping classes and their hierarchy:

All the class will have value to hold the String value from current line.

**A - will have list of B**

    class A {
        List<B> bList;
        String value;

        public A(String value) {
            this.value = value;
        }

        public void addB(B b) {
            if (bList == null) {
                bList = new ArrayList<B>();
            }
            bList.add(b);
        }
    }


**B - will have list of NN and list of C**

    class B {
            List<C> cList;
            List<NN> nnList;
            String value;
                public B(String value) {
                this.value = value;
            }
                public void addNN(NN nn) {
                if (nnList == null) {
                    nnList = new ArrayList<NN>();
                }
                nnList.add(nn);
            }
                public void addC(C c) {
                if (cList == null) {
                    cList = new ArrayList<C>();
                }
                cList.add(c);
            }
        }

**C - will have list of DDs and NNs**

    class C {
            List<DD> ddList;
            List<NN> nnList;
            String value;
            public C(String value) {
                this.value = value;
            }
            public void addDD(DD dd) {
                if (ddList == null) {
                    ddList = new ArrayList<DD>();
                }
                ddList.add(dd);
            }
            public void addNN(NN nn) {
                if (nnList == null) {
                    nnList = new ArrayList<NN>();
                }
                nnList.add(nn);
            }
        }

**DD - will have list of NNs**

    class DD {
            String value;
            List<NN> nnList;
            public DD(String value) {
                this.value = value;
            }
            public void addNN(NN nn) {
                if (nnList == null) {
                    nnList = new ArrayList<NN>();
                }
                nnList.add(nn);
            }
        }

**NN- will hold the line only**

    class NN {
        String value;

        public NN(String value) {
            this.value = value;
        }
    }

What I Did So Far :

The method public A parse(List<String> lines) reads the input list and returns the object A. Since, there might be multiple B, i have created separate method 'parseB to parse each occurrence.

At parseB method, loops through the i = startIndex + 1 to i < lines.size() and checks the start of lines. Occurrence of "NN" is added to current object of B. If "C" is detected at start, it calls another method parseC. The loop will break when we detect "B" or "A" at start.

Similar logic is used in parseC_DD.

public class GTTest {    
    public A parse(List<String> lines) {
        A a;
        for (int i = 0; i < lines.size(); i++) {
            String curLine = lines.get(i);
            if (curLine.startsWith("A")) { 
                a = new A(curLine);
                continue;
            }
            if (curLine.startsWith("B")) {
                i = parseB(lines, i); // returns index i to skip all the lines that are read inside parseB(...)
                continue;
            }
        }
        return a; // return mapped object
    }

    private int parseB(List<String> lines, int startIndex) {
        int i;
        B b = new B(lines.get(startIndex));
        for (i = startIndex + 1; i < lines.size(); i++) {
            String curLine = lines.get(i);
            if (curLine.startsWith("NN")) {
                b.addNN(new NN(curLine));
                continue;
            }
            if (curLine.startsWith("C")) {
                i = parseC(b, lines, i);
                continue;
            }
            a.addB(b);
            if (curLine.startsWith("B") || curLine.startsWith("A")) { //ending condition
                System.out.println("B A "+curLine);
                --i;
                break;
            }
        }
        return i; // return nextIndex to read
    }

    private int parseC(B b, List<String> lines, int startIndex) {

        int i;
        C c = new C(lines.get(startIndex));

        for (i = startIndex + 1; i < lines.size(); i++) {
            String curLine = lines.get(i);
            if (curLine.startsWith("NN")) {
                c.addNN(new NN(curLine));
                continue;
            }           

            if (curLine.startsWith("DD")) {
                i = parseC_DD(c, lines, i);
                continue;
            }

            b.addC(c);
            if (curLine.startsWith("C") || curLine.startsWith("A") || curLine.startsWith("B")) {
                System.out.println("C A B "+curLine);
                --i;
                break;
            }
        }
        return i;//return next index

    }

    private int parseC_DD(C c, List<String> lines, int startIndex) {
        int i;
        DD d = new DD(lines.get(startIndex));
        c.addDD(d);
        for (i = startIndex; i < lines.size(); i++) {
            String curLine = lines.get(i);
            if (curLine.startsWith("NN")) {
                d.addNN(new NN(curLine));
                continue;
            }
            if (curLine.startsWith("DD")) {
                d=new DD(curLine);
                continue;
            }       
            c.addDD(d);
            if (curLine.startsWith("NN") || curLine.startsWith("C") || curLine.startsWith("A") || curLine.startsWith("B")) {
                System.out.println("NN C A B "+curLine);
                --i;
                break;
            }

        }
        return i;//return next index

    }
public static void main(String[] args) {
        GTTest gt = new GTTest();
        List<String> list = new ArrayList<String>();
        list.add("A1");
        list.add("B1");
        list.add("NN1");
        list.add("NN2");
        list.add("C1");
        list.add("NNXX");
        list.add("DD1");
        list.add("DD2");
        list.add("NN3");
        list.add("NN4");
        list.add("DD3");
        list.add("NN5");
        list.add("B2");
        list.add("NN6");
        list.add("C2");
        list.add("DD4");
        list.add("DD5");
        list.add("NN7");
        list.add("NN8");
        list.add("DD6");
        list.add("NN7");
        list.add("C3");
        list.add("DD7");
        list.add("DD8");
        A a = gt.parse(list);
            //show values of a 

    }
}

My logic is not working properly. Is there any other approach you can figure out? Do you have any suggestions/improvements to my way?

like image 874
gtiwari333 Avatar asked Mar 28 '12 19:03

gtiwari333


2 Answers

Use hierarchy of objects:


    public interface Node {
        Node getParent();
        Node getLastChild();
        boolean addChild(Node n);
        void setValue(String value);
        Deque  getChildren();
    }

    private static abstract class NodeBase implements Node {
        ...     
        abstract boolean canInsert(Node n);    
        public String toString() {
            return value;
        }
        ...    
    }

    public static class A extends NodeBase {
        boolean canInsert(Node n) {
            return n instanceof B;
        }
    }
    public static class B extends NodeBase {
        boolean canInsert(Node n) {
            return n instanceof NN || n instanceof C;
        }
    }

    ...

    public static class NN extends NodeBase {
        boolean canInsert(Node n) {
            return false;
        }
    }

Create a tree class:

public class MyTree {

    Node root;
    Node lastInserted = null;

    public void insert(String label) {
        Node n = NodeFactory.create(label);

        if (lastInserted == null) {
            root = n;
            lastInserted = n;
            return;
        }
        Node current = lastInserted;
        while (!current.addChild(n)) {
            current = current.getParent();
            if (current == null) {
                throw new RuntimeException("Impossible to insert " + n);
            }
        }
        lastInserted = n;
    }
    ...
}

And then print the tree:


public class MyTree {
    ...
    public static void main(String[] args) {
        List input;
        ...
        MyTree tree = new MyTree();
        for (String line : input) {
            tree.insert(line);
        }
        tree.print();
    }

    public void print() {
        printSubTree(root, "");
    }
    private static void printSubTree(Node root, String offset) {
        Deque  children = root.getChildren();
        Iterator i = children.descendingIterator();
        System.out.println(offset + root);
        while (i.hasNext()) {
            printSubTree(i.next(), offset + " ");
        }
    }
}
like image 96
Aleksey Otrubennikov Avatar answered Oct 23 '22 05:10

Aleksey Otrubennikov


A mealy automaton solution with 5 states: wait for A, seen A, seen B, seen C, and seen DD.

The parse is done completely in one method. There is one current Node that is the last Node seen except the NN ones. A Node has a parent Node except the root. In state seen (0), the current Node represents a (0) (e.g. in state seen C, current can be C1 in the example above). The most fiddling is in state seen DD, that has the most outgoing edges (B, C, DD, and NN).

public final class Parser {
    private final static class Token { /* represents A1 etc. */ }
    public final static class Node implements Iterable<Node> {
        /* One Token + Node children, knows its parent */
    }

    private enum State { ExpectA, SeenA, SeenB, SeenC, SeenDD, }

    public Node parse(String text) {
        return parse(Token.parseStream(text));
    }

    private Node parse(Iterable<Token> tokens) {
        State currentState = State.ExpectA;
        Node current = null, root = null;
        while(there are tokens) {
            Token t = iterator.next();
            switch(currentState) {
                /* do stuff for all states */

                /* example snippet for SeenC */
                case SeenC:
                if(t.Prefix.equals("B")) {
                    current.PN.PN.AddChildNode(new Node(t, current.PN.PN));
                    currentState = State.SeenB;
                } else if(t.Prefix.equals("C")) {

            }
        }
        return root;
    }
}

I'm not satisfied with those trainwrecks to go up the hierarchy to insert a Node somewhere else (current.PN.PN). Eventually, explicit state classes would make the private parse method more readable. Then, the solution gets more akin to the one provided by @AlekseyOtrubennikov. Maybe a straight LL approach yields code that is more beautiful. Maybe best to just rephrase the grammar to a BNF one and delegate parser creation.


A straightforward LL parser, one production rule:
// "B" ("NN" || C)*
private Node rule_2(TokenStream ts, Node parent) {
    // Literal "B"
    Node B = literal(ts, "B", parent);
    if(B == null) {
        // error
        return null;
    }

    while(true) {
        // check for "NN"
        Node nnLit = literal(ts, "NN", B);
        if(nnLit != null)
            B.AddChildNode(nnLit);

        // check for C
        Node c = rule_3(ts, parent);
        if(c != null)
            B.AddChildNode(c);

        // finished when both rules did not match anything
        if(nnLit == null && c == null)
            break;
    }

    return B;
}

TokenStream enhances Iterable<Token> by allowing to lookahead into the stream - LL(1) because parser must choose between literal NN or deep diving in two cases (rule_2 being one of them). Looks nice, however, missing some C# features here...

like image 3
Stefan Hanke Avatar answered Oct 23 '22 05:10

Stefan Hanke