swpick.bend
cswap-bend-cli@0.1.0.0 · 261 lines · 40 definitions
1import Base
2import cswap-bend-cli@0.1.0.0/json.bend as Json
3import cswap-bend-cli@0.1.0.0/store/seq.bend as SSeq
4
5
6
7
8
9
10type Cand is Data:
11 Cand{slot: String, disabled: Bool, switchable: Bool, headroom: Maybe<&2, U32>}
12
13
14type Sel is Data:
15 SelOk{slot: String, warns: List<&2, String>}
16 SelNone{warns: List<&2, String>, exhausted: Bool}
17
18type Pick is Data:
19 Pick{slot: String, h: U32, found: Bool}
20
21type BestOut is Data:
22 BPick{slot: String}
23 BNote{note: String}
24
25def skip_with(+w: String, +ex: Bool, +r: Sel) -> Sel:
26 match r:
27 case SelOk{s, ws}:
28 SelOk{s, w <> ws}
29 case SelNone{ws, e}:
30 SelNone{w <> ws, e || ex}
31
32def at_limit(+h: Maybe<&2, U32>) -> Bool:
33 match h:
34 case Some{x}:
35 U32.is_eq(x, 0)
36 case None{}:
37 False{}
38
39def sel_step2(+dis: Bool, +sw: Bool, +limit: Bool, +slot: String, +r: Sel) -> Sel:
40 match dis sw limit:
41 case True{} _ _:
42 skip_with("Skipped Account-" ++ slot ++ " (disabled)", False{}, r)
43 case False{} False{} _:
44 skip_with("Skipped Account-" ++ slot ++ " (no stored credentials/config)", False{}, r)
45 case False{} True{} True{}:
46 skip_with("Skipped Account-" ++ slot ++ " (at 5h/7d limit)", True{}, r)
47 case False{} True{} False{}:
48 SelOk{slot, []}
49
50def sel_step(+avail: Bool, +c: Cand, +r: Sel) -> Sel:
51 match c:
52 case Cand{slot, dis, sw, hr}:
53 sel_step2(dis, sw, avail && at_limit(hr), slot, r)
54
55
56
57def select_next(+cs: List<&2, Cand>, +avail: Bool) -> Sel:
58 match cs:
59 case Nil{}:
60 SelNone{[], False{}}
61 case Con{c, rest}:
62 sel_step(avail, c, select_next(rest, avail))
63
64def sel_n(+c: Bool, +x: Nat, +y: Nat) -> Nat:
65 match c:
66 case True{}:
67 x
68 case False{}:
69 y
70
71def index_of(+xs: List<&2, String>, +x: String, +i: Nat) -> Nat:
72 match xs:
73 case Nil{}:
74 0n
75 case Con{h, t}:
76 sel_n(String.eq(h, x), i, index_of(t, x, 1n+i))
77
78def rotated2(+xs: List<&2, String>, +idx: Nat) -> List<&2, String>:
79 List.append(&2, String, List.drop(&2, String, xs, 1n+idx), List.take(&2, String, xs, idx))
80
81
82
83def rotated(+xs: List<&2, String>, +anchor: Maybe<&2, String>) -> List<&2, String>:
84 rotated2(xs, index_of(xs, SSeq.str_or(anchor, ""), 0n))
85
86def better(+found: Bool, +h: U32, +cur: Pick) -> Bool:
87 match cur:
88 case Pick{s, ch, f}:
89 Bool.not(f) || U32.is_gt(h, ch)
90
91def sel_pick(+c: Bool, +x: Pick, +y: Pick) -> Pick:
92 match c:
93 case True{}:
94 x
95 case False{}:
96 y
97
98def best_known(+cs: List<&2, Cand>, +cur: Pick) -> Pick:
99 match cs:
100 case Nil{}:
101 cur
102 case Con{Cand{slot, d, sw, Some{h}}, t}:
103 best_known(t, sel_pick(better(True{}, h, cur), Pick{slot, h, True{}}, cur))
104 case Con{c, t}:
105 best_known(t, cur)
106
107def any_unknown(+cs: List<&2, Cand>) -> Bool:
108 match cs:
109 case Nil{}:
110 False{}
111 case Con{Cand{s, d, sw, None{}}, t}:
112 True{}
113 case Con{c, t}:
114 any_unknown(t)
115
116def best4(+wins: Bool, +s: String, +unknown: Bool, +exhausted: Bool) -> BestOut:
117 match wins unknown exhausted:
118 case True{} _ _:
119 BPick{s}
120 case False{} True{} _:
121 BNote{"incomplete-comparison"}
122 case False{} False{} True{}:
123 BNote{"exhausted"}
124 case False{} False{} False{}:
125 BNote{"stay"}
126
127def best3(+p: Pick, +cur: U32, +unknown: Bool) -> BestOut:
128 match p:
129 case Pick{s, h, f}:
130 best4(U32.is_gt(h, cur), s, unknown, U32.is_eq(cur, 0))
131
132def best2(+cur: Maybe<&2, U32>, +others: List<&2, Cand>, +p: Pick) -> BestOut:
133 match cur p:
134 case None{} _:
135 BNote{"current-unavailable"}
136 case Some{c} Pick{s, h, False{}}:
137 BNote{"no-comparison"}
138 case Some{c} Pick{s, h, True{}}:
139 best3(Pick{s, h, True{}}, c, any_unknown(others))
140
141
142
143def select_best(+cur: Maybe<&2, U32>, +others: List<&2, Cand>) -> BestOut:
144 match others:
145 case Nil{}:
146 BNote{"none"}
147 case Con{o, t}:
148 best2(cur, Con{o, t}, best_known(Con{o, t}, Pick{"", 0, False{}}))
149
150def pct2(+parts: List<&2, String>) -> U32:
151 match parts:
152 case Con{i, Con{f, Nil{}}}:
153 (SSeq.str_u32(i) * 100 + SSeq.str_u32(String.take(f ++ "00", 2n)) : U32)
154 case Con{i, Nil{}}:
155 (SSeq.str_u32(i) * 100 : U32)
156 case _:
157 0
158
159
160def pct_hundredths(+t: String) -> U32:
161 pct2(String.split(t, '.'))
162
163def pct_of2(+m: Maybe<&2, Json.Json>) -> Maybe<&2, U32>:
164 match m:
165 case Some{Json.JNum{t}}:
166 Some{pct_hundredths(t)}
167 case _:
168 None{}
169
170def pct_of(+w: Json.Json) -> Maybe<&2, U32>:
171 pct_of2(Json.get("pct", w))
172
173def push_pct(+m: Maybe<&2, U32>, +rest: List<&2, U32>) -> List<&2, U32>:
174 match m:
175 case Some{p}:
176 p <> rest
177 case None{}:
178 rest
179
180def lower_all(+xs: List<&2, String>) -> List<&2, String>:
181 match xs:
182 case Nil{}:
183 []
184 case Con{h, t}:
185 String.to_lower(h) <> lower_all(t)
186
187def mem(+x: String, +xs: List<&2, String>) -> Bool:
188 match xs:
189 case Nil{}:
190 False{}
191 case Con{h, t}:
192 String.eq(h, x) || mem(x, t)
193
194def scoped_pick(+hit: Bool, +m: Maybe<&2, U32>, +rest: List<&2, U32>) -> List<&2, U32>:
195 match hit:
196 case True{}:
197 push_pct(m, rest)
198 case False{}:
199 rest
200
201def scoped_pcts(+items: Json.Json, +wanted: List<&2, String>) -> List<&2, U32>:
202 match items:
203 case Json.JCons{s, t}:
204 scoped_pick(mem("all", wanted) || mem(String.to_lower(SSeq.field_str(s, "name")), wanted), pct_of(s), scoped_pcts(t, wanted))
205 case _:
206 []
207
208def window_pct2(+m: Maybe<&2, Json.Json>) -> Maybe<&2, U32>:
209 match m:
210 case Some{w}:
211 pct_of(w)
212 case None{}:
213 None{}
214
215def window_pct(+row: Json.Json, +key: String) -> Maybe<&2, U32>:
216 window_pct2(Json.get(key, row))
217
218def sel_l(+c: Bool, +x: List<&2, U32>, +y: List<&2, U32>) -> List<&2, U32>:
219 match c:
220 case True{}:
221 x
222 case False{}:
223 y
224
225
226
227def pcts(+row: Json.Json, +models: List<&2, String>) -> List<&2, U32>:
228 push_pct(window_pct(row, "five_hour"), push_pct(window_pct(row, "seven_day"),
229 sel_l(List.is_empty(&2, String, models), [], scoped_pcts(SSeq.unwrap_arr(SSeq.get_or("scoped", row, Json.JNull{})), lower_all(models)))))
230
231def max_pct(+xs: List<&2, U32>, +acc: U32) -> U32:
232 match xs:
233 case Nil{}:
234 acc
235 case Con{h, t}:
236 max_pct(t, SSeq.max_u32(acc, h))
237
238def min_u32(+a: U32, +b: U32) -> U32:
239 SSeq.sel.u(U32.is_lt(a, b), a, b)
240
241def headroom2(+xs: List<&2, U32>) -> Maybe<&2, U32>:
242 match xs:
243 case Nil{}:
244 None{}
245 case Con{h, t}:
246 Some{(10000 - min_u32(10000, max_pct(Con{h, t}, 0)) : U32)}
247
248
249def headroom(+row: Json.Json, +models: List<&2, String>) -> Maybe<&2, U32>:
250 headroom2(pcts(row, models))
251
252
253def usage_row(+usage: Json.Json, +slot: String) -> Json.Json:
254 SSeq.get_or("lastGood", SSeq.get_or(slot, SSeq.accounts_of(usage), Json.JNull{}), Json.JNull{})
255
256def sel_lc(+c: Bool, +x: List<&2, Cand>, +y: List<&2, Cand>) -> List<&2, Cand>:
257 match c:
258 case True{}:
259 x
260 case False{}:
261 y