diff --git a/.gitignore b/.gitignore index 4a805e6..12179ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,20 @@ -/ebin -/deps +# The directory Mix will write compiled artifacts to. +/_build/ + +# If you run "mix test --cover", coverage assets end up here. +/cover/ + +# The directory Mix downloads your dependencies sources to. +/deps/ + +# Where 3rd-party dependencies like ExDoc output generated docs. +/doc/ + +# Ignore .fetch files in case you like to edit your project deps locally. +/.fetch + +# If the VM crashes, it generates a dump, let's ignore it too. erl_crash.dump + +# Also ignore archive artifacts (built via "mix archive.build"). *.ez -*.swp -docs -tags -.DS_Store -graphs diff --git a/config/config.exs b/config/config.exs new file mode 100644 index 0000000..ab1bc64 --- /dev/null +++ b/config/config.exs @@ -0,0 +1,39 @@ +# This file is responsible for configuring your application +# and its dependencies with the aid of the Mix.Config module. +use Mix.Config + +# This configuration is loaded before any dependency and is restricted +# to this project. If another project depends on this project, this +# file won't be loaded nor affect the parent project. For this reason, +# if you want to provide default values for your application for +# 3rd-party users, it should be done in your "mix.exs" file. + +# You can configure your application as: +# +# config :ropex, key: :value +# +# and access this configuration in your application as: +# +# Application.get_env(:ropex, :key) +# +# You can also configure a 3rd-party app: +# +# config :logger, level: :info +# + +# It is also possible to import configuration files, relative to this +# directory. For example, you can emulate configuration per environment +# by uncommenting the line below and defining dev.exs, test.exs and such. +# Configuration from the imported file will override the ones defined +# here (which is why it is important to import them last). +# +# import_config "#{Mix.env}.exs" + +if Mix.env == :dev do + config :mix_test_watch, + tasks: [ + # "dialyzer", + "test", + # "credo", + ] +end diff --git a/lib/rope.ex b/lib/rope.ex index 113cd68..c203681 100644 --- a/lib/rope.ex +++ b/lib/rope.ex @@ -12,7 +12,7 @@ defmodule Rope do 4. Should be able to handle alternate representations (ex: IO stream) - we'll see A rope is build up of leaf and parent/concatenation nodes. Leaf nodes contain the - chunks of binary strings that are concatenated or inserted into the rope. The + chunks of binary strings that are concatenated or inserted into the rope. The parent/concatentation nodes are purely used to link the various leaf nodes together. The concatentation nodes contain basic tree information. @@ -35,24 +35,43 @@ defmodule Rope do - https://en.wikipedia.org/wiki/Rope_\(data_structure\) """ + require Record @partition_size_threshold 1000 @partition_term_ratio 0.1 @partition_depth_limit 3 + defmodule Rnode do + defstruct [ + length: 0, + depth: 1, + left: nil, + right: nil + ] - defrecordp :rnode, Rope, - length: 0 :: non_neg_integer, - depth: 1 :: non_neg_integer, - left: nil :: rope, - right: nil :: rope + @type t :: %__MODULE__{ + length: non_neg_integer, + depth: non_neg_integer, + left: Rope.t, + right: Rope.t + } + end + + defmodule Rleaf do + defstruct [ + length: 0, + depth: 0, + value: nil + ] - defrecordp :rleaf, Rope, - length: 0 :: non_neg_integer, - depth: 0 :: non_neg_integer, - value: nil :: binary + @type t :: %__MODULE__{ + length: non_neg_integer, + depth: non_neg_integer, + value: binary + } + end - @type rope :: rnode_t | rleaf_t | nil + @type t :: Rnode.t | Rleaf.t | nil # copied the type defs from the String module @type str :: binary @@ -69,13 +88,13 @@ defmodule Rope do iex> Rope.new("Don't panic") |> Rope.to_string "Don't panic" """ - @spec new(str | nil) :: rope + @spec new(str | nil) :: t def new(nil) do nil end def new(str) when is_binary(str) do - rleaf(length: String.length(str), value: str) + %Rleaf{length: String.length(str), value: str} end @doc """ @@ -91,8 +110,9 @@ defmodule Rope do iex> Rope.concat([Rope.new("terrible"), " ghastly", " silence"]) |> Rope.to_string "terrible ghastly silence" """ - @spec concat(list(rope | str), list) :: rope - def concat([first | rest], opts // []) do + @spec concat(list(t | str), list) :: t + def concat(_rope, _opts \\ []) + def concat([first | rest], opts) do Enum.reduce(rest, ropeify(first), fn(right, left) -> do_concat(left, right, opts) end) end @@ -102,36 +122,36 @@ defmodule Rope do @doc """ - Returns a sub-rope starting at the offset given by the first, and a length given by + Returns a sub-rope starting at the offset given by the first, and a length given by the second. If the offset is greater than string length, than it returns nil. Similar to String.slice/3, check the tests for some examples of usage. """ - @spec slice(rope, integer, integer) :: rope + @spec slice(t, integer, integer) :: t def slice(nil, _start, _len) do nil end - def slice(_rope, start, len) when len == 0 do + def slice(_rope, _start, len) when len == 0 do ropeify "" end - def slice(rnode(length: rlen), start, len) + def slice(%Rnode{length: rlen}, start, len) when start == rlen and len > 0 do ropeify "" end - def slice(node = rnode(length: rlen), start, len) + def slice(node = %Rnode{length: rlen}, start, len) when start == 0 and rlen <= len and len > 0 do node end - def slice(rnode(length: rlen), start, len) + def slice(%Rnode{length: rlen}, start, len) when start > rlen and len > 0 do nil end - def slice(leaf = rleaf(length: rlen, value: value), start, len) + def slice(leaf = %Rleaf{length: rlen, value: value}, start, len) when len > 0 do if start == 0 and rlen <= len do leaf @@ -141,12 +161,15 @@ defmodule Rope do end def slice(rope, start, len) when len > 0 do - rnode(left: left, - right: right) = rope + %Rnode{left: left, + right: right} = rope - if start < 0 do - start = rope.length + start - end + start = + if start < 0 do + rope.length + start + else + start + end {startRight, lenRight} = if start < left.length do @@ -169,7 +192,7 @@ defmodule Rope do Checks if the rope is considered balanced based on comparing total length and depth verses the fibanocci sequence. """ - @spec balanced?(rope) :: bool + @spec balanced?(t) :: boolean def balanced?(nil) do true end @@ -186,13 +209,17 @@ defmodule Rope do efficient. This is a pretty greedy rebalancing and should produce a fully balanced rope. """ - @spec rebalance(rope) :: rope + @spec rebalance(t) :: t def rebalance(nil) do nil end - def rebalance(rope) when is_record(rope, Rope) do - leaves = rope + def rebalance(%Rleaf{} = rope), do: rebalance_rope(rope) + def rebalance(%Rnode{} = rope), do: rebalance_rope(rope) + + defp rebalance_rope(rope) do + leaves = + rope |> Enum.reduce([], fn(leaf, acc) -> [leaf | acc] end) |> Enum.reverse @@ -207,12 +234,12 @@ defmodule Rope do iex> Rope.length(Rope.concat([Rope.new("terrible"), " ghastly silence"])) 24 """ - @spec length(rope) :: non_neg_integer + @spec length(t) :: non_neg_integer def length(rope) do case rope do nil -> 0 - rleaf(length: len) -> len - rnode(length: len) -> len + %Rleaf{length: len} -> len + %Rnode{length: len} -> len end end @@ -232,12 +259,12 @@ defmodule Rope do iex> Rope.depth(Rope.concat([Rope.new("terrible"), " ghastly", " silence"])) 2 """ - @spec depth(rope) :: non_neg_integer + @spec depth(t) :: non_neg_integer def depth(rope) do case rope do nil -> 0 - rnode(depth: depth) -> depth - rleaf(depth: depth) -> depth + %Rnode{depth: depth} -> depth + %Rleaf{depth: depth} -> depth end end @@ -253,15 +280,18 @@ defmodule Rope do iex> Rope.insert_at(Rope.concat(["infinite ", "number ", "monkeys"]), -7, "of ") |> Rope.to_string "infinite number of monkeys" """ - @spec insert_at(rope, integer, str) :: rope + @spec insert_at(t, integer, str) :: t def insert_at(nil, _index, str) do ropeify(str) end def insert_at(rope, index, str) do - if index < 0 do - index = rope.length + index - end + index = + if index < 0 do + rope.length + index + else + index + end left = slice(rope, 0, index) right = slice(rope, index, rope.length) @@ -270,8 +300,8 @@ defmodule Rope do end @doc """ - Produces a new rope with the substr defined by the starting index and the length of - characters removed. The advantage of this is it takes full advantage of ropes + Produces a new rope with the substr defined by the starting index and the length of + characters removed. The advantage of this is it takes full advantage of ropes being optimized for index based operation. ## Examples @@ -282,15 +312,18 @@ defmodule Rope do iex> Rope.remove_at(Rope.concat(["infinite ", "number of ", "monkeys"]), -7, 3) |> Rope.to_string "infinite number of keys" """ - @spec remove_at(rope, integer, non_neg_integer) :: rope + @spec remove_at(t, integer, non_neg_integer) :: t def remove_at(nil, _index, _len) do nil end def remove_at(rope, index, len) do - if index < 0 do - index = rope.length + index - end + index = + if index < 0 do + rope.length + index + else + index + end left = slice(rope, 0, index) right = slice(rope, index + len, rope.length) @@ -309,14 +342,19 @@ defmodule Rope do iex> Rope.find(Rope.concat(["loathe it", " or ignore it,", " you can't like it"]), "and") -1 """ - @spec find(rope, str) :: integer + @spec find(t, str) :: integer def find(nil, _term) do -1 end - defrecordp :findctxt, - findIndex: 0 :: non_neg_integer, #where the match started in the rope - termIndex: 0 :: non_neg_integer #next index to try + Record.defrecordp :findctxt, + findIndex: 0, #where the match started in the rope + termIndex: 0 #next index to try + + @type findctxt_t :: record(:findctxt, + findIndex: non_neg_integer, #where the match started in the rope + termIndex: non_neg_integer #next index to try + ) def find(rope, term) do termLen = String.length(term) @@ -324,12 +362,12 @@ defmodule Rope do foundMatch = fn(findctxt(termIndex: i)) -> i == termLen end {_offset, possibles} = do_reduce_while(rope, { 0, []}, - fn(leaf, {_offset, possibles}) -> + fn(_leaf, {_offset, possibles}) -> # it wil continue so long as we retruen true # we want to stop when we have a match not Enum.any?(possibles, foundMatch) end, - fn(rleaf(length: len, value: chunk), {offset, possibles}) -> + fn(%Rleaf{length: len, value: chunk}, {offset, possibles}) -> {offset + len, build_possible_matches(offset, chunk, term, possibles)} end ) @@ -338,7 +376,7 @@ defmodule Rope do |> Enum.filter(foundMatch) |> Enum.map(fn(findctxt(findIndex: i)) -> i end) |> Enum.reverse - |> Enum.first + |> Enum.at(0) if match == nil do -1 @@ -360,7 +398,7 @@ defmodule Rope do iex> Rope.find_all(Rope.concat(["loathe it", " or ignore it,", " you can't like it"]), "and") [] """ - @spec find_all(rope, str) :: list(non_neg_integer) + @spec find_all(t, str) :: list(non_neg_integer) def find_all(rope, term) do termLength = String.length(term) segments = partition_rope(0, rope, termLength, 0) @@ -369,18 +407,18 @@ defmodule Rope do segments |> Enum.map(fn({offset, length}) -> slice = Rope.slice(rope, offset, length) - ref = make_ref + ref = make_ref() spawn_link(fn() -> matches = do_find_all(slice, term) |> Enum.map(fn(match) -> match + offset end) - parent <- {ref, :pfind, self(), matches} + send parent, {ref, :pfind, self(), matches} end) end) - |> Enum.map(fn(child) -> + |> Enum.map(fn(_child) -> receive do - {ref, :pfind, child, matches} -> + {_ref, :pfind, _child, matches} -> matches end end) @@ -392,11 +430,11 @@ defmodule Rope do @doc """ Replaces the first match with the replacement text and returns the new rope. If not found then the existing rope is returned. - By default, it replaces all entries, except if the global option + By default, it replaces all entries, except if the global option is set to false. """ - @spec replace(rope, str, str, list) :: rope - def replace(rope, pattern, replacement, opts // []) do + @spec replace(t, str, str, list) :: t + def replace(rope, pattern, replacement, opts \\ []) do global = Keyword.get(opts, :global, true) if global do @@ -409,20 +447,19 @@ defmodule Rope do @doc """ Converts the entire rope to a single binary. """ - @spec to_string(rope) :: binary + @spec to_string(t) :: binary def to_string(rope) do rope - |> Stream.map(fn(rleaf(value: value)) -> value end) + |> Enum.map(fn(%Rleaf{value: value}) -> value end) |> Enum.join end defp ropeify(rope) do case rope do - rnode() -> rope - rleaf() -> rope - <<_ :: binary>> -> - Rope.new(rope) + %Rnode{} -> rope + %Rleaf{} -> rope + <<_ :: binary>> -> Rope.new(rope) nil -> nil end end @@ -432,28 +469,27 @@ defmodule Rope do end defp do_concat(rope, nil, _opts) do - ropeify rope + ropeify(rope) end defp do_concat(nil, rope, _opts) do - ropeify rope + ropeify(rope) end defp do_concat(rope1, rope2, opts) do rebalance = Keyword.get(opts, :rebalance, true) - rope1 = ropeify rope1 - rope2 = ropeify rope2 + rope1 = ropeify(rope1) + rope2 = ropeify(rope2) - depth = rope1.depth - if rope2.depth > depth do - depth = rope2.depth - end + depth = max(rope2.depth, rope1.depth) - rope = rnode(depth: depth + 1, - left: rope1, - right: rope2, - length: rope1.length + rope2.length) + rope = %Rnode{ + depth: depth + 1, + left: rope1, + right: rope2, + length: rope1.length + rope2.length + } if rebalance and (not Rope.balanced?(rope)) do Rope.rebalance(rope) @@ -499,14 +535,17 @@ defmodule Rope do [] end - defp partition_rope(offset, rleaf(length: len), termLength, _depth) do + defp partition_rope(offset, %Rleaf{length: len}, termLength, _depth) do [{offset, len + termLength}] end - defp partition_rope(offset, rnode(length: len, right: right, left: left), termLength, depth) do - if offset > termLength do - offset = offset - termLength - end + defp partition_rope(offset, %Rnode{length: len, right: right, left: left}, termLength, depth) do + offset = + if offset > termLength do + offset - termLength + else + offset + end cond do depth >= @partition_depth_limit -> @@ -531,12 +570,12 @@ defmodule Rope do foundMatch = fn(findctxt(termIndex: i)) -> i == termLen end {_offset, possibles} = Enum.reduce(rope, { 0, []}, - fn(rleaf(length: len, value: chunk), {offset, possibles}) -> + fn(%Rleaf{length: len, value: chunk}, {offset, possibles}) -> {offset + len, build_possible_matches(offset, chunk, term, possibles)} end ) - match = possibles + possibles |> Enum.filter(foundMatch) |> Enum.map(fn(findctxt(findIndex: i)) -> i end) |> Enum.reverse @@ -558,7 +597,7 @@ defmodule Rope do {offset, subropes} = Enum.reduce(indexes, {0, []}, fn(index, {offset, ropes}) -> len = index - offset - if offset != 0, do: len = len + # if offset != 0, do: len = len leftRope = slice(rope, offset, len) {index + termLen, [replacement | [leftRope | ropes]]} @@ -567,8 +606,8 @@ defmodule Rope do leftRope = slice(rope, offset, rope.length) subropes = [leftRope | subropes] - subropes = Enum.reverse subropes - rebuild_rope [], subropes + subropes = Enum.reverse(subropes) + rebuild_rope([], subropes) end defp do_reduce_while(enumerable, acc, whiler, reducer) do @@ -586,7 +625,7 @@ defmodule Rope do end defp rebuild_rope(subropes, [leaf1, leaf2 | leaves]) do - subrope = Rope.concat([leaf1, leaf2]) + subrope = Rope.concat([leaf1, leaf2], rebalance: false) rebuild_rope([subrope | subropes], leaves) end @@ -620,7 +659,7 @@ defmodule Rope do end - defimpl String.Chars, for: Rope do + defimpl String.Chars do @doc """ Converts the entire rope to a single binary string. """ @@ -630,7 +669,7 @@ defmodule Rope do end - defimpl Enumerable, for: Rope do + defimpl Enumerable, for: Rope.Rleaf do @moduledoc """ A convenience implementation that enumerates over the leaves of the rope but none of the parent/concatenation nodes. @@ -642,76 +681,60 @@ defmodule Rope do A count of the leaf nodes in the rope. This current traverses the rope to count them. """ def count(rope) do - Rope.reduce_leaves(rope, 0, fn(_leaf, acc) -> acc + 1 end) + {:ok, reduce(rope, 0, fn(_leaf, acc) -> acc + 1 end)} end @doc """ Searches the ropes leaves in order for a match. """ def member?(rope, value) do - try do - Rope.reduce_leaves(rope, false, - fn(leaf, false) -> - if leaf == value do - #yeah yeah, it's an error for control flow - throw :found - end - false - end) - catch - :throw, :found -> true - end + {:ok, Enum.any?(rope, fn (leaf) -> leaf.value == value end)} end @doc """ Reduces over the leaf nodes. """ - def reduce(rope, acc, fun) do - Rope.reduce_leaves(rope, acc, fun) + def reduce(_, {:halt, acc}, _fun), do: {:halted, acc} + def reduce(rope, {:suspend, acc}, fun), do: {:suspended, acc, &reduce(rope, &1, fun)} + # def reduce([], {:cont, acc}, _fun), do: {:done, acc} + # def reduce([h | t], {:cont, acc}, fun), do: reduce(t, fun.(h, acc), fun) + + def reduce(%Rnode{right: right, left: left}, {:cont, acc}, fun) do + acc = reduce(left, acc, fun) + reduce(right, acc, fun) end - end - @doc false - def reduce_leaves(rnode(right: right, left: left), acc, fun) do - acc = reduce_leaves(left, acc, fun) - reduce_leaves(right, acc, fun) - end - - def reduce_leaves(rleaf() = leaf, acc, fun) do - fun.(leaf, acc) - end - - def reduce_leaves(nil, acc, _fun) do - acc - end - - - defimpl Inspect, for: Rope do - @doc """ - Traveres the leaf nodes and converts the chunks of binary data into a single - algebra document. Will convert '\n' characters into algebra document line breaks. - """ - def inspect(rope, _opts) do - Rope.to_algebra_doc(rope) + def reduce(%Rnode{right: right, left: left}, acc, fun) do + acc = reduce(left, acc, fun) + reduce(right, acc, fun) end - end - @doc false - def to_algebra_doc(rnode(left: nil, right: right)) do - to_algebra_doc(right) - end + def reduce(%Rleaf{} = leaf, {:cont, acc}, fun) do + fun.(leaf, acc) + end - def to_algebra_doc(rnode(left: left, right: nil)) do - to_algebra_doc(left) + def reduce(%Rleaf{} = leaf, acc, fun) do + fun.(leaf, acc) + end end - def to_algebra_doc(rnode(left: left, right: right)) do - Inspect.Algebra.concat to_algebra_doc(left), to_algebra_doc(right) + defimpl Enumerable, for: Rope.Rnode do + defdelegate reduce(rope, acc, fun), to: Enumerable.Rope.Rleaf + defdelegate count(rope), to: Enumerable.Rope.Rleaf + defdelegate member?(rope, value), to: Enumerable.Rope.Rleaf end - def to_algebra_doc(rleaf(value: value)) do - [h|tail] = String.split(value, "\n") - Enum.reduce(tail, h, fn(next, last) -> Inspect.Algebra.line(last, next) end) - end + # defimpl Inspect, for: Rope.Rleaf do + # @doc """ + # Traveres the leaf nodes and converts the chunks of binary data into a single + # algebra document. Will convert '\n' characters into algebra document line breaks. + # """ + # def inspect(rope, _opts) do + # Apex.Format.format(rope, []) + # end + # end + # defimpl Inspect, for: Rope.Rnode do + # defdelegate inspect(rope, opts), to: Inspect.Rope.Rleaf + # end end diff --git a/mix.exs b/mix.exs index 606bbc0..63e8285 100644 --- a/mix.exs +++ b/mix.exs @@ -1,38 +1,35 @@ defmodule Rope.Mixfile do use Mix.Project - def version do - "v0.1.1" - end - - def source_url do - "https://github.com/copenhas/ropex" - end - def project do - [ app: :rope, - version: version, - elixir: "~> 0.10.0", + [ + app: :rope, + version: "0.1.2", + elixir: "~> 1.5", + start_permanent: Mix.env == :prod, + deps: deps(), + + # Docs name: "ropex", - source_url: source_url, - deps: deps, - docs: [ - source_url_pattern: "#{source_url}/blob/#{version}/%{path}#L%{line}" - ] + source_url: "https://github.com/ijcd/ropex", ] end - # Configuration for the OTP application + # Run "mix help compile.app" to learn about applications. def application do [ + extra_applications: [:logger] ] end - # Returns the list of dependencies in the format: - # { :foobar, "0.1", git: "https://github.com/elixir-lang/foobar.git" } + # Run "mix help deps" to learn about dependencies. defp deps do [ - { :ex_doc, github: "elixir-lang/ex_doc" } + {:apex, "~>1.0.0"}, + {:mix_test_watch, "~> 0.3", only: :dev, runtime: false}, + {:credo, "~> 0.8.5"}, + {:dialyxir, "~> 0.5", only: [:dev, :test], runtime: false}, + # {:stream_data, "~> 0.2.0"}, ] end end diff --git a/mix.lock b/mix.lock index 912387f..79ba3c2 100644 --- a/mix.lock +++ b/mix.lock @@ -1 +1,8 @@ -[ "ex_doc": {:git, "git://github.com/elixir-lang/ex_doc.git", "c13d10aaed19ef05c2c8362b9b555926e9770902", []} ] +%{"apex": {:hex, :apex, "1.0.0", "abf230314d35ca4c48a902f693247f190ad42fc14862b9c4f7dbb7077b21c20a", [:mix], [], "hexpm"}, + "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, + "credo": {:hex, :credo, "0.8.6", "335f723772d35da499b5ebfdaf6b426bfb73590b6fcbc8908d476b75f8cbca3f", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"}, + "dialyxir": {:hex, :dialyxir, "0.5.1", "b331b091720fd93e878137add264bac4f644e1ddae07a70bf7062c7862c4b952", [:mix], [], "hexpm"}, + "ex_machina": {:hex, :ex_machina, "2.0.0", "ec284c6f57233729cea9319e083f66e613e82549f78eccdb2059aeba5d0df9f3", [:mix], [{:ecto, "~> 2.1", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, + "fs": {:hex, :fs, "0.9.2", "ed17036c26c3f70ac49781ed9220a50c36775c6ca2cf8182d123b6566e49ec59", [:rebar], [], "hexpm"}, + "mix_test_watch": {:hex, :mix_test_watch, "0.5.0", "2c322d119a4795c3431380fca2bca5afa4dc07324bd3c0b9f6b2efbdd99f5ed3", [:mix], [{:fs, "~> 0.9.1", [hex: :fs, repo: "hexpm", optional: false]}], "hexpm"}, + "stream_data": {:hex, :stream_data, "0.2.0", "887b7701cd4ea235e0d704ce60f86096ff5754dae55c3ead4e1d43f152a9239e", [:mix], [], "hexpm"}} diff --git a/package.exs b/package.exs deleted file mode 100644 index cb17f7d..0000000 --- a/package.exs +++ /dev/null @@ -1,8 +0,0 @@ -Expm.Package.new( - name: "ropex", - description: "Rope in elixir providing faster index based operations then binaries.", - version: "0.1.1", - keywords: ["string", "data structure"], - maintainers: [[name: "Sean Copenhaver", email: "sean.copenhaver+expm@gmail.com"]], - repositories: [[github: "copenhas/ropex", tag: "v0.1.1"]] -) diff --git a/test/doc_test.exs b/test/doc_test.exs deleted file mode 100644 index dafbd19..0000000 --- a/test/doc_test.exs +++ /dev/null @@ -1,8 +0,0 @@ -Code.require_file "test_helper.exs", __DIR__ - -defmodule DocTest do - use ExUnit.Case - - doctest Rope - -end diff --git a/test/performance.exs b/test/performance.exs index 01b4a56..116a34b 100644 --- a/test/performance.exs +++ b/test/performance.exs @@ -2,8 +2,9 @@ Code.require_file "test_helper.exs", __DIR__ defmodule PerformanceTest do use ExUnit.Case + import Record - defrecord TestCtxt, + Record.defrecord TestCtxt, rope: nil, extra: [], time: 0 @@ -25,7 +26,7 @@ defmodule PerformanceTest do IO.puts "\nSMALL ROPE: concat without rebalance takes #{avg} microseconds" assert avg < threshold, "concats avg of #{avg} microseconds, longer then threshold of #{threshold} microseconds" - threshold = 1_700 + threshold = 1_700 avg = rope |> build_ctxt |> run(10, :slice) IO.puts "\nSMALL ROPE: slice takes #{avg} microseconds" assert avg < threshold, "slices on a balanced rope avg of #{avg} microseconds, longer then threshold of #{threshold} microseconds" @@ -45,7 +46,7 @@ defmodule PerformanceTest do IO.puts "\nHUGE ROPE: concat with no rebalance takes #{avg} microseconds" assert avg < threshold, "concats avg of #{avg} microseconds, longer then threshold of #{threshold} microseconds" - threshold = 4_000 + threshold = 4_000 avg = rope |> build_ctxt |> run(10, :slice) IO.puts "\nHUGE ROPE: slice takes #{avg} microseconds" assert avg < threshold, "slice on a balanced rope avg of #{avg} microseconds, longer then threshold of #{threshold} microseconds" @@ -101,7 +102,7 @@ defmodule PerformanceTest do #gotta preserve the newlines [first | rest] = String.split(text, "\n") rest - |> Enum.reduce(first, fn(line, rope) -> + |> Enum.reduce(first, fn(line, rope) -> Rope.concat([rope, "\n" <> line], rebalance: false) end) |> Rope.rebalance @@ -140,12 +141,12 @@ defmodule PerformanceTest do finished.time / num end - def generate_args(:slice, TestCtxt[rope: rope]) + def generate_args(:slice, TestCtxt[rope: rope]) when is_record(rope, Rope) do {:random.uniform(div(rope.length, 2)), :random.uniform(rope.length) + 100} end - def generate_args(:slice, TestCtxt[rope: text]) + def generate_args(:slice, TestCtxt[rope: text]) when is_binary(text) do {:random.uniform(div(String.length(text), 2)), :random.uniform(String.length(text)) + 100} end @@ -186,25 +187,25 @@ defmodule PerformanceTest do ########################### # Rope operations ########################### - def execute_operation({:slice, {start, len}}, TestCtxt[rope: rope] = ctxt) + def execute_operation({:slice, {start, len}}, TestCtxt[rope: rope] = ctxt) when is_record(rope, Rope) do Rope.slice(rope, start, len) ctxt #leaving the context unchanged end - def execute_operation({:concat, word}, TestCtxt[rope: rope] = ctxt) + def execute_operation({:concat, word}, TestCtxt[rope: rope] = ctxt) when is_record(rope, Rope) do newRope = Rope.concat([rope | [word]]) ctxt.rope newRope end - def execute_operation({:concat_no_rebalance, word}, TestCtxt[rope: rope] = ctxt) + def execute_operation({:concat_no_rebalance, word}, TestCtxt[rope: rope] = ctxt) when is_record(rope, Rope) do newRope = Rope.concat([rope, word], rebalance: false) ctxt.rope newRope end - def execute_operation({:find, term}, TestCtxt[rope: rope] = ctxt) + def execute_operation({:find, term}, TestCtxt[rope: rope] = ctxt) when is_record(rope, Rope) do Rope.find(rope, term) ctxt #no change, and find returns the index @@ -218,19 +219,19 @@ end ############################ # String operations for comparison ############################ - def execute_operation({:slice, {start, len}}, TestCtxt[rope: rope] = ctxt) + def execute_operation({:slice, {start, len}}, TestCtxt[rope: rope] = ctxt) when is_binary(rope) do String.slice(rope, start, len) ctxt #leaving the context unchanged end - def execute_operation({:concat, word}, TestCtxt[rope: rope] = ctxt) + def execute_operation({:concat, word}, TestCtxt[rope: rope] = ctxt) when is_binary(rope) do newRope = rope <> word ctxt.rope newRope end - def execute_operation({:find, term}, TestCtxt[rope: rope] = ctxt) + def execute_operation({:find, term}, TestCtxt[rope: rope] = ctxt) when is_binary(rope) do String.contains?(rope, term) ctxt #no change, and find returns the index diff --git a/test/rope_test.exs b/test/rope_test.exs index 2898b96..54ec7d9 100644 --- a/test/rope_test.exs +++ b/test/rope_test.exs @@ -1,33 +1,67 @@ +defmodule RopeTestMacros do + @moduledoc false + + defmacro assert_rope_equal(left, right) do + quote bind_quoted: [left: left, right: right] do + left_value = value_for(left) + right_value = value_for(right) + + left_length = length_for(left) + right_length = length_for(right) + + assert left_value == right_value + assert left_length == right_length + end + end + + def value_for(nil), do: "" + def value_for(%Rope.Rleaf{} = rope), do: Rope.to_string(rope) + def value_for(%Rope.Rnode{} = rope), do: Rope.to_string(rope) + def value_for(str) when is_binary(str), do: str + + + def length_for(nil), do: 0 + def length_for(%Rope.Rleaf{} = rope), do: Rope.length(rope) + def length_for(%Rope.Rnode{} = rope), do: Rope.length(rope) + def length_for(str) when is_binary(str), do: String.length(str) +end + defmodule RopeTest do use ExUnit.Case + import RopeTestMacros + + # doctest Rope + + require Record + @simple "hello world" @text "Have you any idea how much damage that bulldozer would suffer if I just let it roll straight over you?" @longtext File.read!("test/fixtures/towels.txt") test "can create a basic rope" do rope = Rope.new(@simple) - is_equal rope, @simple + assert_rope_equal rope, @simple rope = Rope.new(@text) - is_equal rope, @text + assert_rope_equal rope, @text end test "can concat two single node ropes together" do rope = build_rope @simple - is_equal rope, "hello world" + assert_rope_equal rope, "hello world" end test "can concat two strings together" do rope = Rope.concat(["hello", " world"]) - is_equal rope, "hello world" + assert_rope_equal rope, "hello world" end test "can concat a single node rope and a string" do rope = Rope.new("hello") rope = Rope.concat([rope, " world"]) - is_equal rope, "hello world" + assert_rope_equal rope, "hello world" end test "can concat a multi-node rope and a string together" do @@ -37,58 +71,58 @@ defmodule RopeTest do rope = Rope.concat([rope, str]) - is_equal rope, @text + assert_rope_equal rope, @text end test "can concat a lot" do rope = build_rope @longtext - is_equal rope, @longtext + assert_rope_equal rope, @longtext end test "concat handles nils" do rope = Rope.concat([nil, "test"]) - is_equal rope, "test" + assert_rope_equal rope, "test" rope = Rope.concat(["test", nil]) - is_equal rope, "test" + assert_rope_equal rope, "test" end test "slice with a start greater then the rope length returns the same as String.slice/3" do rope = Rope.new @simple - assert Rope.slice(rope, 50, 10) == String.slice(@simple, 50, 10) + assert_rope_equal Rope.slice(rope, 50, 10), String.slice(@simple, 50, 10) rope = build_rope @simple - assert Rope.slice(rope, 120, 10) == String.slice(@simple, 120, 10) + assert_rope_equal Rope.slice(rope, 120, 10), String.slice(@simple, 120, 10) end test "slice with start equal to the rope returns the same as String.slice/3" do rope = Rope.new "test" - is_equal Rope.slice(rope, 4, 10), String.slice("test", 4, 10) + assert_rope_equal Rope.slice(rope, 4, 10), String.slice("test", 4, 10) rope = Rope.concat ["hello", " world"] length = String.length @simple - is_equal Rope.slice(rope, length, 10), String.slice(@simple, length, 10) + assert_rope_equal Rope.slice(rope, length, 10), String.slice(@simple, length, 10) end test "slice works on single node ropes" do rope = Rope.new "test" - is_equal Rope.slice(rope, 2, 1), "s" + assert_rope_equal Rope.slice(rope, 2, 1), "s" end test "slice works on multi-node ropes" do rope = build_rope @simple - is_equal Rope.slice(rope, 3, 5), String.slice(@simple, 3, 5) + assert_rope_equal Rope.slice(rope, 3, 5), String.slice(@simple, 3, 5) end test "can get slice from middle of text" do rope = build_rope @longtext - is_equal Rope.slice(rope, 231, 15), String.slice(@longtext, 231, 15) + assert_rope_equal Rope.slice(rope, 231, 15), String.slice(@longtext, 231, 15) end test "rebalancing shouldn't effect a slice" do rope = build_rope @longtext rope = Rope.rebalance(rope) - is_equal Rope.slice(rope, 231, 15), String.slice(@longtext, 231, 15) + assert_rope_equal Rope.slice(rope, 231, 15), String.slice(@longtext, 231, 15) end test "get the length of a rope" do @@ -115,7 +149,7 @@ defmodule RopeTest do assert Rope.depth(rope) == 185 rope = Rope.rebalance rope - is_equal rope, @longtext + assert_rope_equal rope, @longtext assert Rope.depth(rope) == 8 end @@ -124,7 +158,7 @@ defmodule RopeTest do rope1= Rope.rebalance rope rope2 = Rope.rebalance rope1 - is_equal rope1, rope2 + assert_rope_equal rope1, rope2 assert Rope.depth(rope1) == Rope.depth(rope2) end @@ -136,7 +170,7 @@ defmodule RopeTest do rope = Rope.concat([rope, build_rope(@longtext)]) ropebalanced = Rope.rebalance(rope) - is_equal rope, ropebalanced + assert_rope_equal rope, ropebalanced end test "find returns the index the search term begins at" do @@ -147,7 +181,7 @@ defmodule RopeTest do index = Rope.find(rope, "towels") subrope = Rope.slice(rope, index, String.length("towels")) - is_equal subrope, "towels" + assert_rope_equal subrope, "towels" end test "find returns -1 if the term could not be found" do @@ -180,33 +214,52 @@ defmodule RopeTest do orig = @text |> build_rope |> Rope.rebalance rope = Rope.replace(orig, "you", "me", global: false) - is_equal rope, String.replace(@text, "you", "me", global: false) + assert_rope_equal rope, String.replace(@text, "you", "me", global: false) rope = Rope.replace(orig, "you", "me") - is_equal rope, String.replace(@text, "you", "me") + assert_rope_equal rope, String.replace(@text, "you", "me") rope = @longtext |> build_rope |> Rope.rebalance rope = Rope.replace(rope, "towel", "duck") - is_equal rope, String.replace(@longtext, "towel", "duck") + assert_rope_equal rope, String.replace(@longtext, "towel", "duck") end test "insert_at allows creating a new rope with the text added" do orig = build_rope "Beware of the Leopard" - is_equal Rope.insert_at(orig, 63, " END"), "Beware of the Leopard END" - is_equal Rope.insert_at(orig, -8, " MIDDLE"), "Beware of the MIDDLE Leopard" - is_equal Rope.insert_at(orig, 2, "SPLIT"), "BeSPLITware of the Leopard" + assert_rope_equal Rope.insert_at(orig, 63, " END"), "Beware of the Leopard END" + assert_rope_equal Rope.insert_at(orig, -8, " MIDDLE"), "Beware of the MIDDLE Leopard" + assert_rope_equal Rope.insert_at(orig, 2, "SPLIT"), "BeSPLITware of the Leopard" end test "remove_at allows removing a substr of the rope" do orig = build_rope "Beware of the Leopard" - is_equal Rope.remove_at(orig, 63, 10), "Beware of the Leopard" - is_equal Rope.remove_at(orig, -7, 3), "Beware of the pard" - is_equal Rope.remove_at(orig, 2, 5), "Beof the Leopard" + assert_rope_equal Rope.remove_at(orig, 63, 10), "Beware of the Leopard" + assert_rope_equal Rope.remove_at(orig, -7, 3), "Beware of the pard" + assert_rope_equal Rope.remove_at(orig, 2, 5), "Beof the Leopard" + end + + test "Enumerable.count" do + rope = Rope.new(@simple) + assert Enum.count(rope) == 1 + + rope = Rope.concat(["hello", " world"]) + assert Enum.count(rope) == 2 + end + + test "Enumerable.member?" do + rope = Rope.new(@simple) + assert Enum.member?(rope, "hello world") + refute Enum.member?(rope, "hello") + + rope = Rope.concat(["hello", " world"]) + assert Enum.member?(rope, "hello") + assert Enum.member?(rope, " world") + refute Enum.member?(rope, "hello world") end - defp build_rope(text, opts // []) do + defp build_rope(text, opts \\ []) do words = text |> String.split(" ") @@ -214,28 +267,8 @@ defmodule RopeTest do words |> Enum.drop(1) - |> Enum.reduce(Rope.new(first), fn (word, rope) -> - Rope.concat([rope, " " <> word], opts) + |> Enum.reduce(Rope.new(first), fn (word, rope) -> + Rope.concat([rope, " " <> word], opts) end) end - - defp is_equal(rope, str) - when is_record(rope, Rope) and is_binary(str) do - assert rope_value(rope) == str - assert Rope.length(rope) == String.length(str) - end - - defp is_equal(rope1, rope2) - when is_record(rope1, Rope) and is_record(rope2, Rope) do - assert rope_value(rope1) == rope_value(rope2) - assert Rope.length(rope1) == Rope.length(rope2) - end - - defp is_equal(thing1, thing2) do - assert thing1 == thing2 - end - - defp rope_value(rope) do - Kernel.inspect rope - end end diff --git a/test/test_helper.exs b/test/test_helper.exs index 4b8b246..869559e 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -1 +1 @@ -ExUnit.start +ExUnit.start() diff --git a/ttb_last_config b/ttb_last_config new file mode 100644 index 0000000..b708383 Binary files /dev/null and b/ttb_last_config differ