diff --git a/lib/contrib/markov/chain.ex b/lib/contrib/markov/chain.ex new file mode 100644 index 0000000..4c95a1d --- /dev/null +++ b/lib/contrib/markov/chain.ex @@ -0,0 +1,35 @@ +defmodule Omnibot.Contrib.Markov.Chain do + alias Omnibot.{Contrib.Markov.Chain, Util} + + @enforce_keys [:order] + defstruct order: 2, chain: [] + + def train(%Chain {chain: chain, order: order}, words) when is_list(words) do + + Enum.filter(words, &(String.length(&1) > 0)) + |> Enum.chunk_every(order + 1, 1) # this gives us a "sliding window" effect + |> Enum.reduce(chain, &case Enum.split(words, order) do + {words, []} -> if length(&1) == order, + # Null case for the chain; this is an "end" state + do: train_one(%Chain {chain: &2, order: order}, words, nil) + # else: TODO ? train [a, nil] -> b ? + {words, [next]} -> + train_one(%Chain {chain: &2, order: order}, words, next) + end + ) + end + + def train_one(%Chain {chain: _chain, order: _order}, _key, _value) do + end + + def lookup(%Chain {chain: chain, order: order}, key) do + if length(key) != order, do: raise(ArgumentError, message: "invalid key (length #{length(key)} vs. order #{order})") + case Util.binary_search(chain, key) do + {_index, value} -> value + nil -> nil + end + end + + def put(%Chain {chain: _chain, order: _order}, _key, _value) do + end +end diff --git a/lib/contrib/markov/markov.ex b/lib/contrib/markov/markov.ex new file mode 100644 index 0000000..f75ebfc --- /dev/null +++ b/lib/contrib/markov/markov.ex @@ -0,0 +1,22 @@ +defmodule Omnibot.Contrib.Markov do + use Omnibot.Plugin + + alias Omnibot.Contrib.Markov.Chain + + @default_config path: :"wordbot.ets", order: 2 + + @impl true + def on_init(cfg) do + # Create the markov database + path = if is_atom(cfg[:path]), + do: cfg[:path], + else: String.to_atom(cfg[:path]) + {:ok, db} = :dets.open_file(path) + db + end + + @impl true + def on_channel_msg(_irc, _channel, _nick, msg) do + _words = String.split(msg, ~r/\s+/) + end +end diff --git a/lib/util.ex b/lib/util.ex index af710e6..d1877e9 100644 --- a/lib/util.ex +++ b/lib/util.ex @@ -14,4 +14,26 @@ defmodule Omnibot.Util do def denotify_nick(nick) do String.graphemes(nick) |> Enum.join("\u200b") end + + def binary_search([], _key) do + nil + end + + def binary_search([{key, value} | _], key) do + {0, value} + end + + @doc "Attempts to find to find the given key in a sorted associative array." + def binary_search(list, key) do + {head, tail} = Enum.split(list, trunc(length(list) / 2)) + [{mid, _} | _] = tail + if key < mid do + binary_search(head, key) + else + case binary_search(tail, key) do + nil -> nil + {index, item} -> {index + length(head), item} + end + end + end end diff --git a/test/contrib/markov/chain_test.exs b/test/contrib/markov/chain_test.exs new file mode 100644 index 0000000..e0de4a9 --- /dev/null +++ b/test/contrib/markov/chain_test.exs @@ -0,0 +1,12 @@ +defmodule MarkovChainTest do + use ExUnit.Case + alias Omnibot.Contrib.Markov.Chain + + test "chain train_one works correctly" do + chain = %Chain {order: 2} + |> Chain.train_one(["foo", "bar"], "baz") + #assert chain.chain == [ + #{["foo", "bar"], {"baz", 1}} + #] + end +end diff --git a/test/util_test.exs b/test/util_test.exs index d7921e5..9987a07 100644 --- a/test/util_test.exs +++ b/test/util_test.exs @@ -12,4 +12,21 @@ defmodule Omnibot.UtilTest do assert Util.string_or_nil("") == nil assert Util.string_or_nil("asdf") == "asdf" end + + test "binary_search" do + indexes = 0..10 |> Enum.to_list() + values = indexes |> Enum.map(&({&1, &1 * 2})) + assert Enum.map(indexes, &(Util.binary_search(values, &1))) == values + + indexes = 0..101 |> Enum.to_list() + values = indexes |> Enum.map(&({&1, &1 * 2})) + assert Enum.map(indexes, &(Util.binary_search(values, &1))) == values + + values = [a: 15, b: 22, c: -1, d: 0] + + assert Util.binary_search(values, :a) == {0, 15} + assert Util.binary_search(values, :b) == {1, 22} + assert Util.binary_search(values, :c) == {2, -1} + assert Util.binary_search(values, :d) == {3, 0} + end end