Skip to content

Commit 9a4a3a3

Browse files
committed
feat: ✨ add basic interpolation
1 parent f9a9745 commit 9a4a3a3

File tree

10 files changed

+416
-6
lines changed

10 files changed

+416
-6
lines changed

lib/algo/interpolation.ex

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
defmodule InterpolationApp.Algo.Interpolation do
2+
@moduledoc """
3+
GenServer that prints new results when received
4+
"""
5+
6+
@doc """
7+
Interpolates a value from given points.
8+
"""
9+
@callback interpolate(points :: [tuple()], target_point :: number()) :: number()
10+
11+
@doc """
12+
Interpolates values ​​from given points.
13+
"""
14+
@callback interpolate(points :: [tuple()], target_points :: [number()]) :: [number()]
15+
16+
@callback points_enough?(points :: [tuple()]) :: boolean
17+
end

lib/algo/linear_interpolation.ex

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
defmodule InterpolationApp.Algo.LinearInterpolation do
2+
@moduledoc """
3+
In mathematics, linear interpolation is a method of curve fitting using linear polynomials to construct
4+
new data points within the range of a discrete set of known data points.
5+
"""
6+
7+
@behaviour InterpolationApp.Algo.Interpolation
8+
9+
def get_name, do: "Линейная интерполяция"
10+
11+
def get_enough_points_count, do: 2
12+
13+
@impl true
14+
def points_enough?(points), do: length(points) >= get_enough_points_count()
15+
16+
@impl true
17+
def interpolate(points, target_point) when not is_list(target_point) do
18+
[{x0, y0}, {x1, y1}] = points
19+
20+
a = (y1 - y0) / (x1 - x0)
21+
b = y0 - a * x0
22+
23+
{target_point, a * target_point + b}
24+
end
25+
26+
@impl true
27+
def interpolate(points, xs) when is_list(xs) do
28+
Enum.map(xs, fn x -> interpolate(points, x) end)
29+
end
30+
end

lib/cli/applier.ex

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
defmodule InterpolationApp.CLI.Applier do
2+
@moduledoc """
3+
GenServer that applies input on interpolator
4+
"""
5+
6+
use GenServer
7+
8+
alias InterpolationApp.Utils.InputValidator
9+
10+
# Client API
11+
12+
def start_link(interpolator_pid) do
13+
GenServer.start_link(__MODULE__, interpolator_pid, name: __MODULE__)
14+
end
15+
16+
def apply_input(input) do
17+
GenServer.cast(__MODULE__, {:apply_input, input})
18+
end
19+
20+
# Server Callbacks
21+
22+
@impl true
23+
def init(interpolator_pid) do
24+
{:ok, {interpolator_pid, nil}}
25+
end
26+
27+
@impl true
28+
def handle_cast({:apply_input, input}, {interpolator_pid, prev_x}) do
29+
{validation_status, validation_result} = InputValidator.validate_input(input)
30+
31+
if validation_status == :error do
32+
IO.puts(validation_result)
33+
{:noreply, {interpolator_pid, prev_x}}
34+
else
35+
process_new_point(validation_result, prev_x, interpolator_pid)
36+
end
37+
end
38+
39+
# Helper Functions
40+
41+
defp process_new_point({x, y}, prev_x, interpolator_pid) do
42+
if prev_x == nil or prev_x < x do
43+
GenServer.cast(interpolator_pid, {:add_point, {x, y}})
44+
{:noreply, {interpolator_pid, x}}
45+
else
46+
IO.puts("Следующее значение X должно быть больше предыдущего (но #{x} < #{prev_x})")
47+
{:noreply, {interpolator_pid, prev_x}}
48+
end
49+
end
50+
end

