diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..4a7fe22 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,23 @@ +name: test + +on: + push: + branches: + - master + - main + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: erlef/setup-beam@v1 + with: + otp-version: "27.1.2" + gleam-version: "1.12.0" + rebar3-version: "3" + # elixir-version: "1" + - run: gleam deps download + - run: gleam test + - run: gleam format --check src test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..599be4e --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.beam +*.ez +/build +erl_crash.dump diff --git a/README.md b/README.md index 5926214..b6299e4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,26 @@ # dllist -Gleam implementation of a doubly-linked list. -Allows for a cyclic or terminated lists \ No newline at end of file +Gleam implementation of a doubly-linked list. +Allows for a cyclic or terminated lists +[![Package Version](https://img.shields.io/hexpm/v/dllist)](https://hex.pm/packages/dllist) +[![Hex Docs](https://img.shields.io/badge/hex-docs-ffaff3)](https://hexdocs.pm/dllist/) + +```sh +gleam add dllist@1 +``` +```gleam +import dllist + +pub fn main() -> Nil { + // TODO: An example of the project in use +} +``` + +Further documentation can be found at . + +## Development + +```sh +gleam run # Run the project +gleam test # Run the tests +``` diff --git a/gleam.toml b/gleam.toml new file mode 100644 index 0000000..8cef855 --- /dev/null +++ b/gleam.toml @@ -0,0 +1,19 @@ +name = "dllist" +version = "1.0.0" + +# Fill out these fields if you intend to generate HTML documentation or publish +# your project to the Hex package manager. +# +# description = "" +# licences = ["Apache-2.0"] +# repository = { type = "github", user = "", repo = "" } +# links = [{ title = "Website", href = "" }] +# +# For a full reference of all the available options, you can have a look at +# https://gleam.run/writing-gleam/gleam-toml/. + +[dependencies] +gleam_stdlib = ">= 0.44.0 and < 2.0.0" + +[dev-dependencies] +gleeunit = ">= 1.0.0 and < 2.0.0" diff --git a/manifest.toml b/manifest.toml new file mode 100644 index 0000000..b3c53aa --- /dev/null +++ b/manifest.toml @@ -0,0 +1,11 @@ +# This file was generated by Gleam +# You typically do not need to edit this file + +packages = [ + { name = "gleam_stdlib", version = "0.65.0", build_tools = ["gleam"], requirements = [], otp_app = "gleam_stdlib", source = "hex", outer_checksum = "7C69C71D8C493AE11A5184828A77110EB05A7786EBF8B25B36A72F879C3EE107" }, + { name = "gleeunit", version = "1.6.1", build_tools = ["gleam"], requirements = ["gleam_stdlib"], otp_app = "gleeunit", source = "hex", outer_checksum = "FDC68A8C492B1E9B429249062CD9BAC9B5538C6FBF584817205D0998C42E1DAC" }, +] + +[requirements] +gleam_stdlib = { version = ">= 0.44.0 and < 2.0.0" } +gleeunit = { version = ">= 1.0.0 and < 2.0.0" } diff --git a/src/dllist.gleam b/src/dllist.gleam new file mode 100644 index 0000000..28effd8 --- /dev/null +++ b/src/dllist.gleam @@ -0,0 +1,270 @@ +import gleam/bool +import gleam/dict +import gleam/list +import gleam/option.{type Option, None, Some} +import gleam/result + +/// https://hackage.haskell.org/package/liboleg-2009.9.1/docs/src/Data-FDList.html#empty +pub opaque type Node(a) { + Node(val: a, left: Int, right: Int) +} + +pub type DLList(a) { + DLList(counter: Int, current: Int, mem: dict.Dict(Int, Node(a))) + // CyclicList(counter: Int, current: Int, mem: dict.Dict(Int, Node(a))) +} + +pub fn main() { + let l = new() + let assert Ok(l2) = insert_right(l, 123) |> echo + let assert Ok(l3) = insert_right(l2, 456) |> echo + let assert Ok(l4) = insert_right(l3, 789) + let assert Ok(l5) = insert_right(l4, 0) + + get(l5) |> echo + l5 |> echo + l5 |> to_list |> echo + let l6 = delete(l5) + l6 |> echo + let l7 = delete(l6) + l7 |> echo + let l8 = delete(l7) + l8 |> echo + let l9 = delete(l8) + l9 |> echo + + from_list([1, 2, 3, 4]) |> echo + + let assert Ok(l1) = new() |> insert_right(1) + l1 |> echo + let l1t = take(l1, 5) + l1t |> echo + let assert Ok(l2) = insert_right(l1, 2) + l2 |> echo + let l2t = take(l2, 5) + l2t |> echo +} + +pub fn new() -> DLList(a) { + DLList(1, 0, dict.new()) +} + +fn get_curr_node(list: DLList(a)) -> Result(Node(a), Nil) { + dict.get(list.mem, list.current) +} + +pub fn get(list: DLList(a)) -> Result(a, Nil) { + get_curr_node(list) |> result.map(fn(n) { n.val }) +} + +pub fn is_empty(list: DLList(a)) -> Bool { + dict.is_empty(list.mem) +} + +pub fn insert_right(list: DLList(a), val: a) -> Result(DLList(a), Nil) { + let ref = list.counter + case is_empty(list) { + True -> { + let node = Node(val, ref, ref) + Ok(DLList(ref + 1, ref, dict.insert(list.mem, ref, node))) + } + False -> { + use curr_node <- result.try(dict.get(list.mem, list.current)) + let curr_node_2 = Node(..curr_node, right: ref) + let next = curr_node.right + + let next_node = case next == list.current { + True -> Ok(curr_node_2) + False -> dict.get(list.mem, next) + } + let new_mem = case next == list.current { + True -> list.mem + False -> dict.insert(list.mem, list.current, curr_node_2) + } + let new_mem = case next_node { + Ok(next_node) -> { + let next_node_2 = Node(..next_node, left: ref) + dict.insert(new_mem, next, next_node_2) + } + Error(_) -> new_mem + } + let new_node = Node(val, list.current, next) + + let new_mem = dict.insert(new_mem, ref, new_node) + Ok(DLList(list.counter + 1, list.counter, new_mem)) + } + } +} + +pub fn delete(list: DLList(a)) -> DLList(a) { + let #(mem, curr_node) = remove_lookup(list, list.current) + case curr_node { + None -> list + Some(curr_node) -> { + let l = curr_node.left + let r = curr_node.right + case l, r { + 0, 0 -> new() + 0, n if n == list.current -> new() + n, 0 if n == list.current -> new() + n, m if n == list.current && m == list.current -> new() + _, 0 -> { + DLList( + ..list, + current: l, + mem: update_dict_entry(mem, l, fn(n) { Node(..n, right: r) }), + ) + } + _, r if r == list.current -> { + DLList( + ..list, + current: l, + mem: update_dict_entry(mem, l, fn(n) { Node(..n, right: l) }), + ) + } + 0, _ -> { + DLList( + ..list, + current: r, + mem: update_dict_entry(mem, r, fn(n) { Node(..n, left: l) }), + ) + } + l, _ if l == list.current -> { + DLList( + ..list, + current: r, + mem: update_dict_entry(mem, r, fn(n) { Node(..n, left: r) }), + ) + } + l, r if l == r -> { + DLList( + ..list, + current: r, + mem: update_dict_entry(mem, r, fn(n) { + Node(..n, left: r, right: r) + }), + ) + } + _, _ -> { + DLList( + ..list, + current: r, + mem: update_dict_entry(mem, r, fn(n) { Node(..n, left: l) }) + |> update_dict_entry(l, fn(n) { Node(..n, right: r) }), + ) + } + } + } + } +} + +pub fn move_right(list: DLList(a)) -> DLList(a) { + do_move_right(list) |> result.unwrap(list) +} + +fn do_move_right(list: DLList(a)) -> Result(DLList(a), Nil) { + case get_curr_node(list) { + Error(_) -> Error(Nil) + Ok(node) -> { + case node.right { + 0 -> Ok(list) + r -> Ok(DLList(..list, current: r)) + } + } + } +} + +pub fn move_left(list: DLList(a)) -> DLList(a) { + do_move_left(list) + |> result.unwrap(list) +} + +fn do_move_left(list: DLList(a)) -> Result(DLList(a), Nil) { + case get_curr_node(list) { + Error(_) -> Error(Nil) + Ok(node) -> { + case node.left { + 0 -> Ok(list) + l -> Ok(DLList(..list, current: l)) + } + } + } +} + +pub fn from_list(list: List(a)) -> Result(DLList(a), Nil) { + list.try_fold(list, new(), insert_right) +} + +pub fn take(list: DLList(a), n_times: Int) -> List(a) { + take_loop(list, n_times, []) +} + +fn take_loop(list: DLList(a), n_times: Int, acc: List(a)) -> List(a) { + use <- bool.guard(when: is_empty(list), return: []) + case n_times { + 0 -> list.reverse(acc) + n -> { + let assert Ok(item) = get(list) + take_loop(move_right(list), n - 1, [item, ..acc]) + } + } +} + +pub fn to_list(list: DLList(a)) -> List(a) { + case is_empty(list) { + True -> [] + False -> { + let assert Ok(a) = get(list) + [a, ..to_list_loop(list.current, do_move_right(list))] + } + } +} + +fn to_list_loop(ref: Int, list: Result(DLList(a), Nil)) -> List(a) { + case list { + Error(_) -> [] + Ok(DLList(_, current, _)) if current == ref -> [] + Ok(list) -> { + let assert Ok(a) = get(list) + [a, ..to_list_loop(ref, do_move_right(list))] + } + } +} + +pub fn update(list: DLList(a), value: a) -> Result(DLList(a), Nil) { + use <- bool.guard(when: is_empty(list), return: Error(Nil)) + let assert Ok(curr_node) = get_curr_node(list) + Ok( + DLList( + ..list, + mem: dict.insert(list.mem, list.current, Node(..curr_node, val: value)), + ), + ) +} + +fn update_dict_entry( + list: dict.Dict(Int, Node(a)), + ref: Int, + upd: fn(Node(a)) -> Node(a), +) -> dict.Dict(Int, Node(a)) { + case dict.get(list, ref) { + Ok(n) -> { + dict.insert(list, ref, upd(n)) + } + Error(_) -> list + } +} + +fn remove_lookup( + list: DLList(a), + id: Int, +) -> #(dict.Dict(Int, Node(a)), Option(Node(a))) { + case dict.get(list.mem, id) { + Ok(Node(_, _, _) as n) -> { + #(dict.delete(list.mem, id), Some(n)) + } + Error(_) -> { + #(list.mem, None) + } + } +} diff --git a/test/dllist_test.gleam b/test/dllist_test.gleam new file mode 100644 index 0000000..fba3c88 --- /dev/null +++ b/test/dllist_test.gleam @@ -0,0 +1,13 @@ +import gleeunit + +pub fn main() -> Nil { + gleeunit.main() +} + +// gleeunit test functions end in `_test` +pub fn hello_world_test() { + let name = "Joe" + let greeting = "Hello, " <> name <> "!" + + assert greeting == "Hello, Joe!" +}