src/series.bend

bend-visgraph@2.0.0.0 · 121 lines · 15 definitions

1# A time series from text with one value per line, as ts2vg's command line
2# reads it. Blank lines are skipped. Each value is rounded once, from its
3# decimal text to F32. Its x is its index, or the number in the same place
4# in a file of x coordinates, which `xs` checks.
5import Base
6
7type Parsed is Data:
8  Values{rev: List<&2, F32>, n: U32}
9  Bad{line: U32}
10
11def add(blank: Bool, m: Maybe<&2, F32>, +line: U32, rev: List<&2, F32>, +n: U32) -> Parsed:
12  match blank m:
13    case True{} _:
14      Values{rev, n}
15    case False{} Some{v}:
16      Values{v <> rev, (n + 1 : U32)}
17    case False{} None{}:
18      Bad{line}
19
20def step(s: String, +line: U32, st: Parsed) -> Parsed:
21  match st:
22    case Values{rev, +n}:
23      +t = String.trim(s)
24      add(String.is_empty(t), F32.read(t), line, rev, n)
25    case Bad{l}:
26      Bad{l}
27
28# Base's String.lines recurses once per character, too deep for the JS
29# lane's stack, so this walks the text with tail calls. `h` is the character
30# before `s`, `nl` whether it is a newline, and `cur` the line before `h`,
31# last character first.
32def scan(s: String, nl: Bool, +h: Char, cur: String, +line: U32, st: Parsed) -> Parsed:
33  match s nl:
34    case SNil{} True{}:
35      step(String.reverse(cur), line, st)
36    case SNil{} False{}:
37      step(String.reverse(SCon{h, cur}), line, st)
38    case SCon{+h2, t} True{}:
39      scan(t, Char.is_eq(h2, '\n'), h2, SNil{}, (line + 1 : U32), step(String.reverse(cur), line, st))
40    case SCon{+h2, t} False{}:
41      scan(t, Char.is_eq(h2, '\n'), h2, SCon{h, cur}, line, st)
42
43# The values of `text`, last first, and how many there are; or the first
44# line, counted from 1, that holds no number.
45def parse(text: String) -> Parsed:
46  match text:
47    case SNil{}:
48      Values{Nil{}, 0}
49    case SCon{+h, t}:
50      scan(t, Char.is_eq(h, '\n'), h, SNil{}, 1, Values{Nil{}, 0})
51
52def column(rev: List<&2, F32>, +i: U32, ys: Array<F32>) -> Array<F32>:
53  match rev:
54    case Nil{}:
55      ys
56    case Con{y, rest}:
57      column(rest, (i - 1 : U32), Array.set(F32, ys, i, y))
58
59# The `n` values `rev`, last first, as an array indexed from 0.
60def ys(rev: List<&2, F32>, +n: U32) -> Array<F32>:
61  column(rev, (n - 1 : U32), Array.new(F32, Nat.add(1n, U32.log2(n)), 0.0))
62
63def read_text.done(m: File & Result<&1, &1, U32 & String, String>) -> IO(String):
64  (f, r) = m
65  do IO<String>:
66    text : String <- IO.pass(String, r)
67    File.close(f)
68    return text
69
70def read_text.sized(m: File & Result<&1, &1, U32 & String, U32>) -> IO(String):
71  (f, r) = m
72  do IO<String>:
73    size : U32 <- IO.pass(U32, r)
74    got : File & Result<&1, &1, U32 & String, String> <- File.read(f, size)
75    read_text.done(got)
76
77def read_text(path: String) -> IO(String):
78  do IO<String>:
79    f : File <- IO.try(File, File.open(path, "r"))
80    sized : File & Result<&1, &1, U32 & String, U32> <- File.size(f)
81    read_text.sized(sized)
82
83# The x coordinates of a file, last first, if they suit a series; or why
84# they do not.
85type XsCheck is Data:
86  XsOk{rev: List<&2, F32>}
87  XsBad{msg: String}
88
89# Whether each value of `rest`, last first, is below `a`, as ts2vg checks
90# that x increases.
91def increasing(rest: List<&2, F32>, +a: F32, ok: Bool) -> Bool:
92  match rest:
93    case Nil{}:
94      ok
95    case Con{+b, t}:
96      increasing(t, b, ok && Bool.not(F32.is_le(F32.sub(a, b), 0.0)))
97
98def increases(xrev: List<&2, F32>) -> Bool:
99  match xrev:
100    case Nil{}:
101      True{}
102    case Con{+a, t}:
103      increasing(t, a, True{})
104
105def xs_check(same: Bool, up: Bool, rev: List<&2, F32>, +n: U32, +m: U32) -> XsCheck:
106  match same up:
107    case False{} _:
108      XsBad{"the x coordinates hold " ++ U32.show(m) ++ " values and the series " ++ U32.show(n)}
109    case True{} False{}:
110      XsBad{"the x coordinates must increase"}
111    case True{} True{}:
112      XsOk{rev}
113
114# The x coordinates `p` of a series of `n` values: ts2vg takes one x per
115# value, each above the one before.
116def xs(p: Parsed, +n: U32) -> XsCheck:
117  match p:
118    case Bad{line}:
119      XsBad{"line " ++ U32.show(line) ++ " of the x coordinates holds no number"}
120    case Values{+rev, +m}:
121      xs_check(U32.is_eq(n, m), increases(rev), rev, n, m)