blob: 3004507da841e7740a8a7e8670224946022b1e5d [file] [log] [blame]
Serge Bazanski18c1a262022-07-07 14:24:53 +02001--
2-- lume
3--
4-- Copyright (c) 2016 rxi
5--
6-- This library is free software; you can redistribute it and/or modify it
7-- under the terms of the MIT license. See LICENSE for details.
8--
9
10local lume = { _version = "2.2.3" }
11
12local pairs, ipairs = pairs, ipairs
13local type, assert, unpack = type, assert, unpack or table.unpack
14local tostring, tonumber = tostring, tonumber
15local math_floor = math.floor
16local math_ceil = math.ceil
17local math_atan2 = math.atan2 or math.atan
18local math_sqrt = math.sqrt
19local math_abs = math.abs
20
21local noop = function()
22end
23
24local identity = function(x)
25 return x
26end
27
28local patternescape = function(str)
29 return str:gsub("[%(%)%.%%%+%-%*%?%[%]%^%$]", "%%%1")
30end
31
32local absindex = function(len, i)
33 return i < 0 and (len + i + 1) or i
34end
35
36local iscallable = function(x)
37 if type(x) == "function" then return true end
38 local mt = getmetatable(x)
39 return mt and mt.__call ~= nil
40end
41
42local getiter = function(x)
43 if lume.isarray(x) then
44 return ipairs
45 elseif type(x) == "table" then
46 return pairs
47 end
48 error("expected table", 3)
49end
50
51local iteratee = function(x)
52 if x == nil then return identity end
53 if iscallable(x) then return x end
54 if type(x) == "table" then
55 return function(z)
56 for k, v in pairs(x) do
57 if z[k] ~= v then return false end
58 end
59 return true
60 end
61 end
62 return function(z) return z[x] end
63end
64
65
66
67function lume.clamp(x, min, max)
68 return x < min and min or (x > max and max or x)
69end
70
71
72function lume.round(x, increment)
73 if increment then return lume.round(x / increment) * increment end
74 return x >= 0 and math_floor(x + .5) or math_ceil(x - .5)
75end
76
77
78function lume.sign(x)
79 return x < 0 and -1 or 1
80end
81
82
83function lume.lerp(a, b, amount)
84 return a + (b - a) * lume.clamp(amount, 0, 1)
85end
86
87
88function lume.smooth(a, b, amount)
89 local t = lume.clamp(amount, 0, 1)
90 local m = t * t * (3 - 2 * t)
91 return a + (b - a) * m
92end
93
94
95function lume.pingpong(x)
96 return 1 - math_abs(1 - x % 2)
97end
98
99
100function lume.distance(x1, y1, x2, y2, squared)
101 local dx = x1 - x2
102 local dy = y1 - y2
103 local s = dx * dx + dy * dy
104 return squared and s or math_sqrt(s)
105end
106
107
108function lume.angle(x1, y1, x2, y2)
109 return math_atan2(y2 - y1, x2 - x1)
110end
111
112
113function lume.vector(angle, magnitude)
114 return math.cos(angle) * magnitude, math.sin(angle) * magnitude
115end
116
117
118function lume.random(a, b)
119 if not a then a, b = 0, 1 end
120 if not b then b = 0 end
121 return a + math.random() * (b - a)
122end
123
124
125function lume.randomchoice(t)
126 return t[math.random(#t)]
127end
128
129
130function lume.weightedchoice(t)
131 local sum = 0
132 for _, v in pairs(t) do
133 assert(v >= 0, "weight value less than zero")
134 sum = sum + v
135 end
136 assert(sum ~= 0, "all weights are zero")
137 local rnd = lume.random(sum)
138 for k, v in pairs(t) do
139 if rnd < v then return k end
140 rnd = rnd - v
141 end
142end
143
144
145function lume.isarray(x)
146 return (type(x) == "table" and x[1] ~= nil) and true or false
147end
148
149
150function lume.push(t, ...)
151 local n = select("#", ...)
152 for i = 1, n do
153 t[#t + 1] = select(i, ...)
154 end
155 return ...
156end
157
158
159function lume.remove(t, x)
160 local iter = getiter(t)
161 for i, v in iter(t) do
162 if v == x then
163 if lume.isarray(t) then
164 table.remove(t, i)
165 break
166 else
167 t[i] = nil
168 break
169 end
170 end
171 end
172 return x
173end
174
175
176function lume.clear(t)
177 local iter = getiter(t)
178 for k in iter(t) do
179 t[k] = nil
180 end
181 return t
182end
183
184
185function lume.extend(t, ...)
186 for i = 1, select("#", ...) do
187 local x = select(i, ...)
188 if x then
189 for k, v in pairs(x) do
190 t[k] = v
191 end
192 end
193 end
194 return t
195end
196
197
198function lume.shuffle(t)
199 local rtn = {}
200 for i = 1, #t do
201 local r = math.random(i)
202 if r ~= i then
203 rtn[i] = rtn[r]
204 end
205 rtn[r] = t[i]
206 end
207 return rtn
208end
209
210
211function lume.sort(t, comp)
212 local rtn = lume.clone(t)
213 if comp then
214 if type(comp) == "string" then
215 table.sort(rtn, function(a, b) return a[comp] < b[comp] end)
216 else
217 table.sort(rtn, comp)
218 end
219 else
220 table.sort(rtn)
221 end
222 return rtn
223end
224
225
226function lume.array(...)
227 local t = {}
228 for x in ... do t[#t + 1] = x end
229 return t
230end
231
232
233function lume.each(t, fn, ...)
234 local iter = getiter(t)
235 if type(fn) == "string" then
236 for _, v in iter(t) do v[fn](v, ...) end
237 else
238 for _, v in iter(t) do fn(v, ...) end
239 end
240 return t
241end
242
243
244function lume.map(t, fn)
245 fn = iteratee(fn)
246 local iter = getiter(t)
247 local rtn = {}
248 for k, v in iter(t) do rtn[k] = fn(v) end
249 return rtn
250end
251
252
253function lume.all(t, fn)
254 fn = iteratee(fn)
255 local iter = getiter(t)
256 for _, v in iter(t) do
257 if not fn(v) then return false end
258 end
259 return true
260end
261
262
263function lume.any(t, fn)
264 fn = iteratee(fn)
265 local iter = getiter(t)
266 for _, v in iter(t) do
267 if fn(v) then return true end
268 end
269 return false
270end
271
272
273function lume.reduce(t, fn, first)
274 local acc = first
275 local started = first and true or false
276 local iter = getiter(t)
277 for _, v in iter(t) do
278 if started then
279 acc = fn(acc, v)
280 else
281 acc = v
282 started = true
283 end
284 end
285 assert(started, "reduce of an empty table with no first value")
286 return acc
287end
288
289
290function lume.set(t)
291 local rtn = {}
292 for k in pairs(lume.invert(t)) do
293 rtn[#rtn + 1] = k
294 end
295 return rtn
296end
297
298
299function lume.filter(t, fn, retainkeys)
300 fn = iteratee(fn)
301 local iter = getiter(t)
302 local rtn = {}
303 if retainkeys then
304 for k, v in iter(t) do
305 if fn(v) then rtn[k] = v end
306 end
307 else
308 for _, v in iter(t) do
309 if fn(v) then rtn[#rtn + 1] = v end
310 end
311 end
312 return rtn
313end
314
315
316function lume.reject(t, fn, retainkeys)
317 fn = iteratee(fn)
318 local iter = getiter(t)
319 local rtn = {}
320 if retainkeys then
321 for k, v in iter(t) do
322 if not fn(v) then rtn[k] = v end
323 end
324 else
325 for _, v in iter(t) do
326 if not fn(v) then rtn[#rtn + 1] = v end
327 end
328 end
329 return rtn
330end
331
332
333function lume.merge(...)
334 local rtn = {}
335 for i = 1, select("#", ...) do
336 local t = select(i, ...)
337 local iter = getiter(t)
338 for k, v in iter(t) do
339 rtn[k] = v
340 end
341 end
342 return rtn
343end
344
345
346function lume.concat(...)
347 local rtn = {}
348 for i = 1, select("#", ...) do
349 local t = select(i, ...)
350 if t ~= nil then
351 local iter = getiter(t)
352 for _, v in iter(t) do
353 rtn[#rtn + 1] = v
354 end
355 end
356 end
357 return rtn
358end
359
360
361function lume.find(t, value)
362 local iter = getiter(t)
363 for k, v in iter(t) do
364 if v == value then return k end
365 end
366 return nil
367end
368
369
370function lume.match(t, fn)
371 fn = iteratee(fn)
372 local iter = getiter(t)
373 for k, v in iter(t) do
374 if fn(v) then return v, k end
375 end
376 return nil
377end
378
379
380function lume.count(t, fn)
381 local count = 0
382 local iter = getiter(t)
383 if fn then
384 fn = iteratee(fn)
385 for _, v in iter(t) do
386 if fn(v) then count = count + 1 end
387 end
388 else
389 if lume.isarray(t) then
390 return #t
391 end
392 for _ in iter(t) do count = count + 1 end
393 end
394 return count
395end
396
397
398function lume.slice(t, i, j)
399 i = i and absindex(#t, i) or 1
400 j = j and absindex(#t, j) or #t
401 local rtn = {}
402 for x = i < 1 and 1 or i, j > #t and #t or j do
403 rtn[#rtn + 1] = t[x]
404 end
405 return rtn
406end
407
408
409function lume.first(t, n)
410 if not n then return t[1] end
411 return lume.slice(t, 1, n)
412end
413
414
415function lume.last(t, n)
416 if not n then return t[#t] end
417 return lume.slice(t, -n, -1)
418end
419
420
421function lume.invert(t)
422 local rtn = {}
423 for k, v in pairs(t) do rtn[v] = k end
424 return rtn
425end
426
427
428function lume.pick(t, ...)
429 local rtn = {}
430 for i = 1, select("#", ...) do
431 local k = select(i, ...)
432 rtn[k] = t[k]
433 end
434 return rtn
435end
436
437
438function lume.keys(t)
439 local rtn = {}
440 local iter = getiter(t)
441 for k in iter(t) do rtn[#rtn + 1] = k end
442 return rtn
443end
444
445
446function lume.clone(t)
447 local rtn = {}
448 for k, v in pairs(t) do rtn[k] = v end
449 return rtn
450end
451
452
453function lume.fn(fn, ...)
454 assert(iscallable(fn), "expected a function as the first argument")
455 local args = { ... }
456 return function(...)
457 local a = lume.concat(args, { ... })
458 return fn(unpack(a))
459 end
460end
461
462
463function lume.once(fn, ...)
464 local f = lume.fn(fn, ...)
465 local done = false
466 return function(...)
467 if done then return end
468 done = true
469 return f(...)
470 end
471end
472
473
474local memoize_fnkey = {}
475local memoize_nil = {}
476
477function lume.memoize(fn)
478 local cache = {}
479 return function(...)
480 local c = cache
481 for i = 1, select("#", ...) do
482 local a = select(i, ...) or memoize_nil
483 c[a] = c[a] or {}
484 c = c[a]
485 end
486 c[memoize_fnkey] = c[memoize_fnkey] or {fn(...)}
487 return unpack(c[memoize_fnkey])
488 end
489end
490
491
492function lume.combine(...)
493 local n = select('#', ...)
494 if n == 0 then return noop end
495 if n == 1 then
496 local fn = select(1, ...)
497 if not fn then return noop end
498 assert(iscallable(fn), "expected a function or nil")
499 return fn
500 end
501 local funcs = {}
502 for i = 1, n do
503 local fn = select(i, ...)
504 if fn ~= nil then
505 assert(iscallable(fn), "expected a function or nil")
506 funcs[#funcs + 1] = fn
507 end
508 end
509 return function(...)
510 for _, f in ipairs(funcs) do f(...) end
511 end
512end
513
514
515function lume.call(fn, ...)
516 if fn then
517 return fn(...)
518 end
519end
520
521
522function lume.time(fn, ...)
523 local start = os.clock()
524 local rtn = {fn(...)}
525 return (os.clock() - start), unpack(rtn)
526end
527
528
529local lambda_cache = {}
530
531function lume.lambda(str)
532 if not lambda_cache[str] then
533 local args, body = str:match([[^([%w,_ ]-)%->(.-)$]])
534 assert(args and body, "bad string lambda")
535 local s = "return function(" .. args .. ")\nreturn " .. body .. "\nend"
536 lambda_cache[str] = lume.dostring(s)
537 end
538 return lambda_cache[str]
539end
540
541
542local serialize
543
544local serialize_map = {
545 [ "boolean" ] = tostring,
546 [ "nil" ] = tostring,
547 [ "string" ] = function(v) return string.format("%q", v) end,
548 [ "number" ] = function(v)
549 if v ~= v then return "0/0" -- nan
550 elseif v == 1 / 0 then return "1/0" -- inf
551 elseif v == -1 / 0 then return "-1/0" end -- -inf
552 return tostring(v)
553 end,
554 [ "table" ] = function(t, stk)
555 stk = stk or {}
556 if stk[t] then error("circular reference") end
557 local rtn = {}
558 stk[t] = true
559 for k, v in pairs(t) do
560 rtn[#rtn + 1] = "[" .. serialize(k, stk) .. "]=" .. serialize(v, stk)
561 end
562 stk[t] = nil
563 return "{" .. table.concat(rtn, ",") .. "}"
564 end
565}
566
567setmetatable(serialize_map, {
568 __index = function(_, k) error("unsupported serialize type: " .. k) end
569})
570
571serialize = function(x, stk)
572 return serialize_map[type(x)](x, stk)
573end
574
575function lume.serialize(x)
576 return serialize(x)
577end
578
579
580function lume.deserialize(str)
581 return lume.dostring("return " .. str)
582end
583
584
585function lume.split(str, sep)
586 if not sep then
587 return lume.array(str:gmatch("([%S]+)"))
588 else
589 assert(sep ~= "", "empty separator")
590 local psep = patternescape(sep)
591 return lume.array((str..sep):gmatch("(.-)("..psep..")"))
592 end
593end
594
595
596function lume.trim(str, chars)
597 if not chars then return str:match("^[%s]*(.-)[%s]*$") end
598 chars = patternescape(chars)
599 return str:match("^[" .. chars .. "]*(.-)[" .. chars .. "]*$")
600end
601
602
603function lume.wordwrap(str, limit)
604 limit = limit or 72
605 local check
606 if type(limit) == "number" then
607 check = function(s) return #s >= limit end
608 else
609 check = limit
610 end
611 local rtn = {}
612 local line = ""
613 for word, spaces in str:gmatch("(%S+)(%s*)") do
614 local s = line .. word
615 if check(s) then
616 table.insert(rtn, line .. "\n")
617 line = word
618 else
619 line = s
620 end
621 for c in spaces:gmatch(".") do
622 if c == "\n" then
623 table.insert(rtn, line .. "\n")
624 line = ""
625 else
626 line = line .. c
627 end
628 end
629 end
630 table.insert(rtn, line)
631 return table.concat(rtn)
632end
633
634
635function lume.format(str, vars)
636 if not vars then return str end
637 local f = function(x)
638 return tostring(vars[x] or vars[tonumber(x)] or "{" .. x .. "}")
639 end
640 return (str:gsub("{(.-)}", f))
641end
642
643
644function lume.trace(...)
645 local info = debug.getinfo(2, "Sl")
646 local t = { info.short_src .. ":" .. info.currentline .. ":" }
647 for i = 1, select("#", ...) do
648 local x = select(i, ...)
649 if type(x) == "number" then
650 x = string.format("%g", lume.round(x, .01))
651 end
652 t[#t + 1] = tostring(x)
653 end
654 print(table.concat(t, " "))
655end
656
657
658function lume.dostring(str)
659 return assert((loadstring or load)(str))()
660end
661
662
663function lume.uuid()
664 local fn = function(x)
665 local r = math.random(16) - 1
666 r = (x == "x") and (r + 1) or (r % 4) + 9
667 return ("0123456789abcdef"):sub(r, r)
668 end
669 return (("xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"):gsub("[xy]", fn))
670end
671
672
673function lume.hotswap(modname)
674 local oldglobal = lume.clone(_G)
675 local updated = {}
676 local function update(old, new)
677 if updated[old] then return end
678 updated[old] = true
679 local oldmt, newmt = getmetatable(old), getmetatable(new)
680 if oldmt and newmt then update(oldmt, newmt) end
681 for k, v in pairs(new) do
682 if type(v) == "table" then update(old[k], v) else old[k] = v end
683 end
684 end
685 local err = nil
686 local function onerror(e)
687 for k in pairs(_G) do _G[k] = oldglobal[k] end
688 err = lume.trim(e)
689 end
690 local ok, oldmod = pcall(require, modname)
691 oldmod = ok and oldmod or nil
692 xpcall(function()
693 package.loaded[modname] = nil
694 local newmod = require(modname)
695 if type(oldmod) == "table" then update(oldmod, newmod) end
696 for k, v in pairs(oldglobal) do
697 if v ~= _G[k] and type(v) == "table" then
698 update(v, _G[k])
699 _G[k] = v
700 end
701 end
702 end, onerror)
703 package.loaded[modname] = oldmod
704 if err then return nil, err end
705 return oldmod
706end
707
708
709local ripairs_iter = function(t, i)
710 i = i - 1
711 local v = t[i]
712 if v then return i, v end
713end
714
715function lume.ripairs(t)
716 return ripairs_iter, t, (#t + 1)
717end
718
719
720function lume.color(str, mul)
721 mul = mul or 1
722 local r, g, b, a
723 r, g, b = str:match("#(%x%x)(%x%x)(%x%x)")
724 if r then
725 r = tonumber(r, 16) / 0xff
726 g = tonumber(g, 16) / 0xff
727 b = tonumber(b, 16) / 0xff
728 a = 1
729 elseif str:match("rgba?%s*%([%d%s%.,]+%)") then
730 local f = str:gmatch("[%d.]+")
731 r = (f() or 0) / 0xff
732 g = (f() or 0) / 0xff
733 b = (f() or 0) / 0xff
734 a = f() or 1
735 else
736 error(("bad color string '%s'"):format(str))
737 end
738 return r * mul, g * mul, b * mul, a * mul
739end
740
741
742function lume.rgba(color)
743 local a = math_floor((color / 16777216) % 256)
744 local r = math_floor((color / 65536) % 256)
745 local g = math_floor((color / 256) % 256)
746 local b = math_floor((color) % 256)
747 return r, g, b, a
748end
749
750
751local chain_mt = {}
752chain_mt.__index = lume.map(lume.filter(lume, iscallable, true),
753 function(fn)
754 return function(self, ...)
755 self._value = fn(self._value, ...)
756 return self
757 end
758 end)
759chain_mt.__index.result = function(x) return x._value end
760
761function lume.chain(value)
762 return setmetatable({ _value = value }, chain_mt)
763end
764
765setmetatable(lume, {
766 __call = function(_, ...)
767 return lume.chain(...)
768 end
769})
770
771
772return lume