lib/cli/config_parser.ex

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
defmodule InterpolationApp.CLI.ConfigParser do
2+
@moduledoc """
3+
Parses args for interpolation-app CLI
4+
"""
5+
6+
@methods ["linear", "lagrange2", "lagrange3", "newton"]
7+
8+
def parse_args(args) do
9+
args
10+
|> Enum.reduce(%{}, &parse_arg/2)
11+
|> validate_args()
12+
end
13+
14+
defp parse_arg("--method1=" <> method, acc) do
15+
Map.put(acc, :method1, String.trim(method))
16+
end
17+
18+
defp parse_arg("--method2=" <> method, acc) do
19+
Map.put(acc, :method2, String.trim(method))
20+
end
21+
22+
defp parse_arg("--step=" <> step, acc) do
23+
Map.put(acc, :step, String.trim(step))
24+
end
25+
26+
defp parse_arg("--accuracy=" <> accuracy, acc) do
27+
Map.put(acc, :accuracy, String.trim(accuracy))
28+
end
29+
30+
defp parse_arg(_, acc), do: acc
31+
32+
defp validate_args(%{method1: method1, method2: method2, step: step, accuracy: accuracy}) do
33+
cond do
34+
!valid_method?(method1) ->
35+
{:error,
36+
"Неправильный method1: #{method1}. Должен быть один из [#{Enum.join(@methods, ", ")}]."}
37+
38+
!valid_method?(method2) ->
39+
{:error,
40+
"Неправильный method2: #{method2}. Должен быть один из [#{Enum.join(@methods, ", ")}]."}
41+
42+
!valid_step?(step) ->
43+
{:error,
44+
"Неправильный step: #{step}. Должен быть положительным числом (например: 0.1, 1, 10, ...)."}
45+
46+
!valid_accuracy?(accuracy) ->
47+
{:error, "Неправильный accuracy: #{accuracy}. Должен быть положительным целым числом."}
48+
49+
true ->
50+
{:ok,
51+
%{
52+
method1: module_from_method(method1),
53+
method2: module_from_method(method2),
54+
step: elem(Float.parse(step), 0),
55+
accuracy: elem(Integer.parse(accuracy), 0)
56+
}}
57+
end
58+
end
59+
60+
defp validate_args(%{method1: method1, method2: method2, step: step}) do
61+
validate_args(%{method1: method1, method2: method2, step: step, accuracy: "3"})
62+
end
63+
64+
defp validate_args(_) do
65+
{:error, "Ошибка! Обязательные аргументы: --method1=, --method2=, --step="}
66+
end
67+
68+
defp valid_method?(method), do: method in @methods
69+
70+
defp valid_step?(step) do
71+
case Float.parse(step) do
72+
{value, ""} -> value > 0
73+
_ -> false
74+
end
75+
end
76+
77+
defp valid_accuracy?(accuracy) do
78+
case Integer.parse(accuracy) do
79+
{value, ""} -> value > 0
80+
_ -> false
81+
end
82+
end
83+
84+
defp module_from_method(method) do
85+
Module.concat(["InterpolationApp", "Algo", "#{String.capitalize(method)}Interpolation"])
86+
end
87+
end

lib/cli/interpolator.ex

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
defmodule InterpolationApp.CLI.Interpolator do
2+
@moduledoc """
3+
GenServer that interpolates values
4+
"""
5+
6+
use GenServer
7+
8+
alias InterpolationApp.Utils.PointGenerator
9+
10+
# Client API
11+
12+
def start_link(config, printer_pid) do
13+
GenServer.start_link(__MODULE__, {config, printer_pid}, name: __MODULE__)
14+
end
15+
16+
def add_point(point) do
17+
GenServer.cast(__MODULE__, {:add_point, point})
18+
end
19+
20+
# Server Callbacks
21+
22+
@impl true
23+
def init({config, printer_pid}) do
24+
{:ok, {[], config, printer_pid}}
25+
end
26+
27+
@impl true
28+
def handle_cast({:add_point, point}, {points, config, printer_pid}) do
29+
points = Enum.reverse([point | Enum.reverse(points)])
30+
methods = Enum.uniq([config.method1, config.method2])
31+
32+
Enum.each(methods, fn method ->
33+
if method.points_enough?(points) do
34+
limit = method.get_enough_points_count()
35+
interpolation_points = Enum.slice(points, -limit, limit)
36+
generated_points = PointGenerator.generate(interpolation_points, config.step)
37+
38+
GenServer.cast(printer_pid, {
39+
:print,
40+
method,
41+
method.interpolate(interpolation_points, generated_points)
42+
})
43+
end
44+
end)
45+
46+
{:noreply, {points, config, printer_pid}}
47+
end
48+
end

