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
/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

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
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

37
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

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
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

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
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

2
test/test_helper.exs

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

BIN
ttb_last_config

Binary file not shown.
Loading…
Cancel
Save