I hope someone can help me with this...
Let's say I have this basic bookmark class:
public class FlatBookmark
{
public int PageIndex { get; set; }
public string Path { get; set; }
}
And a 'flat' list of bookmarks:
List<FlatBookmark> flatBookmarks = new List<FlatBookmark>()
{
new FlatBookmark() { Path = "Category 1||Page 1", PageIndex = 0 },
new FlatBookmark() { Path = "Category 1||Page 1||Attachment 1", PageIndex = 1 },
new FlatBookmark() { Path = "Category 1||Page 1||Attachment 2", PageIndex = 2 },
new FlatBookmark() { Path = "Category 1||Page 2", PageIndex = 3 },
new FlatBookmark() { Path = "Category 1||Page 2", PageIndex = 4 }, // Ignore (path already exists)
new FlatBookmark() { Path = "Category 1||Page 2", PageIndex = 5 }, // Ignore (path already exists)
new FlatBookmark() { Path = "Category 1||Page 3", PageIndex = 6 },
new FlatBookmark() { Path = "", PageIndex = 123 }, // Empty or null paths should be completely ignored
new FlatBookmark() { Path = null, PageIndex = 321 }, // Empty or null paths should be completely ignored
new FlatBookmark() { Path = "Category 2||Page 1", PageIndex = 7 },
new FlatBookmark() { Path = "Category 2||Page 2", PageIndex = 8 },
new FlatBookmark() { Path = "Category 1||Page 1||Attachment 1", PageIndex = 9 }, // Create a new 'Category1' root, because it is separated by the previous one
new FlatBookmark() { Path = "Category 1||Page 1||Attachment 1", PageIndex = 10 }, // Ignore (path already exists)
new FlatBookmark() { Path = "Category 1||Page 1||Attachment 2", PageIndex = 11 },
};
I now want to populate a new List<Bookmark>
with nested bookmarks, splitted on any given string of the Path, and where the Title becomes the last part of the Path.
public class Bookmark
{
public int PageIndex { get; set; }
public string Title { get; set; }
public List<Bookmark> Bookmarks; // Nested children
}
Like this:
Category 1: PageIndex=0
Page 1: PageIndex=0
Attachment 1: PageIndex=1
Attachment 2: PageIndex=2
Page 2: PageIndex=3
Page 3: PageIndex=6
Category 2: PageIndex=7
Page 1: PageIndex=7
Page 2: PageIndex=8
Category 1: PageIndex=9
Page 1: PageIndex=9
Attachment 1: PageIndex=9
Attachment 2: PageIndex=11
I created a Unit test for it:
// Category 1
Assert.AreEqual("Category 1", bookmarks[0].Title);
Assert.AreEqual(0, bookmarks[0].PageIndex);
Assert.AreEqual("Page 1", bookmarks[0].Bookmarks[0].Title);
Assert.AreEqual(0, bookmarks[0].Bookmarks[0].PageIndex);
Assert.AreEqual("Attachment 1", bookmarks[0].Bookmarks[0].Bookmarks[0].Title);
Assert.AreEqual(1, bookmarks[0].Bookmarks[0].Bookmarks[0].PageIndex);
Assert.AreEqual("Attachment 2", bookmarks[0].Bookmarks[0].Bookmarks[1].Title);
Assert.AreEqual(2, bookmarks[0].Bookmarks[0].Bookmarks[1].PageIndex);
Assert.AreEqual("Page 2", bookmarks[0].Bookmarks[1].Title);
Assert.AreEqual(3, bookmarks[0].Bookmarks[1].PageIndex);
Assert.AreEqual("Page 3", bookmarks[0].Bookmarks[2].Title);
Assert.AreEqual(6, bookmarks[0].Bookmarks[2].PageIndex);
// Category 2
Assert.AreEqual("Category 2", bookmarks[1].Title);
Assert.AreEqual(7, bookmarks[1].PageIndex);
Assert.AreEqual("Page 1", bookmarks[1].Bookmarks[0].Title);
Assert.AreEqual(7, bookmarks[1].Bookmarks[0].PageIndex);
Assert.AreEqual("Page 2", bookmarks[1].Bookmarks[1].Title);
Assert.AreEqual(8, bookmarks[1].Bookmarks[1].PageIndex);
// Category 1 again (not combined with the first one, because there was another category in between)
Assert.AreEqual("Category 1", bookmarks[2].Title);
Assert.AreEqual(9, bookmarks[2].PageIndex);
Assert.AreEqual("Page 1", bookmarks[2].Bookmarks[0].Title);
Assert.AreEqual(9, bookmarks[2].Bookmarks[0].PageIndex);
Assert.AreEqual("Attachment 1", bookmarks[2].Bookmarks[0].Bookmarks[0].Title);
Assert.AreEqual(9, bookmarks[2].Bookmarks[0].Bookmarks[0].PageIndex);
Assert.AreEqual("Attachment 2", bookmarks[2].Bookmarks[0].Bookmarks[1].Title);
Assert.AreEqual(11, bookmarks[2].Bookmarks[0].Bookmarks[1].PageIndex);
I'm stuck on the recursive part (I think...), I can loop through each FlatBookmark
, and split the path (string[] parts = bookmark.Split('/')
), and then for each part I want to somehow look back if it has any parents, or should have any parent, and if not: create them...
Can someone perhaps point me in the right direction of how to create a method like this?
It should basically have no limit on the amount of nested elements...
--- UPDATE ---
--- SOLUTION ---
This is the solution I came up with:
public List<Bookmark> CreateNestedBookmarks(List<FlatBookmark> flatBookmarks, string separator = "/")
{
// Create a 'root' bookmark list (happens for every nested level, with recursion)
var bookmarks = new List<Bookmark>();
foreach (var flatBookmark in flatBookmarks)
{
if(!string.IsNullOrWhiteSpace(flatBookmark.Path))
{
// Only use the title
string title = flatBookmark.Path.Split(new string[] { separator }, StringSplitOptions.None)[0];
bool exists = bookmarks.LastOrDefault()?.Title == title;
// Add the bookmark to the list if it's not already added before and not empty/null
if (!exists && !string.IsNullOrWhiteSpace(title))
{
bookmarks.Add(new Bookmark()
{
Title = title,
PageIndex = flatBookmark.PageIndex
});
}
}
}
// Fetch the nested bookmarks for each 'root'-bookmark
foreach(var bookmark in bookmarks)
{
bool found = false;
List<FlatBookmark> childBookmarks = new List<FlatBookmark>();
foreach (var flatBookmark in flatBookmarks)
{
if(!string.IsNullOrWhiteSpace(flatBookmark.Path))
{
string[] splittedPath = flatBookmark.Path.Split(new string[] { separator }, StringSplitOptions.None);
string title = splittedPath[0];
if (title == bookmark.Title)
{
// Strip the first part of the path
flatBookmark.Path = string.Join(separator, splittedPath.Skip(1).Take(splittedPath.Length - 1));
childBookmarks.Add(flatBookmark);
found = true;
}
else
{
// Stop iteration when a new title is found, with this way it keeps the input order intact
// Multiple bookmarks on the same level with the same title should only be combined when there is no different title in between
// Cat1
// Cat2
// Cat1
if (found)
{
break;
}
}
}
}
// Create nested bookmarks (if they exist)
if(childBookmarks.Count > 0)
{
bookmark.Bookmarks = CreateNestedBookmarks(childBookmarks, separator);
}
}
return bookmarks;
}
I give you a starting point as pseudo code:
public List<Bookmark> ParseFlatBookmarks (List<FlatBookmark> flatBookmarks)
{
// define the list of local root bookmarks.
// loop over the FLAT bookmarks
// look only for the root bookmark in each path
// (ex. in Path = "Category 1/Page 1" you will look for "Category 1")
// then you check if found bookmark is already present in your root bookmarks list, if it is not than you create it and add it to the list.
// loop over found root bookmarks
// create a list of all the FLAT bookmarks that has current root bookmark as root
// (ex. if you have "Category 1" as current root bookmark you will have a list containing "Category 1/Page 1", "Category 1/Page 1/Attachment 1", etc. and the last one will be "Category 1/Page 3".
// to recursively navigate the FLAT bookmarks you will need to adjust their path.
// To do that just remove root bookmark from path
// (ex. if you currently have "Category 1" as root bookmark "Category 1/Page 1" as FLAT bookmark, you will have to set its path as "Page 1")
// recursive call passing currently root bookmark's FLAT bookmark children as parameter
// add found bookmarks to the currently root bookmark
// return list of root bookmarks
}
On top of that you will have to check when the path is empty (it means you've navigated all the way to the child bookmarks
The unique feature you've asked should be covered when you check if a just found bookmark as the same title as one previously stored.
If you can't adjust their path, you could always create new flat bookmarks with adjusted paths.
I hope I gave you some ideas to solve your problem