How To Do Recursive Observable Call in RxJava? - java

I am quite new to RxJava (and Reactive paradigm in general), so please bear with me.
Suppose I have this News and this nested Comment data structure:
public class News {
public int id;
public int[] commentIds; //only top level comments
public News(int id, int[] commentIds) {
this.id = id;
this.commentIds = commentIds;
}
}
public class Comment {
public int id;
public int parentId; //ID of parent News or parent comment
public int[] childIds;
public Comment(int id, int parentId, int[] childIds) {
this.id = id;
this.parentId = parentId;
this.childIds = childIds;
}
}
and suppose I have this API endpoint:
getComments(int commentId) //return Observable<Comment> for Comment with ID commentId
Now, let's assume:
getComments(1); //will return Comment(1, 99, [3,4])
getComments(2); //will return Comment(2, 99, [5,6])
getComments(3); //will return Comment(3, 1, [])
getComments(4); //will return Comment(4, 1, [])
getComments(5); //will return Comment(5, 2, [])
getComments(6); //will return Comment(6, 2, [])
**
Now, if I have News n = News(99, [1,2]), how do I get all of its children comment recursively? i.e. to get comments with ID [1,2,3,4,5,6]?
**
I have searched and stumbled upon this: https://jkschneider.github.io/blog/2014/recursive-observables-with-rxjava.html
This is the recursion function:
public class FileRecursion {
static Observable<File> listFiles(File f) {
if(f.isDirectory())
return Observable.from(f.listFiles()).flatMap(FileRecursion::listFiles);
return Observable.just(f);
}
public static void main(String[] args) {
Observable.just(new File("/Users/joschneider/Desktop"))
.flatMap(FileRecursion::listFiles)
.subscribe(f -> System.out.println(f.getAbsolutePath()));
}
}
It shows an example on how to do recursive observable calls, but the inner function (f.listFiles()) is a blocking operation (doesn't return another Observable). In my case, the inner function (getComments) is a non-blocking function that returns another Observables. How do I do that?
Any help will be much appreciated.

This does practically the same thing described in the article:
Observable<Comment> getInnerComments(Comment comment) {
if (comment.childIds.length > 0)
return Observable.merge(
Observable.just(comment),
Observable.from(comment.childIds)
.flatMap(id -> getComments(id))
.flatMap(this::getInnerComments));
return Observable.just(comment);
}
public static void main(String[] args) {
getComments(1)
.flatMap(this::getInnerComments)
.subscribe(c -> System.out.println(comment.toString()));
}
I start with the comment with id = 1, then I pass it to getInnerComments(). The getInnerComments() checks if the comment has children. If it does, it iterates over every child id (Observable#from) and loads every child with your getComments(int) API. Then every child is passed to the getInnerComments() to do the same procedure. If a comment doesn't have children, it is immediately returned using Observable#just.
This is pseudo-code and it wasn't tested, but you should get the idea.
Below is an example of how to get all comments and then aggregate them to one List<Comment>.
getNews(99)
.flatMap(news -> Observable.from(news.commentIds))
.flatMap(commentId -> getComments(commentId))
.flatMap(comment -> getInnerComments(comment))
.toList()
.subscribe(commentList -> { });

Related

Convert sequential Monos to Flux

I have a web service where I want to retrieve the elements of a tree up to the root node.
I have a Webflux interface which returns a Mono on each call:
public interface WebService {
Mono<Node> fetchNode(String nodeId);
}
public class Node {
public String id;
public String parentId; // null, if parent node
}
Let's assume there is a tree
1
2 3
4 5
I want to create the following method:
public interface ParentNodeResolver {
Flux<Node> getNodeChain(String nodeId);
}
which would give me on getNodeChain(5) a Flux with the nodes for 5, 3 and 1 and then completes.
Unfortunately, I don't quite understand how I can combine Monos sequentially, but without blocking them. With Flux.generate(), I think I need to block on each mono to check whether it has a next element. Other methods which I've found seem to combine only a fixed number of Monos, but not in this recursive fashion.
Here is a sample code which would simulate the network request with some delay.
public class MonoChaining {
ExecutorService executorService = Executors.newFixedThreadPool(5);
#Test
void name() {
var nodeChain = generateNodeChainFlux("5")
.collectList()
.block();
assertThat(nodeChain).isNotEmpty();
}
private Flux<Node> generateNodeChainFlux(String nodeId) {
//TODO
return Flux.empty();
}
public Mono<Node> getSingleNode(String nodeId) {
var future =
CompletableFuture.supplyAsync(() -> {
try {
Thread.sleep(2000); // Simulate delay
if ("5".equals(nodeId)) {
return new Node("5", "3");
} else if ("3".equals(nodeId)) {
return new Node("3", "1");
} else if ("1".equals(nodeId)) {
return new Node("1", null);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}, executorService);
return Mono.fromFuture(future);
}
public static class Node {
public String id;
public String parentId;
public Node(String id, String parentId) {
this.id = id;
this.parentId = parentId;
}
}
}
Is there a way to retrieve this?
Thanks!
The operator you are looking for is Mono#expand. It is used for recursively expanding sequences. Read more here.
In your case:
private Flux<Node> generateNodeChainFlux(String nodeId) {
return getSingleNode(nodeId).expand(node -> getSingleNode(node.parentId));
}
Using recursion with flatMap to get parent node and concat to append the current node to resulting flux might work. Try the code below:
public Flux<Node> getNodeChain(String nodeId) {
return fetchNode(nodeId).flatMapMany(node -> {
if (node.parent != null) {
Flux<Node> nodeChain = getNodeChain(node.parent);
return Flux.concat(Flux.just(node), nodeChain);
}
return Flux.just(node);
});
}
Here I'm using flatMapMany to convert Mono to Flux.

Java Stream API with non-pure functions chain

Let's assume I have a class Person
public class Person {
private final String name;
private final int age;
private boolean rejected;
private String rejectionComment;
public void reject(String comment) {
this.rejected = true;
this.rejectionComment = comment;
}
// constructor & getters are ommited
}
and my app is something like that
class App {
public static void main(String[] args) {
List<Person> persons = Arrays.asList(
new Person("John", 10),
new Person("Sarah", 20),
new Person("Daniel", 30)
)
persons.forEach(p -> {
rejectIfYoungerThan15(p);
rejectIfNameStartsWithD(p);
// other rejection functions
}
}
private static void rejectIfYoungerThan15(Person p) {
if (!p.isRejected() && p.getAge() < 15) {
p.reject("Too young")
}
}
private static void rejectIfNameStartsWithD(Person p) {
if (!p.isRejected() && p.getName().startsWith("D")) {
p.reject("Name starts with 'D'")
}
}
// other rejection functions
}
The thing is I don't like that I have to perform !p.isRejected() check in every rejection function. Moreover, it doesn't make sense to pass an already rejected person to next filters.
So my idea is to use a mechanism of Stream.filter and make something like
persons.stream().filter(this::rejectIfYoungerThan15).filter(this::rejectIfNameStartsWithD)...
And change signature for these methods to return true if a passed Person has not been rejected and false otherwise.
But it seems to me that it's a very bad idea to use filter with non-pure functions.
Do you have any ideas of how to make it in more elegant way?
When you change the check functions to only check the condition (i.e. not to call p.isRejected()) and return boolean, you already made the necessary steps to short-circuit:
private static boolean rejectIfYoungerThan15(Person p) {
if(p.getAge() < 15) {
p.reject("Too young");
return true;
}
return false;
}
private static boolean rejectIfNameStartsWithD(Person p) {
if(p.getName().startsWith("D")) {
p.reject("Name starts with 'D'");
return true;
}
return false;
}
usable as
persons.forEach(p -> {
if(rejectIfYoungerThan15(p)) return;
if(rejectIfNameStartsWithD(p)) return;
// other rejection functions
}
}
A Stream’s filter operation wouldn’t do anything other than checking the returned boolean value and bail out. But depending on the Stream’s actual terminal operation the short-circuiting could go even farther and end up in not checking all elements, so you should not bring in a Stream operation here.
Calling these methods from lambda is fine, however, for better readability, you can rename these methods to show what they are doing and return boolean, e.g.:
private boolean hasEligibleAge(Person p){..}
private boolean hasValidName(Person p){..}
Another approach would be to wrap these methods into another method (to reflect the business logic/flow), e.g.:
private boolean isEligible(Person p){
//check age
//check name
}
You should make Person immutable, and let the reject-methods return a new Person. That will allow you to chain map-calls. Something like this:
public class Person {
private final String name;
private final int age;
private final boolean rejected;
private final String rejectionComment;
public Person reject(String comment) {
return new Person(name, age, true, comment);
}
// ...
}
class App {
// ...
private static Person rejectIfYoungerThan15(Person p) {
if (!p.isRejected() && p.getAge() < 15) {
return p.reject("Too young");
}
return p;
}
}
Now you can do this:
persons.stream()
.map(App::rejectIfYoungerThan15)
.map(App::rejectIfNameStartsWithD)
.collect(Collectors.toList());
If you want to remove rejected persons, you can add a filter after the mapping:
.filter(person -> !person.isRejected())
EDIT:
If you need to short circuit the rejections, you could compose your rejection functions into a new function and make it stop after the first rejection. Something like this:
/* Remember that the stream is lazy, so it will only call new rejections
* while the person isn't rejected.
*/
public Function<Person, Person> shortCircuitReject(List<Function<Person, Person>> rejections) {
return person -> rejections.stream()
.map(rejection -> rejection.apply(person))
.filter(Person::isRejected)
.findFirst()
.orElse(person);
}
Now your stream can look like this:
List<Function<Person, Person>> rejections = Arrays.asList(
App::rejectIfYoungerThan15,
App::rejectIfNameStartsWithD);
List<Person> persons1 = persons.stream()
.map(shortCircuitReject(rejections))
.collect(Collectors.toList());

Java 8 Lambda Expression validation

I was reading the article about validation using Predicates here. I am trying to implement it in Spring Boot framework where I am having some questions.
In the code:
public class LamdaPersonValidator implements PersonValidator {
public void validate(Person person) {
notNull.and(between(2, 12)).test(person.getFirstName()).throwIfInvalid("firstname");
notNull.and(between(4, 30)).test(person.getLastName()).throwIfInvalid("secondname");
notNull.and(between(3, 50)).and(contains("#")).test(person.getEmail()).throwIfInvalid("email");
intBetween(0, 110).test(person.getAge()).throwIfInvalid("age");
}
}
it is not mentioned on what could be the standard way to check if the person object in the validate method is itself is null. Is it OK to just put a null check like if(persone != null) { // notNull.and..} or there could be some better way to do null check.
Another thing is suppose, I want to do some custom checks like if person exists in the database or not. In this case, I need to connect to the database to check so. In this case, I need to Autowire the interface where static variable and method is not possible.
So, what could be best approach to use this when doing validation from the database?
We are not the code judges of the holy inquisition, so it’s not our duty to tell you, whether it is “OK to just put a null check”.
Of course, it is ok to write is as an ordinary if statement, like we did the last 25 years, just like it is ok to invent a verbose framework encapsulating the null check and bringing the term “lambda” somehow into it. The only remaining question would be if you really intent to write if(person != null) { /* do the checks */ }, in other words, allow a null person to pass the test.
In case, you want to reject null persons (which would be more reasonable), there is already a possibility to write it without an explicit test, Objects.requireNonNull, since Java 7, which demonstrates that you don’t need an “everything’s better with lambdas” framework to achieve that goal. Generally, you can write validating code reasonably with conventional code, contrary to the article’s example, utilizing simple tools like the && operator and putting common code into methods:
public void validate(Person person) {
Objects.requireNonNull(person, "person is null");
checkString(person.getFirstName(), "first name", 2, 12);
checkString(person.getLastName(), "last name", 4, 30);
checkString(person.getEmail(), "email", 3, 50);
if(!person.getEmail().contains("#"))
throw new IllegalArgumentException("invalid email format");
checkBounds(person.getAge(), "age", 0, 110);
}
private void checkString(String nameValue, String nameType, int min, int max) {
Objects.requireNonNull(nameValue, () -> nameType+" is null");
checkBounds(nameValue.length(), nameType, min, max);
}
private void checkBounds(int value, String valueType, int min, int max) {
if(value < min || value > max)
throw new IllegalArgumentException(valueType+" is not within ["+min+" "+max+']');
}
This does the same as your code, without any framework with “Lambda” in its name, still having readable validation code and allowing to reuse the checking code. That said, instead of a class name LamdaPersonValidator, which reflects how you implemented it, you should use class names reflecting the responsibilities of a class. Clearly, a validator responsible for validating some properties of an object should not get mixed up with a validator checking the presence of an entity in the database. The latter is an entirely different topic on its own and should also be in a question on its own.
The code above is only meant to be an example how to achieve the same as the original code. It should never appear in production code in this form, as it is a demonstration of a widespread anti-pattern, to apply arbitrary unreasonable constraints to properties, most likely invented by the programmer while writing the code.
Why does it assume that a person must have a first name and a last name and why does it assume that a first name must have at least two and at most twelve characters, while the last name must be between four and thirty characters?
It’s actually not even characters, as the association between char units and actual characters is not 1:1.
A must read for every programmer thinking about implementing name validation, is Falsehoods Programmers Believe About Names (With Examples).
Likewise, Wikipedia’s List of the verified oldest people lists one hundred people having an age above 110.
And there is no reason to assume that an email address can’t have more than fifty characters. A true validation of the correct Email pattern may turn out to be something to omit deliberately…
You can write GenericValidator like that also:
Write AbstractValidator class for common work:
public abstract class AbstractValidator {
private Map<Predicate, String> validatorMap = new LinkedHashMap<>();
protected List<String> messages;
public AbstractValidator() {
this.messages = new ArrayList<>();
}
protected <E> AbstractValidator add(Predicate<E> predicate, String reason) {
validatorMap.put(predicate, reason);
return this;
}
protected AbstractValidator apply(String fieldName, Object val) {
AtomicBoolean flag= new AtomicBoolean(true);
this.validatorMap.forEach((modifier, reason) -> {
if (flag.get() && !modifier.test(val)) {
String message = MessageFormat.format("{0} {1}", fieldName, reason);
messages.add(message);
flag.set(false);
}
});
this.validatorMap.clear();
return this;
}
public void end(String exceptionStatus) {
Optional.ofNullable(messages).filter(CollectionUtils::isEmpty)
.orElseThrow(() -> {
RuntimeException ex = new RuntimeException(exceptionStatus, messages);
messages.clear();
return ex;
});
}
}
Write GenericValidator class which will extend the AbstractValidator for your validation implementation:
public class GenericValidator extends AbstractValidator {
private GenericValidator() {
super();
}
public static GenericValidator of() {
return new GenericValidator();
}
public GenericValidator nonNull() {
add(Objects::nonNull, "Field value is null");
return this;
}
public GenericValidator notEmpty() {
add(StringUtils::isNotEmpty, "Field is empty");
return this;
}
public GenericValidator min(int min) {
add(s -> ((String) s).length() >= min, "Field min size is " + min);
return this;
}
public GenericValidator max(int max) {
add(s -> ((String) s).length() <= max, "Field max size is " + max);
return this;
}
public GenericValidator notEmptyList() {
add(CollectionUtils::isNotEmpty, "Field List is null/Empty");
return this;
}
public GenericValidator apply(String fieldName, Object val) {
return (GenericValidator) super.apply(fieldName, val);
}
}
Please test accordingly. Example for test cases:
class GenericValidatorTest {
#Test
void genericValidationSuccessCase() {
Abc abc = new Abc();
abc.setName("a");
abc.setVal(1);
abc.setAbslist(Collections.singletonList(new ChildAbc()));
GenericValidator of = GenericValidator.of();
of.nonNull().apply("abc", abc).end(GENERIC_JSON_SERIALIZATION);
of.notEmpty().min(1).max(1).apply("name", abc.getName())
.nonNull().apply("value", abc.getVal())
.notEmptyList().apply("childAbc", abc.getAbslist())
.end(GENERIC_JSON_SERIALIZATION);
}
#Test
void genericValidationWhenObjectNull() {
GenericValidator of = GenericValidator.of();
Assertions.assertThrows(BusinessException.class, () -> of.nonNull()
.apply("abc", null).end(GENERIC_JSON_SERIALIZATION));
}
#Test
void genericValidationWithExceptionInput() {
Abc abc = new Abc();
abc.setName("a");
abc.setVal(1);
GenericValidator of = GenericValidator.of();
of.nonNull().apply("abc", abc).end(GENERIC_JSON_SERIALIZATION);
GenericValidator apply = of.notEmpty().min(1).max(1).apply("name", abc.getName())
.nonNull().apply("value", abc.getVal())
.notEmptyList().apply("childAbc", abc.getAbslist());
Assertions.assertThrows(BusinessException.class, () -> apply.end(GENERIC_JSON_SERIALIZATION));
}
}
class Abc {
String name;
Integer val;
List<ChildAbc> abslist;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getVal() {
return val;
}
public void setVal(Integer val) {
this.val = val;
}
public List<ChildAbc> getAbslist() {
return abslist;
}
public void setAbslist(List<ChildAbc> abslist) {
this.abslist = abslist;
}
}
class ChildAbc {
String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}

How to implement recursive method properly?

I have a list and this is how it looks like:
I have an arraylist<XmlProperty> myList.
My myList looks like this Image, so myList.get(0) ist the first row, myList.get(1) the second row etc.
An important point is that there are myList-Elements which are for example of Type "AdressType", so there are "children"-elements.
For this myList the Relations look like:
Stage = 0 Stage = 1 Stage = 2
Adress
-------------City
-------------Street
---------------------------StreetName
---------------------------HouseNumber
---------------------------Suffix
-------------PostalCode
So elements of Stage=1 are the children of the elements of the element of Stage=0.
You see it in the column "ChildNr" which elements are the children of the certain element.
So I want to invoke all methods with the objectValues but I have to take care about elements which have children because before I invoke them I have to invoke the children at first.
I tried to implement it, but I cannot implement the recursion properly.
public void buildChildrenObjectsRecursively(Object object, int xmlNumber, int fieldNr, int objectLength) throws InvocationTargetException, IllegalAccessException {
//amount of children-elements
if (objectLength != 0) {
if (myList.get(fieldNr).isHasChild() == false) {
myList.get(fieldNr).getCreateMethod().invoke(object, myList.get(fieldNr).getInstance());
fieldNr++;
} else { //recursive call
int childLength = getLengthOfObject(myList.get(fieldNr).getInstance());
buildChildrenObjectsRecursively(myList.get(fieldNr).getInstance(), xmlNumber, fieldNr + 1, childLength);
myList.get(fieldNr).getCreateMethod().invoke(object, allXmls.get(xmlNumber).get(fieldNr).getInstance());
}
objectLength--;
}
}
getInstance() is the Object in column "ObjectValue".
So where is my mistake?
All I want to do is:
invoke method on object
if there are children , then invoke methods on children-elements first, after that invoke method on object
UPDATE
I have to clarify it.
All what I want to do is:
invoke method object
if there are childre, the invoke methods on children-element first. after that invoke on object
This means for our example in the picture:
List with the order {1, 2, 3, 4, 5, 6, 7} should be {2, 4, 5, 6, 3, 7, 1}.
So in this order I can invoke the methods with a Loop easily.
So how can I do this?
The data struct define, you need use lombok if you using the Stage class :
import lombok.Data;
#Data
public class Stage{
private Integer number;
private Integer stageNumber;
private List<Integer> childNumber;
public static final class StageBuilder {
private Integer number;
private Integer stageNumber;
private List<Integer> childNumber;
private StageBuilder() {
}
public static StageBuilder aStage() {
return new StageBuilder();
}
public StageBuilder withNumber(Integer number) {
this.number = number;
return this;
}
public StageBuilder withStageNumber(Integer stageNumber) {
this.stageNumber = stageNumber;
return this;
}
public StageBuilder withChildNumber(List<Integer> childNumber) {
this.childNumber = childNumber;
return this;
}
public Stage build() {
Stage stage = new Stage();
stage.setNumber(number);
stage.setStageNumber(stageNumber);
stage.setChildNumber(childNumber);
return stage;
}
}
}
The implement of the stage, just using java8 to sort by the stage number:
public class StageTest extends TestCase {
public void test() {
Stage stage1 = Stage.StageBuilder.aStage().withNumber(1).withStageNumber(0).withChildNumber(Arrays.asList(2, 3, 7)).build();
Stage stage2 = Stage.StageBuilder.aStage().withNumber(2).withStageNumber(1).build();
Stage stage3 = Stage.StageBuilder.aStage().withNumber(3).withStageNumber(1).withChildNumber(Arrays.asList(4, 5, 6)).build();
Stage stage4 = Stage.StageBuilder.aStage().withNumber(4).withStageNumber(2).build();
Stage stage5 = Stage.StageBuilder.aStage().withNumber(5).withStageNumber(2).build();
Stage stage6 = Stage.StageBuilder.aStage().withNumber(6).withStageNumber(2).build();
Stage stage7 = Stage.StageBuilder.aStage().withNumber(7).withStageNumber(1).build();
List<Stage> stageList = Arrays.asList(stage1, stage2, stage3, stage4, stage5, stage6, stage7);
stageList.sort((o1, o2) -> o2.getStageNumber() - o1.getStageNumber());
stageList.forEach(item -> System.out.println(item.getNumber()));
}
}

Java multidimensional array for navigation

I have entity AdminResource. Table has columns:
id | resource | path | parent | slug
1 Sport 1 0 sport
2 Football 1-2 1 sport-football
3 Estonia 1-2-3 2 sport-football-estonia
In my controller I get data
List<AdminResource> resources = resourceDAO.findAdminResources(user_id);
But I have problem now. I want to make new formated array/object with children items. Like this (by PHP, Javascript experience):
0: {
id: 1,
resource: Sport,
children: {
0: {
id: 2,
resource: Football,
children: {
id:....
}
}
}
}
Children can be very deep.
How I can create multidimensional array?
I this know in PHP and in Nodejs.
But in Java I have had a lot of errors.
Yes, I know about recursive logic. But...
I can't create with ArrayList, because I got error - key must be int.
I don't understand about HasMap, how I can create deep list.
I can't find similar examples in Google, maybe I can't understand its.
I can't understand how need work with multidimensional arrays/object in Java.
As finrod already explained, arrays are not what you are looking for. Judging by the {} syntax in your example, it looks more like a hierarchy of objects.
A tree seems to be a good option. As I said in my comment, a tree consists of nodes that hold a value (AdminResource in this case) and have children (a list of other nodes).
Here is a very basic example:
public static void main(String[] args)
{
List<AdminResource> resources = Arrays.asList(new AdminResource("Sport", Arrays.asList(1)),
new AdminResource("Football", Arrays.asList(1, 2)),
new AdminResource("Estonia", Arrays.asList(1, 2, 3)));
AdminNode root = new AdminNode(new AdminResource("ROOT", Collections.emptyList()));
resources.forEach(root::addResource);
for (AdminResource r : root)
{
System.out.println(r.getId());
}
}
public static class AdminNode
implements Iterable<AdminResource>
{
private AdminResource resource;
private List<AdminNode> children;
public AdminNode(AdminResource resource)
{
this.resource = resource;
this.children = new ArrayList<>();
}
public void addResource(AdminResource resource)
{
addResource(resource, new LinkedList<>(resource.getPath()));
}
private void addResource(AdminResource resource, Queue<Integer> path)
{
if (path.size() > 1)
{
Integer nextParent = path.poll();
for (AdminNode child : children)
{
if (child.getResource().getId().equals(nextParent))
{
child.addResource(resource, path);
}
}
}
else
{
children.add(new AdminNode(resource));
}
}
public AdminResource getResource() { return resource; }
#Override
public Iterator<AdminResource> iterator()
{
return stream().iterator();
}
public Stream<AdminResource> stream()
{
return goDown(this).skip(1).map(AdminNode::getResource);
}
private static Stream<AdminNode> goDown(AdminNode node)
{
return Stream.concat(Stream.of(node), node.children.stream().flatMap(AdminNode::goDown));
}
}
public static class AdminResource
{
private Integer id;
private String resource;
private List<Integer> path;
public AdminResource(String resource, List<Integer> path)
{
this.id = path.isEmpty() ? null : path.get(path.size() - 1);
this.resource = resource;
this.path = path;
}
public Integer getId() { return id; }
public List<Integer> getPath() { return path; }
}
The important class is AdminNode. You start with a dummy root node, which offers a method to add more AdminResources. That method recursively crawls down the path of the new resource, and finds the right place to add it. Similar methods can be written for removal or searching.
As I said, this a very basic example. It assumes that your list of resources is properly order. It ignores resources if the path to them is not existent yet. And so on...
But this should give you an idea of what trees are and how to start. They are used in a lot of places. A common usage is the component hierarchy of a User Interface, for example.
From what I can gather from the data you provided, I think what you want isn't a multidimensional array.
I think a tree or maybe an oriented graph if there can be multiple parents (same as the tree except a Node would have an array of parent nodes) is what you want.

Categories

Resources