diff --git a/README.md b/README.md index c2d7665..703d2f0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # D3Trees -English | [日本語](./ja-README.md) - An Erlang tree library for data-driven documents. ## Installation @@ -40,6 +38,34 @@ Here's a basic example of how to use it: }] % end of "FirstNode" children }] % end of "RootNode" children } +%% lookup a value in the tree +3> d3trees:lookup(["FirstNode", "SecondNode"], UpdatedTree). +{value, "SecondNodeValue"} +4> d3trees:lookup(["FirstNode"], UpdatedTree). +none +%% increment a value in the tree +5> IntegerTree = d3trees:increment(["x", "y"], 10, Tree). +#{ + name => "RootNode" + , children => [#{ + name => "x" + , children => [#{ + name => "y" + , value => 10 + }] % end of "x" children + }] % end of "RootNode" children +} +6> d3trees:increment(["x", "y"], 2, IntegerTree). +#{ + name => "RootNode" + , children => [#{ + name => "x" + , children => [#{ + name => "y" + , value => 12 + }] % end of "x" children + }] % end of "RootNode" children +} ``` @@ -54,6 +80,7 @@ This project is licensed under the Apache License - see the [LICENSE](LICENSE) f ## Version History - **0.1.0** (Initial Commit) - [Release Notes](https://github.com/ts-klassen/d3trees/releases/tag/0.1.0) +- **0.1.1** (Initial Commit) - [Release Notes](https://github.com/ts-klassen/d3trees/releases/tag/0.1.1) ## Testing diff --git a/ja-README.md b/ja-README.md deleted file mode 100644 index 234e29a..0000000 --- a/ja-README.md +++ /dev/null @@ -1,67 +0,0 @@ -# D3Trees - -[English](./README.md) | 日本語 - -An Erlang tree library for data-driven documents. - -## インストール - -このライブラリをErlangプロジェクトに含めるには、rebar.configファイルに依存関係として追加します: - -```erlang -{deps, [ - {d3trees, {git, "https://github.com/ts-klassen/d3trees.git", {tag, "0.1.0"}}} -]}. -``` - -依存関係を管理するために[Rebar3](https://www.rebar3.org/)をインストールしていることを確認してください。 - -## 使用方法 - -このライブラリは、ツリー構造を作成および操作する機能を提供します。特に、[D3](https://d3js.org) のようなビジュアライゼーション(例: [Collapsible tree](https://observablehq.com/@d3/collapsible-tree?intent=fork)、[Zoomable icicle](https://observablehq.com/@d3/zoomable-icicle?intent=fork)、[Zoomable sunburst](https://observablehq.com/@d3/zoomable-sunburst?intent=fork))でツリー状のデータ構造が必要なシナリオに役立ちます。[jsone](https://github.com/sile/jsone) のようなライブラリを使用してツリーをJSONに変換できます。 - -以下は、使用方法の基本的な例です: - -```erlang -%% 新しいツリーノードを作成 -1> Tree = d3trees:new("RootNode"). -#{name => "RootNode"} - -%% ツリーに値を追加 -2> UpdatedTree = d3trees:upsert(["FirstNode", "SecondNode"], "SecondNodeValue", Tree). -#{ - name => "RootNode" - , children => [#{ - name => "FirstNode" - , children => [#{ - name => "SecondNode" - , value => "SecondNodeValue" - }] % "FirstNode"の終わり - }] % "RootNode"の終わり -} -``` - -## ライセンス - -このプロジェクトはApache Licenseの下でライセンスされています。詳細は[LICENSE](LICENSE)ファイルを参照してください。 - -## 著者と謝辞 - -- **ts-klassen** - 著者 - -## バージョン履歴 - -- **0.1.0** (初回コミット) - [リリースノート](https://github.com/ts-klassen/d3trees/releases/tag/0.1.0) - -## テスト - -提供されているEUnitテストを実行するには、Rebar3を使用できます: - -```bash -rebar3 eunit -``` - -## コントリビューション - -コントリビューションガイドラインは提供されておらず、サポート/連絡情報も現時点では提供されていません。質問や提案がある場合は、[GitHubリポジトリ](https://github.com/ts-klassen/d3trees/issues)で issue を開いてください。 - diff --git a/src/d3trees.app.src b/src/d3trees.app.src index 7fc3785..fa1f18d 100644 --- a/src/d3trees.app.src +++ b/src/d3trees.app.src @@ -1,6 +1,6 @@ {application, d3trees, [{description, "An Erlang tree library for data-driven documents."}, - {vsn, "0.1.0"}, + {vsn, "0.1.1"}, {registered, []}, {applications, [kernel, diff --git a/src/d3trees.erl b/src/d3trees.erl index 957bf9e..04cf25c 100644 --- a/src/d3trees.erl +++ b/src/d3trees.erl @@ -3,6 +3,8 @@ -export([ new/1 , upsert/3 + , lookup/2 + , increment/3 ]). -export_type([ name/0 @@ -39,7 +41,7 @@ new(Name) -> %% }] % end of "RootNode" children %% } -spec upsert( - PathBelowRoot::path() , Value::value(), Tree::tree() + PathBelowRoot::path(), Value::value(), Tree::tree() ) -> UpdatedTree::tree(). upsert([], Value, Tree) -> Tree#{value=>Value}; @@ -53,6 +55,74 @@ upsert([Name|Names], Value, Tree) -> Children1 = upsert_child(Child1, Children0), Tree#{children=>Children1}. +%% %%% lookup/2 usage %%% +%% 3> d3trees:lookup(["FirstNode", "SecondNode"], UpdatedTree). +%% {value, "SecondNodeValue"} +%% 4> d3trees:lookup(["FirstNode"], UpdatedTree). +%% none +-spec lookup( + PathBelowRoot::path(), Tree::tree() + ) -> {value, Value::value()} | none. +lookup(PathBelowRoot, Tree) -> + try lookup_(PathBelowRoot, Tree) of + Res -> Res + catch + throw:none -> none + end. +lookup_([], Tree) -> + case Tree of + #{value:=Value} -> {value, Value}; + _ -> throw(none) + end; +lookup_([Name|Names], Tree) -> + Children = case Tree of + #{children:=C} -> C; + _ -> throw(none) + end, + Child = find_child(Name, Children), + lookup_(Names, Child). + +%% %%% increment/3 usage %%% +%% 5> Tree. +%% #{name => "RootNode"} +%% 6> IntegerTree = d3trees:increment(["x", "y"], 10, Tree). +%% #{ +%% name => "RootNode" +%% , children => [#{ +%% name => "x" +%% , children => [#{ +%% name => "y" +%% , value => 10 +%% }] % end of "x" children +%% }] % end of "RootNode" children +%% } +%% 7> d3trees:increment(["x", "y"], 2, IntegerTree). +%% #{ +%% name => "RootNode" +%% , children => [#{ +%% name => "x" +%% , children => [#{ +%% name => "y" +%% , value => 12 +%% }] % end of "x" children +%% }] % end of "RootNode" children +%% } +-spec increment( + PathBelowRoot::path(), Value::integer(), Tree::tree() + ) -> UpdatedTree::tree(). +increment(Path, Value, Tree) when is_integer(Value) -> + UpdatedValue = case lookup(Path, Tree) of + {value, V} when is_integer(V) -> + V + Value; + none -> + Value; + {value, V} -> + error({non_integer_value, V, Path}) + end, + upsert(Path, UpdatedValue, Tree); +increment(_Path, Value, _Tree) -> + error({non_integer_value, Value}). + find_child(Name, []) -> #{name=>Name}; find_child(Name, [#{name:=Name}=Child|_]) -> diff --git a/test/d3trees_tests.erl b/test/d3trees_tests.erl index b4de603..44d71ed 100644 --- a/test/d3trees_tests.erl +++ b/test/d3trees_tests.erl @@ -93,3 +93,165 @@ update_existing_path_without_children_test() -> ExpectedTree = #{name => "RootNode", children => [#{name => "FirstNode", value => "NewValue", children => [#{name => "SecondNode", value => "SecondNodeValue"}]}]}, ?assertEqual(ExpectedTree, Result). +% Test the lookup/2 function when the path exists in the tree. +lookup_existing_path_test() -> + % Arrange: Create a sample tree with an existing path + InitialTree = #{name => "RootNode", children => [#{name => "FirstNode", children => [#{name => "SecondNode", value => "SecondNodeValue"}]}]}, + + % Act: Call the lookup/2 function to retrieve a value + Result = d3trees:lookup(["FirstNode", "SecondNode"], InitialTree), + + % Assert: Check that the result is as expected + Expected = {value, "SecondNodeValue"}, + ?assertEqual(Expected, Result). + +% Test the lookup/2 function when the path doesn't exist in the tree. +lookup_nonexistent_path_test() -> + % Arrange: Create a sample tree with a different path + InitialTree = #{name => "RootNode", children => [#{name => "FirstNode", value => "FirstNodeValue"}]}, + + % Act: Call the lookup/2 function to retrieve a value for a non-existent path + Result = d3trees:lookup(["NonExistentNode"], InitialTree), + + % Assert: Check that the result is 'none' + ?assertEqual(none, Result). + +% Test the lookup/2 function when the tree is empty (should return 'none'). +lookup_empty_tree_test() -> + % Arrange: Create an empty tree + EmptyTree = #{name => "EmptyTree"}, + + % Act: Call the lookup/2 function on an empty tree + Result = d3trees:lookup(["FirstNode", "SecondNode"], EmptyTree), + + % Assert: Check that the result is 'none' + ?assertEqual(none, Result). + +% Test the lookup/2 function when the specified path is partially present in the tree. +lookup_partial_path_test() -> + % Arrange: Create a sample tree with a partial path + InitialTree = #{name => "RootNode", children => [#{name => "FirstNode", value => "FirstNodeValue"}]}, + + % Act: Call the lookup/2 function with a path that is partially present + Result = d3trees:lookup(["FirstNode", "NonExistentNode"], InitialTree), + + % Assert: Check that the result is 'none' + ?assertEqual(none, Result). + +% Test the lookup/2 function when the tree has multiple levels of nesting. +lookup_nested_tree_test() -> + % Arrange: Create a sample tree with multiple levels of nesting + InitialTree = #{name => "RootNode", children => [#{name => "FirstNode", children => [#{name => "SecondNode", children => [#{name => "ThirdNode", value => "ThirdNodeValue"}]}]}]}, + + % Act: Call the lookup/2 function to retrieve a value from a nested path + Result = d3trees:lookup(["FirstNode", "SecondNode", "ThirdNode"], InitialTree), + + % Assert: Check that the result is as expected + Expected = {value, "ThirdNodeValue"}, + ?assertEqual(Expected, Result). + +% Test the lookup/2 function when the path is an empty list (should return 'none'). +lookup_empty_path_test() -> + % Arrange: Create a sample tree + InitialTree = #{name => "RootNode", children => [#{name => "FirstNode", value => "FirstNodeValue"}]}, + + % Act: Call the lookup/2 function with an empty path + Result = d3trees:lookup([], InitialTree), + + % Assert: Check that the result is 'none' + ?assertEqual(none, Result). + +% Test the lookup/2 function when the tree has multiple branches and the path leads to a leaf node. +lookup_leaf_node_test() -> + % Arrange: Create a sample tree with multiple branches and a leaf node + InitialTree = #{name => "RootNode", children => [ + #{name => "BranchA", children => [#{name => "LeafA1", value => "LeafA1Value"}]}, + #{name => "BranchB", children => [#{name => "LeafB1", value => "LeafB1Value"}]} + ]}, + + % Act: Call the lookup/2 function to retrieve a value from a leaf node + Result = d3trees:lookup(["BranchA", "LeafA1"], InitialTree), + + % Assert: Check that the result is as expected + Expected = {value, "LeafA1Value"}, + ?assertEqual(Expected, Result). + +% Test the lookup/2 function when the path contains non-existent intermediate nodes. +lookup_nonexistent_intermediate_nodes_test() -> + % Arrange: Create a sample tree with missing intermediate nodes + InitialTree = #{name => "RootNode", children => [#{name => "FirstNode", value => "FirstNodeValue"}]}, + + % Act: Call the lookup/2 function with a path containing non-existent intermediate nodes + Result = d3trees:lookup(["FirstNode", "NonExistentNode", "LeafNode"], InitialTree), + + % Assert: Check that the result is 'none' + ?assertEqual(none, Result). + +% Test the lookup/2 function when the tree contains nodes with the same name at different levels. +lookup_same_named_nodes_test() -> + % Arrange: Create a sample tree with nodes having the same name at different levels + InitialTree = #{name => "RootNode", children => [ + #{name => "FirstNode", children => [ + #{name => "SecondNode", value => "SecondNodeValue"}, + #{name => "ThirdNode", value => "ThirdNodeValue"} + ]} + ]}, + + % Act: Call the lookup/2 function to retrieve values from nodes with the same name + Result1 = d3trees:lookup(["FirstNode", "SecondNode"], InitialTree), + Result2 = d3trees:lookup(["FirstNode", "ThirdNode"], InitialTree), + + % Assert: Check that the results are as expected + Expected1 = {value, "SecondNodeValue"}, + Expected2 = {value, "ThirdNodeValue"}, + ?assertEqual(Expected1, Result1), + ?assertEqual(Expected2, Result2). + +% Test the increment/3 function when the path exists in the tree. +increment_existing_path_test() -> + % Arrange: Create a sample tree with an existing path and an integer value + InitialTree = #{name => "RootNode", children => [#{name => "x", children => [#{name => "y", value => 10}]}]}, + + % Act: Call the increment/3 function to increment the value + UpdatedTree = d3trees:increment(["x", "y"], 5, InitialTree), + + % Assert: Check that the value is incremented as expected + Result = d3trees:lookup(["x", "y"], UpdatedTree), + Expected = {value, 15}, + ?assertEqual(Expected, Result). + +% Test the increment/3 function when the path doesn't exist in the tree (should create the path). +increment_nonexistent_path_test() -> + % Arrange: Create a sample tree with a different path + InitialTree = #{name => "RootNode", children => [#{name => "a"}]}, + + % Act: Call the increment/3 function on a non-existent path + UpdatedTree = d3trees:increment(["x", "y"], 5, InitialTree), + + % Assert: Check that the path is created and the value is set correctly + Result = d3trees:lookup(["x", "y"], UpdatedTree), + Expected = {value, 5}, + ?assertEqual(Expected, Result). + +% Test the increment/3 function when given non-integer values. +increment_non_integer_value_test() -> + % Arrange: Create a sample tree with an existing path and a integer value + InitialTree = #{name => "RootNode", children => [#{name => "x", children => [#{name => "y", value => 10}]}]}, + + % Act: Call the increment/3 function on a path with a non-integer value + {'EXIT', {Result, _}} = catch d3trees:increment(["x", "y"], "z", InitialTree), + + % Assert: Check that an error is returned + ?assertMatch({non_integer_value, "z"}, Result). + +% Test the increment/3 function when the path exists but the value is not an integer. +increment_existing_path_non_integer_value_test() -> + % Arrange: Create a sample tree with an existing path and a non-integer value + InitialTree = #{name => "RootNode", children => [#{name => "x", children => [#{name => "y", value => "String"}]}]}, + + % Act: Call the increment/3 function on a path with a non-integer value + {'EXIT', {Result, _}} = catch d3trees:increment(["x", "y"], 5, InitialTree), + + % Assert: Check that an error is returned + ?assertMatch({non_integer_value, "String", ["x", "y"]}, Result). +