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