Browse Source

Merge pull request #2 from ijcd/master

Update for Elixir 1.5.1
master
Sean Copenhaver 7 years ago
committed by GitHub
parent
commit
5da9cafd26
  1. 25
      .gitignore
  2. 39
      config/config.exs
  3. 311
      lib/rope.ex
  4. 37
      mix.exs
  5. 9
      mix.lock
  6. 8
      package.exs
  7. 8
      test/doc_test.exs
  8. 27
      test/performance.exs
  9. 139
      test/rope_test.exs
  10. 2
      test/test_helper.exs
  11. BIN
      ttb_last_config

25
.gitignore

@ -1,9 +1,20 @@
/ebin # The directory Mix will write compiled artifacts to.
/deps /_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 erl_crash.dump
# Also ignore archive artifacts (built via "mix archive.build").
*.ez *.ez
*.swp
docs
tags
.DS_Store
graphs

39
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

311
lib/rope.ex

@ -12,7 +12,7 @@ defmodule Rope do
4. Should be able to handle alternate representations (ex: IO stream) - we'll see 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 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. parent/concatentation nodes are purely used to link the various leaf nodes together.
The concatentation nodes contain basic tree information. The concatentation nodes contain basic tree information.
@ -35,24 +35,43 @@ defmodule Rope do
- https://en.wikipedia.org/wiki/Rope_\(data_structure\) - https://en.wikipedia.org/wiki/Rope_\(data_structure\)
""" """
require Record
@partition_size_threshold 1000 @partition_size_threshold 1000
@partition_term_ratio 0.1 @partition_term_ratio 0.1
@partition_depth_limit 3 @partition_depth_limit 3
defmodule Rnode do
defstruct [
length: 0,
depth: 1,
left: nil,
right: nil
]
defrecordp :rnode, Rope, @type t :: %__MODULE__{
length: 0 :: non_neg_integer, length: non_neg_integer,
depth: 1 :: non_neg_integer, depth: non_neg_integer,
left: nil :: rope, left: Rope.t,
right: nil :: rope right: Rope.t
}
end
defmodule Rleaf do
defstruct [
length: 0,
depth: 0,
value: nil
]
defrecordp :rleaf, Rope, @type t :: %__MODULE__{
length: 0 :: non_neg_integer, length: non_neg_integer,
depth: 0 :: non_neg_integer, depth: non_neg_integer,
value: nil :: binary 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 # copied the type defs from the String module
@type str :: binary @type str :: binary
@ -69,13 +88,13 @@ defmodule Rope do
iex> Rope.new("Don't panic") |> Rope.to_string iex> Rope.new("Don't panic") |> Rope.to_string
"Don't panic" "Don't panic"
""" """
@spec new(str | nil) :: rope @spec new(str | nil) :: t
def new(nil) do def new(nil) do
nil nil
end end
def new(str) when is_binary(str) do def new(str) when is_binary(str) do
rleaf(length: String.length(str), value: str) %Rleaf{length: String.length(str), value: str}
end end
@doc """ @doc """
@ -91,8 +110,9 @@ defmodule Rope do
iex> Rope.concat([Rope.new("terrible"), " ghastly", " silence"]) |> Rope.to_string iex> Rope.concat([Rope.new("terrible"), " ghastly", " silence"]) |> Rope.to_string
"terrible ghastly silence" "terrible ghastly silence"
""" """
@spec concat(list(rope | str), list) :: rope @spec concat(list(t | str), list) :: t
def concat([first | rest], opts // []) do def concat(_rope, _opts \\ [])
def concat([first | rest], opts) do
Enum.reduce(rest, ropeify(first), fn(right, left) -> do_concat(left, right, opts) end) Enum.reduce(rest, ropeify(first), fn(right, left) -> do_concat(left, right, opts) end)
end end
@ -102,36 +122,36 @@ defmodule Rope do
@doc """ @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. 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. 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 def slice(nil, _start, _len) do
nil nil
end end
def slice(_rope, start, len) when len == 0 do def slice(_rope, _start, len) when len == 0 do
ropeify "" ropeify ""
end end
def slice(rnode(length: rlen), start, len) def slice(%Rnode{length: rlen}, start, len)
when start == rlen and len > 0 do when start == rlen and len > 0 do
ropeify "" ropeify ""
end 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 when start == 0 and rlen <= len and len > 0 do
node node
end end
def slice(rnode(length: rlen), start, len) def slice(%Rnode{length: rlen}, start, len)
when start > rlen and len > 0 do when start > rlen and len > 0 do
nil nil
end 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 when len > 0 do
if start == 0 and rlen <= len do if start == 0 and rlen <= len do
leaf leaf
@ -141,12 +161,15 @@ defmodule Rope do
end end
def slice(rope, start, len) when len > 0 do def slice(rope, start, len) when len > 0 do
rnode(left: left, %Rnode{left: left,
right: right) = rope right: right} = rope
if start < 0 do start =
start = rope.length + start if start < 0 do
end rope.length + start
else
start
end
{startRight, lenRight} = {startRight, lenRight} =
if start < left.length do if start < left.length do
@ -169,7 +192,7 @@ defmodule Rope do
Checks if the rope is considered balanced based on comparing total length Checks if the rope is considered balanced based on comparing total length
and depth verses the fibanocci sequence. and depth verses the fibanocci sequence.
""" """
@spec balanced?(rope) :: bool @spec balanced?(t) :: boolean
def balanced?(nil) do def balanced?(nil) do
true true
end end
@ -186,13 +209,17 @@ defmodule Rope do
efficient. This is a pretty greedy rebalancing and should produce efficient. This is a pretty greedy rebalancing and should produce
a fully balanced rope. a fully balanced rope.
""" """
@spec rebalance(rope) :: rope @spec rebalance(t) :: t
def rebalance(nil) do def rebalance(nil) do
nil nil
end end
def rebalance(rope) when is_record(rope, Rope) do def rebalance(%Rleaf{} = rope), do: rebalance_rope(rope)
leaves = 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.reduce([], fn(leaf, acc) -> [leaf | acc] end)
|> Enum.reverse |> Enum.reverse
@ -207,12 +234,12 @@ defmodule Rope do
iex> Rope.length(Rope.concat([Rope.new("terrible"), " ghastly silence"])) iex> Rope.length(Rope.concat([Rope.new("terrible"), " ghastly silence"]))
24 24
""" """
@spec length(rope) :: non_neg_integer @spec length(t) :: non_neg_integer
def length(rope) do def length(rope) do
case rope do case rope do
nil -> 0 nil -> 0
rleaf(length: len) -> len %Rleaf{length: len} -> len
rnode(length: len) -> len %Rnode{length: len} -> len
end end
end end
@ -232,12 +259,12 @@ defmodule Rope do
iex> Rope.depth(Rope.concat([Rope.new("terrible"), " ghastly", " silence"])) iex> Rope.depth(Rope.concat([Rope.new("terrible"), " ghastly", " silence"]))
2 2
""" """
@spec depth(rope) :: non_neg_integer @spec depth(t) :: non_neg_integer
def depth(rope) do def depth(rope) do
case rope do case rope do
nil -> 0 nil -> 0
rnode(depth: depth) -> depth %Rnode{depth: depth} -> depth
rleaf(depth: depth) -> depth %Rleaf{depth: depth} -> depth
end end
end end
@ -253,15 +280,18 @@ defmodule Rope do
iex> Rope.insert_at(Rope.concat(["infinite ", "number ", "monkeys"]), -7, "of ") |> Rope.to_string iex> Rope.insert_at(Rope.concat(["infinite ", "number ", "monkeys"]), -7, "of ") |> Rope.to_string
"infinite number of monkeys" "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 def insert_at(nil, _index, str) do
ropeify(str) ropeify(str)
end end
def insert_at(rope, index, str) do def insert_at(rope, index, str) do
if index < 0 do index =
index = rope.length + index if index < 0 do
end rope.length + index
else
index
end
left = slice(rope, 0, index) left = slice(rope, 0, index)
right = slice(rope, index, rope.length) right = slice(rope, index, rope.length)
@ -270,8 +300,8 @@ defmodule Rope do
end end
@doc """ @doc """
Produces a new rope with the substr defined by the starting index and the length of 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 characters removed. The advantage of this is it takes full advantage of ropes
being optimized for index based operation. being optimized for index based operation.
## Examples ## Examples
@ -282,15 +312,18 @@ defmodule Rope do
iex> Rope.remove_at(Rope.concat(["infinite ", "number of ", "monkeys"]), -7, 3) |> Rope.to_string iex> Rope.remove_at(Rope.concat(["infinite ", "number of ", "monkeys"]), -7, 3) |> Rope.to_string
"infinite number of keys" "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 def remove_at(nil, _index, _len) do
nil nil
end end
def remove_at(rope, index, len) do def remove_at(rope, index, len) do
if index < 0 do index =
index = rope.length + index if index < 0 do
end rope.length + index
else
index
end
left = slice(rope, 0, index) left = slice(rope, 0, index)
right = slice(rope, index + len, rope.length) 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") iex> Rope.find(Rope.concat(["loathe it", " or ignore it,", " you can't like it"]), "and")
-1 -1
""" """
@spec find(rope, str) :: integer @spec find(t, str) :: integer
def find(nil, _term) do def find(nil, _term) do
-1 -1
end end
defrecordp :findctxt, Record.defrecordp :findctxt,
findIndex: 0 :: non_neg_integer, #where the match started in the rope findIndex: 0, #where the match started in the rope
termIndex: 0 :: non_neg_integer #next index to try 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 def find(rope, term) do
termLen = String.length(term) termLen = String.length(term)
@ -324,12 +362,12 @@ defmodule Rope do
foundMatch = fn(findctxt(termIndex: i)) -> i == termLen end foundMatch = fn(findctxt(termIndex: i)) -> i == termLen end
{_offset, possibles} = do_reduce_while(rope, { 0, []}, {_offset, possibles} = do_reduce_while(rope, { 0, []},
fn(leaf, {_offset, possibles}) -> fn(_leaf, {_offset, possibles}) ->
# it wil continue so long as we retruen true # it wil continue so long as we retruen true
# we want to stop when we have a match # we want to stop when we have a match
not Enum.any?(possibles, foundMatch) not Enum.any?(possibles, foundMatch)
end, 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)} {offset + len, build_possible_matches(offset, chunk, term, possibles)}
end end
) )
@ -338,7 +376,7 @@ defmodule Rope do
|> Enum.filter(foundMatch) |> Enum.filter(foundMatch)
|> Enum.map(fn(findctxt(findIndex: i)) -> i end) |> Enum.map(fn(findctxt(findIndex: i)) -> i end)
|> Enum.reverse |> Enum.reverse
|> Enum.first |> Enum.at(0)
if match == nil do if match == nil do
-1 -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") 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 def find_all(rope, term) do
termLength = String.length(term) termLength = String.length(term)
segments = partition_rope(0, rope, termLength, 0) segments = partition_rope(0, rope, termLength, 0)
@ -369,18 +407,18 @@ defmodule Rope do
segments segments
|> Enum.map(fn({offset, length}) -> |> Enum.map(fn({offset, length}) ->
slice = Rope.slice(rope, offset, length) slice = Rope.slice(rope, offset, length)
ref = make_ref ref = make_ref()
spawn_link(fn() -> spawn_link(fn() ->
matches = do_find_all(slice, term) matches = do_find_all(slice, term)
|> Enum.map(fn(match) -> match + offset end) |> Enum.map(fn(match) -> match + offset end)
parent <- {ref, :pfind, self(), matches} send parent, {ref, :pfind, self(), matches}
end) end)
end) end)
|> Enum.map(fn(child) -> |> Enum.map(fn(_child) ->
receive do receive do
{ref, :pfind, child, matches} -> {_ref, :pfind, _child, matches} ->
matches matches
end end
end) end)
@ -392,11 +430,11 @@ defmodule Rope do
@doc """ @doc """
Replaces the first match with the replacement text and returns Replaces the first match with the replacement text and returns
the new rope. If not found then the existing rope is returned. 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. is set to false.
""" """
@spec replace(rope, str, str, list) :: rope @spec replace(t, str, str, list) :: t
def replace(rope, pattern, replacement, opts // []) do def replace(rope, pattern, replacement, opts \\ []) do
global = Keyword.get(opts, :global, true) global = Keyword.get(opts, :global, true)
if global do if global do
@ -409,20 +447,19 @@ defmodule Rope do
@doc """ @doc """
Converts the entire rope to a single binary. Converts the entire rope to a single binary.
""" """
@spec to_string(rope) :: binary @spec to_string(t) :: binary
def to_string(rope) do def to_string(rope) do
rope rope
|> Stream.map(fn(rleaf(value: value)) -> value end) |> Enum.map(fn(%Rleaf{value: value}) -> value end)
|> Enum.join |> Enum.join
end end
defp ropeify(rope) do defp ropeify(rope) do
case rope do case rope do
rnode() -> rope %Rnode{} -> rope
rleaf() -> rope %Rleaf{} -> rope
<<_ :: binary>> -> <<_ :: binary>> -> Rope.new(rope)
Rope.new(rope)
nil -> nil nil -> nil
end end
end end
@ -432,28 +469,27 @@ defmodule Rope do
end end
defp do_concat(rope, nil, _opts) do defp do_concat(rope, nil, _opts) do
ropeify rope ropeify(rope)
end end
defp do_concat(nil, rope, _opts) do defp do_concat(nil, rope, _opts) do
ropeify rope ropeify(rope)
end end
defp do_concat(rope1, rope2, opts) do defp do_concat(rope1, rope2, opts) do
rebalance = Keyword.get(opts, :rebalance, true) rebalance = Keyword.get(opts, :rebalance, true)
rope1 = ropeify rope1 rope1 = ropeify(rope1)
rope2 = ropeify rope2 rope2 = ropeify(rope2)
depth = rope1.depth depth = max(rope2.depth, rope1.depth)
if rope2.depth > depth do
depth = rope2.depth
end
rope = rnode(depth: depth + 1, rope = %Rnode{
left: rope1, depth: depth + 1,
right: rope2, left: rope1,
length: rope1.length + rope2.length) right: rope2,
length: rope1.length + rope2.length
}
if rebalance and (not Rope.balanced?(rope)) do if rebalance and (not Rope.balanced?(rope)) do
Rope.rebalance(rope) Rope.rebalance(rope)
@ -499,14 +535,17 @@ defmodule Rope do
[] []
end end
defp partition_rope(offset, rleaf(length: len), termLength, _depth) do defp partition_rope(offset, %Rleaf{length: len}, termLength, _depth) do
[{offset, len + termLength}] [{offset, len + termLength}]
end end
defp partition_rope(offset, rnode(length: len, right: right, left: left), termLength, depth) do defp partition_rope(offset, %Rnode{length: len, right: right, left: left}, termLength, depth) do
if offset > termLength do offset =
offset = offset - termLength if offset > termLength do
end offset - termLength
else
offset
end
cond do cond do
depth >= @partition_depth_limit -> depth >= @partition_depth_limit ->
@ -531,12 +570,12 @@ defmodule Rope do
foundMatch = fn(findctxt(termIndex: i)) -> i == termLen end foundMatch = fn(findctxt(termIndex: i)) -> i == termLen end
{_offset, possibles} = Enum.reduce(rope, { 0, []}, {_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)} {offset + len, build_possible_matches(offset, chunk, term, possibles)}
end end
) )
match = possibles possibles
|> Enum.filter(foundMatch) |> Enum.filter(foundMatch)
|> Enum.map(fn(findctxt(findIndex: i)) -> i end) |> Enum.map(fn(findctxt(findIndex: i)) -> i end)
|> Enum.reverse |> Enum.reverse
@ -558,7 +597,7 @@ defmodule Rope do
{offset, subropes} = Enum.reduce(indexes, {0, []}, fn(index, {offset, ropes}) -> {offset, subropes} = Enum.reduce(indexes, {0, []}, fn(index, {offset, ropes}) ->
len = index - offset len = index - offset
if offset != 0, do: len = len # if offset != 0, do: len = len
leftRope = slice(rope, offset, len) leftRope = slice(rope, offset, len)
{index + termLen, [replacement | [leftRope | ropes]]} {index + termLen, [replacement | [leftRope | ropes]]}
@ -567,8 +606,8 @@ defmodule Rope do
leftRope = slice(rope, offset, rope.length) leftRope = slice(rope, offset, rope.length)
subropes = [leftRope | subropes] subropes = [leftRope | subropes]
subropes = Enum.reverse subropes subropes = Enum.reverse(subropes)
rebuild_rope [], subropes rebuild_rope([], subropes)
end end
defp do_reduce_while(enumerable, acc, whiler, reducer) do defp do_reduce_while(enumerable, acc, whiler, reducer) do
@ -586,7 +625,7 @@ defmodule Rope do
end end
defp rebuild_rope(subropes, [leaf1, leaf2 | leaves]) do 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) rebuild_rope([subrope | subropes], leaves)
end end
@ -620,7 +659,7 @@ defmodule Rope do
end end
defimpl String.Chars, for: Rope do defimpl String.Chars do
@doc """ @doc """
Converts the entire rope to a single binary string. Converts the entire rope to a single binary string.
""" """
@ -630,7 +669,7 @@ defmodule Rope do
end end
defimpl Enumerable, for: Rope do defimpl Enumerable, for: Rope.Rleaf do
@moduledoc """ @moduledoc """
A convenience implementation that enumerates over the leaves of the rope but none A convenience implementation that enumerates over the leaves of the rope but none
of the parent/concatenation nodes. 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. A count of the leaf nodes in the rope. This current traverses the rope to count them.
""" """
def count(rope) do 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 end
@doc """ @doc """
Searches the ropes leaves in order for a match. Searches the ropes leaves in order for a match.
""" """
def member?(rope, value) do def member?(rope, value) do
try do {:ok, Enum.any?(rope, fn (leaf) -> leaf.value == value end)}
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
end end
@doc """ @doc """
Reduces over the leaf nodes. Reduces over the leaf nodes.
""" """
def reduce(rope, acc, fun) do def reduce(_, {:halt, acc}, _fun), do: {:halted, acc}
Rope.reduce_leaves(rope, acc, fun) 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
end
@doc false def reduce(%Rnode{right: right, left: left}, acc, fun) do
def reduce_leaves(rnode(right: right, left: left), acc, fun) do acc = reduce(left, acc, fun)
acc = reduce_leaves(left, acc, fun) reduce(right, 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)
end end
end
@doc false def reduce(%Rleaf{} = leaf, {:cont, acc}, fun) do
def to_algebra_doc(rnode(left: nil, right: right)) do fun.(leaf, acc)
to_algebra_doc(right) end
end
def to_algebra_doc(rnode(left: left, right: nil)) do def reduce(%Rleaf{} = leaf, acc, fun) do
to_algebra_doc(left) fun.(leaf, acc)
end
end end
def to_algebra_doc(rnode(left: left, right: right)) do defimpl Enumerable, for: Rope.Rnode do
Inspect.Algebra.concat to_algebra_doc(left), to_algebra_doc(right) 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 end
def to_algebra_doc(rleaf(value: value)) do # defimpl Inspect, for: Rope.Rleaf do
[h|tail] = String.split(value, "\n") # @doc """
Enum.reduce(tail, h, fn(next, last) -> Inspect.Algebra.line(last, next) end) # Traveres the leaf nodes and converts the chunks of binary data into a single
end # 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 end

37
mix.exs

@ -1,38 +1,35 @@
defmodule Rope.Mixfile do defmodule Rope.Mixfile do
use Mix.Project use Mix.Project
def version do
"v0.1.1"
end
def source_url do
"https://github.com/copenhas/ropex"
end
def project do def project do
[ app: :rope, [
version: version, app: :rope,
elixir: "~> 0.10.0", version: "0.1.2",
elixir: "~> 1.5",
start_permanent: Mix.env == :prod,
deps: deps(),
# Docs
name: "ropex", name: "ropex",
source_url: source_url, source_url: "https://github.com/ijcd/ropex",
deps: deps,
docs: [
source_url_pattern: "#{source_url}/blob/#{version}/%{path}#L%{line}"
]
] ]
end end
# Configuration for the OTP application # Run "mix help compile.app" to learn about applications.
def application do def application do
[ [
extra_applications: [:logger]
] ]
end end
# Returns the list of dependencies in the format: # Run "mix help deps" to learn about dependencies.
# { :foobar, "0.1", git: "https://github.com/elixir-lang/foobar.git" }
defp deps do 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
end end

9
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"}}

8
package.exs

@ -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"]]
)

8
test/doc_test.exs

@ -1,8 +0,0 @@
Code.require_file "test_helper.exs", __DIR__
defmodule DocTest do
use ExUnit.Case
doctest Rope
end

27
test/performance.exs

@ -2,8 +2,9 @@ Code.require_file "test_helper.exs", __DIR__
defmodule PerformanceTest do defmodule PerformanceTest do
use ExUnit.Case use ExUnit.Case
import Record
defrecord TestCtxt, Record.defrecord TestCtxt,
rope: nil, rope: nil,
extra: [], extra: [],
time: 0 time: 0
@ -25,7 +26,7 @@ defmodule PerformanceTest do
IO.puts "\nSMALL ROPE: concat without rebalance takes #{avg} microseconds" IO.puts "\nSMALL ROPE: concat without rebalance takes #{avg} microseconds"
assert avg < threshold, "concats avg of #{avg} microseconds, longer then threshold of #{threshold} 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) avg = rope |> build_ctxt |> run(10, :slice)
IO.puts "\nSMALL ROPE: slice takes #{avg} microseconds" 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" 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" 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" 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) avg = rope |> build_ctxt |> run(10, :slice)
IO.puts "\nHUGE ROPE: slice takes #{avg} microseconds" 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" 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 #gotta preserve the newlines
[first | rest] = String.split(text, "\n") [first | rest] = String.split(text, "\n")
rest rest
|> Enum.reduce(first, fn(line, rope) -> |> Enum.reduce(first, fn(line, rope) ->
Rope.concat([rope, "\n" <> line], rebalance: false) Rope.concat([rope, "\n" <> line], rebalance: false)
end) end)
|> Rope.rebalance |> Rope.rebalance
@ -140,12 +141,12 @@ defmodule PerformanceTest do
finished.time / num finished.time / num
end end
def generate_args(:slice, TestCtxt[rope: rope]) def generate_args(:slice, TestCtxt[rope: rope])
when is_record(rope, Rope) do when is_record(rope, Rope) do
{:random.uniform(div(rope.length, 2)), :random.uniform(rope.length) + 100} {:random.uniform(div(rope.length, 2)), :random.uniform(rope.length) + 100}
end end
def generate_args(:slice, TestCtxt[rope: text]) def generate_args(:slice, TestCtxt[rope: text])
when is_binary(text) do when is_binary(text) do
{:random.uniform(div(String.length(text), 2)), :random.uniform(String.length(text)) + 100} {:random.uniform(div(String.length(text), 2)), :random.uniform(String.length(text)) + 100}
end end
@ -186,25 +187,25 @@ defmodule PerformanceTest do
########################### ###########################
# Rope operations # 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 when is_record(rope, Rope) do
Rope.slice(rope, start, len) Rope.slice(rope, start, len)
ctxt #leaving the context unchanged ctxt #leaving the context unchanged
end 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 when is_record(rope, Rope) do
newRope = Rope.concat([rope | [word]]) newRope = Rope.concat([rope | [word]])
ctxt.rope newRope ctxt.rope newRope
end 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 when is_record(rope, Rope) do
newRope = Rope.concat([rope, word], rebalance: false) newRope = Rope.concat([rope, word], rebalance: false)
ctxt.rope newRope ctxt.rope newRope
end 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 when is_record(rope, Rope) do
Rope.find(rope, term) Rope.find(rope, term)
ctxt #no change, and find returns the index ctxt #no change, and find returns the index
@ -218,19 +219,19 @@ end
############################ ############################
# String operations for comparison # 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 when is_binary(rope) do
String.slice(rope, start, len) String.slice(rope, start, len)
ctxt #leaving the context unchanged ctxt #leaving the context unchanged
end end
def execute_operation({:concat, word}, TestCtxt[rope: rope] = ctxt) def execute_operation({:concat, word}, TestCtxt[rope: rope] = ctxt)
when is_binary(rope) do when is_binary(rope) do
newRope = rope <> word newRope = rope <> word
ctxt.rope newRope ctxt.rope newRope
end end
def execute_operation({:find, term}, TestCtxt[rope: rope] = ctxt) def execute_operation({:find, term}, TestCtxt[rope: rope] = ctxt)
when is_binary(rope) do when is_binary(rope) do
String.contains?(rope, term) String.contains?(rope, term)
ctxt #no change, and find returns the index ctxt #no change, and find returns the index

139
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 defmodule RopeTest do
use ExUnit.Case use ExUnit.Case
import RopeTestMacros
# doctest Rope
require Record
@simple "hello world" @simple "hello world"
@text "Have you any idea how much damage that bulldozer would suffer if I just let it roll straight over you?" @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") @longtext File.read!("test/fixtures/towels.txt")
test "can create a basic rope" do test "can create a basic rope" do
rope = Rope.new(@simple) rope = Rope.new(@simple)
is_equal rope, @simple assert_rope_equal rope, @simple
rope = Rope.new(@text) rope = Rope.new(@text)
is_equal rope, @text assert_rope_equal rope, @text
end end
test "can concat two single node ropes together" do test "can concat two single node ropes together" do
rope = build_rope @simple rope = build_rope @simple
is_equal rope, "hello world" assert_rope_equal rope, "hello world"
end end
test "can concat two strings together" do test "can concat two strings together" do
rope = Rope.concat(["hello", " world"]) rope = Rope.concat(["hello", " world"])
is_equal rope, "hello world" assert_rope_equal rope, "hello world"
end end
test "can concat a single node rope and a string" do test "can concat a single node rope and a string" do
rope = Rope.new("hello") rope = Rope.new("hello")
rope = Rope.concat([rope, " world"]) rope = Rope.concat([rope, " world"])
is_equal rope, "hello world" assert_rope_equal rope, "hello world"
end end
test "can concat a multi-node rope and a string together" do test "can concat a multi-node rope and a string together" do
@ -37,58 +71,58 @@ defmodule RopeTest do
rope = Rope.concat([rope, str]) rope = Rope.concat([rope, str])
is_equal rope, @text assert_rope_equal rope, @text
end end
test "can concat a lot" do test "can concat a lot" do
rope = build_rope @longtext rope = build_rope @longtext
is_equal rope, @longtext assert_rope_equal rope, @longtext
end end
test "concat handles nils" do test "concat handles nils" do
rope = Rope.concat([nil, "test"]) rope = Rope.concat([nil, "test"])
is_equal rope, "test" assert_rope_equal rope, "test"
rope = Rope.concat(["test", nil]) rope = Rope.concat(["test", nil])
is_equal rope, "test" assert_rope_equal rope, "test"
end end
test "slice with a start greater then the rope length returns the same as String.slice/3" do test "slice with a start greater then the rope length returns the same as String.slice/3" do
rope = Rope.new @simple 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 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 end
test "slice with start equal to the rope returns the same as String.slice/3" do test "slice with start equal to the rope returns the same as String.slice/3" do
rope = Rope.new "test" 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"] rope = Rope.concat ["hello", " world"]
length = String.length @simple 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 end
test "slice works on single node ropes" do test "slice works on single node ropes" do
rope = Rope.new "test" rope = Rope.new "test"
is_equal Rope.slice(rope, 2, 1), "s" assert_rope_equal Rope.slice(rope, 2, 1), "s"
end end
test "slice works on multi-node ropes" do test "slice works on multi-node ropes" do
rope = build_rope @simple 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 end
test "can get slice from middle of text" do test "can get slice from middle of text" do
rope = build_rope @longtext 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 end
test "rebalancing shouldn't effect a slice" do test "rebalancing shouldn't effect a slice" do
rope = build_rope @longtext rope = build_rope @longtext
rope = Rope.rebalance(rope) 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 end
test "get the length of a rope" do test "get the length of a rope" do
@ -115,7 +149,7 @@ defmodule RopeTest do
assert Rope.depth(rope) == 185 assert Rope.depth(rope) == 185
rope = Rope.rebalance rope rope = Rope.rebalance rope
is_equal rope, @longtext assert_rope_equal rope, @longtext
assert Rope.depth(rope) == 8 assert Rope.depth(rope) == 8
end end
@ -124,7 +158,7 @@ defmodule RopeTest do
rope1= Rope.rebalance rope rope1= Rope.rebalance rope
rope2 = Rope.rebalance rope1 rope2 = Rope.rebalance rope1
is_equal rope1, rope2 assert_rope_equal rope1, rope2
assert Rope.depth(rope1) == Rope.depth(rope2) assert Rope.depth(rope1) == Rope.depth(rope2)
end end
@ -136,7 +170,7 @@ defmodule RopeTest do
rope = Rope.concat([rope, build_rope(@longtext)]) rope = Rope.concat([rope, build_rope(@longtext)])
ropebalanced = Rope.rebalance(rope) ropebalanced = Rope.rebalance(rope)
is_equal rope, ropebalanced assert_rope_equal rope, ropebalanced
end end
test "find returns the index the search term begins at" do test "find returns the index the search term begins at" do
@ -147,7 +181,7 @@ defmodule RopeTest do
index = Rope.find(rope, "towels") index = Rope.find(rope, "towels")
subrope = Rope.slice(rope, index, String.length("towels")) subrope = Rope.slice(rope, index, String.length("towels"))
is_equal subrope, "towels" assert_rope_equal subrope, "towels"
end end
test "find returns -1 if the term could not be found" do 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 orig = @text |> build_rope |> Rope.rebalance
rope = Rope.replace(orig, "you", "me", global: false) 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") 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 = @longtext |> build_rope |> Rope.rebalance
rope = Rope.replace(rope, "towel", "duck") rope = Rope.replace(rope, "towel", "duck")
is_equal rope, String.replace(@longtext, "towel", "duck") assert_rope_equal rope, String.replace(@longtext, "towel", "duck")
end end
test "insert_at allows creating a new rope with the text added" do test "insert_at allows creating a new rope with the text added" do
orig = build_rope "Beware of the Leopard" orig = build_rope "Beware of the Leopard"
is_equal Rope.insert_at(orig, 63, " END"), "Beware of the Leopard END" assert_rope_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" assert_rope_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, 2, "SPLIT"), "BeSPLITware of the Leopard"
end end
test "remove_at allows removing a substr of the rope" do test "remove_at allows removing a substr of the rope" do
orig = build_rope "Beware of the Leopard" orig = build_rope "Beware of the Leopard"
is_equal Rope.remove_at(orig, 63, 10), "Beware of the Leopard" assert_rope_equal Rope.remove_at(orig, 63, 10), "Beware of the Leopard"
is_equal Rope.remove_at(orig, -7, 3), "Beware of the pard" assert_rope_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, 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 end
defp build_rope(text, opts // []) do defp build_rope(text, opts \\ []) do
words = text words = text
|> String.split(" ") |> String.split(" ")
@ -214,28 +267,8 @@ defmodule RopeTest do
words words
|> Enum.drop(1) |> Enum.drop(1)
|> Enum.reduce(Rope.new(first), fn (word, rope) -> |> Enum.reduce(Rope.new(first), fn (word, rope) ->
Rope.concat([rope, " " <> word], opts) Rope.concat([rope, " " <> word], opts)
end) end)
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 end

2
test/test_helper.exs

@ -1 +1 @@
ExUnit.start ExUnit.start()

BIN
ttb_last_config

Binary file not shown.
Loading…
Cancel
Save