lib/cli/printer.ex

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
defmodule InterpolationApp.CLI.Printer do
2+
@moduledoc """
3+
GenServer that prints new results when received
4+
"""
5+
6+
use GenServer
7+
8+
# Client API
9+
10+
def start_link(config) do
11+
GenServer.start_link(__MODULE__, config, name: __MODULE__)
12+
end
13+
14+
def print(method, result) do
15+
GenServer.cast(__MODULE__, {:print, method, result})
16+
end
17+
18+
# Server Callbacks
19+
20+
@impl true
21+
def init(config) do
22+
{:ok, config}
23+
end
24+
25+
@impl true
26+
def handle_cast({:print, method, result}, config) do
27+
print_result(method, result, config.step, config.accuracy)
28+
{:noreply, config}
29+
end
30+
31+
# Helper Functions
32+
33+
defp print_result(method, result, step, accuracy) do
34+
{first_x, _} = List.first(result)
35+
36+
IO.puts(
37+
"#{method.get_name()} (идем от точки из введенных #{first_x} с шагом #{step}, покрывая все введенные X)"
38+
)
39+
40+
print_line(result, accuracy)
41+
IO.write("\n")
42+
end
43+
44+
defp print_line(result, accuracy) do
45+
format_string = String.duplicate("~s ", length(result)) <> "~n"
46+
47+
Enum.each([0, 1], fn n ->
48+
:io.format(
49+
format_string,
50+
Enum.map(
51+
result,
52+
&String.pad_trailing(:erlang.float_to_binary(elem(&1, n), decimals: accuracy), 7)
53+
)
54+
)
55+
end)
56+
end
57+
end

lib/cli/repl.ex

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
defmodule InterpolationApp.CLI.Repl do
2+
@moduledoc """
3+
Read-Eval-Print-Loop
4+
"""
5+
6+
alias InterpolationApp.CLI.{Applier, Interpolator, Printer}
7+
8+
def start(config) do
9+
IO.puts("Введите 'quit', чтобы закончить ввод.")
10+
IO.puts("Введите узлы интерполяции:")
11+
12+
{:ok, printer_pid} = Printer.start_link(config)
13+
{:ok, interpolator_pid} = Interpolator.start_link(config, printer_pid)
14+
{:ok, applier_pid} = Applier.start_link(interpolator_pid)
15+
16+
# spawn(fn -> loop(applier_pid) end)
17+
loop(applier_pid)
18+
end
19+
20+
defp loop(applier_pid) do
21+
:io.setopts(:standard_io, active: true)
22+
input = IO.gets("")
23+
24+
if input == :eof or String.trim(input) == "quit" do
25+
IO.puts("\nСпасибо за использование программы!\n")
26+
27+
IO.puts("""
28+
|\ _,,,---,,_
29+
ZZZzz /,`.-'`' -. ;-;;,_
30+
|,4- ) )-,_. ,\ ( `'-'
31+
'---''(_/--' `-'\_)
32+
""")
33+
34+
System.halt()
35+
end
36+
37+
GenServer.cast(applier_pid, {:apply_input, String.trim(input)})
38+
loop(applier_pid)
39+
end
40+
end

lib/main.ex

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,38 @@ defmodule InterpolationApp.Main do
33
Application entry point
44
"""
55

6+
alias InterpolationApp.CLI.{ConfigParser, Repl}
7+
68
@spec main([String.t()]) :: any()
79
def main(args \\ []) do
8-
args
9-
|> parse_args()
10-
|> IO.puts()
10+
if Enum.member?(["--help", "-h"], List.first(args)) do
11+
IO.puts(help())
12+
System.halt()
13+
end
14+
15+
case ConfigParser.parse_args(args) do
16+
{:ok, config} ->
17+
Repl.start(config)
18+
19+
{:error, message} ->
20+
IO.puts(
21+
"Неверный формат запуска!\n\n#{message}\nЧтобы получить помощь, запустите программу с флагом '--help'"
22+
)
23+
end
1124
end
1225

13-
defp parse_args(args) do
14-
{opts, word, _} = args |> OptionParser.parse(switches: [upcase: :boolean])
26+
defp help do
27+
"""
28+
usage: interpolation-app [options] --method1=<method> --method2=<method> --step=<step>
29+
options:
30+
-h, --help Prints this message
31+
--accuracy=<accuracy> Set printing accuracy
1532
16-
{opts, List.to_string(word)}
33+
methods:
34+
* linear
35+
* lagrange2
36+
* lagrange3
37+
* newton
38+
"""
1739
end
1840
end

0 commit comments

Comments
 (0)