902 lines
24 KiB
Plaintext
902 lines
24 KiB
Plaintext
DependencyControl = require "l0.DependencyControl"
|
|
version = DependencyControl{
|
|
name: "(Almost) Functional Suite",
|
|
version: "0.6.0",
|
|
description: "Collection of commonly used functions",
|
|
author: "line0",
|
|
moduleName: "l0.Functional",
|
|
url: "https://github.com/TypesettingTools/Functional",
|
|
feed: "https://raw.githubusercontent.com/TypesettingTools/Functional/master/DependencyControl.json",
|
|
{"aegisub.util", "aegisub.unicode", "aegisub.re"},
|
|
}
|
|
util, unicode, re = version\requireModules!
|
|
logger = version\getLogger!
|
|
|
|
local list, _table, _math, _string, _function, _unicode, _util, _re, rngSeed
|
|
|
|
-- define some generic functions to be used as default parameters for callbacks
|
|
_function = {
|
|
identity: (...) -> ...
|
|
identical: (a, b) -> a == b
|
|
true: -> true
|
|
}
|
|
-- provide a convenient way to check if these generic functions were passed as an argument
|
|
-- so special optimized code paths can be easily provided
|
|
getArgType = (arg) ->
|
|
valType = type arg
|
|
if valType != "function"
|
|
return valType
|
|
|
|
for k, v in pairs _function
|
|
return v if arg == v
|
|
|
|
return "function"
|
|
|
|
|
|
-- list functions are designed to work on numerically keyed tables
|
|
-- calling list will create a new table that provides all of the list functions as methods
|
|
listMeta = {
|
|
__index: (tbl, key) -> list[key] or nil
|
|
}
|
|
|
|
list = setmetatable {
|
|
makeSet: (source, target = {}, overwrite = true, value = true) ->
|
|
target[v] = value for v in *source when overwrite or not target[v]
|
|
return target
|
|
|
|
chunk: (tbl, size = 1) ->
|
|
chunks, c, nextStart = {}, 0, 1
|
|
for i, v in ipairs tbl
|
|
if i == nextStart
|
|
c += 1
|
|
chunks[c], nextStart = {v}, i + size
|
|
else chunks[c][i+size+1-nextStart] = v
|
|
|
|
return chunks, c
|
|
|
|
compact: (tbl, includeFalse = false) ->
|
|
if includeFalse
|
|
[v for _, v in pairs tbl when v]
|
|
else [v for _, v in pairs tbl when v != nil]
|
|
|
|
compare: (a, b, target, iteratee) ->
|
|
if not iteratee and "function" == type target
|
|
target, iteratee = a, target
|
|
|
|
seenIndices, s = {}, 0
|
|
for i, v in ipairs b
|
|
target[i] = iteratee a[i], v, i, a, b
|
|
seenIndices[i] = true
|
|
s += 1
|
|
|
|
for i, v in ipairs a
|
|
unless seenIndices[i]
|
|
target[i] = iteratee v, b[i], i, a, b
|
|
s += 1
|
|
|
|
return target, s
|
|
|
|
compareLeft: (a, b, target, iteratee) ->
|
|
if not iteratee and "function" == type target
|
|
target, iteratee = a, target
|
|
|
|
s = 0
|
|
for i, v in ipairs a
|
|
target[i] = iteratee v, b[i], i, a, b
|
|
s += 1
|
|
|
|
return target, s
|
|
|
|
copy: (tbl) -> [v for v in *tbl]
|
|
|
|
diff: (left, ...) ->
|
|
rightSet = {}
|
|
list.makeSet right, rightSet for right in *{...}
|
|
diff = [v for v in *left when not rightSet[v]]
|
|
return diff, rightSet
|
|
|
|
filter: (tbl, predicate) ->
|
|
[v for i, v in ipairs tbl when predicate v, i, tbl]
|
|
|
|
findInRange: (tbl, first = 1, last = #tbl, predicate) ->
|
|
for i = first, last, #tbl
|
|
return tbl[i], i if predicate tbl[i], i, tbl
|
|
|
|
find: (tbl, predicate) ->
|
|
for i, v in ipairs tbl
|
|
return v, i if predicate v, i, tbl
|
|
|
|
groupBy: (tbl, selector = _function.identity) ->
|
|
groups = {}
|
|
selType = getArgType selector
|
|
|
|
for i, v in ipairs tbl
|
|
groupKey = switch selType
|
|
when _function.identity then v
|
|
when "function" then selector v, i, tbl
|
|
else v[selector]
|
|
|
|
group = groups[groupKey]
|
|
if group
|
|
group.n += 1
|
|
group[group.n] = v
|
|
else groups[groupKey] = {v, n: 1}
|
|
|
|
return groups
|
|
|
|
indexBy: (tbl, key, onlyTables = true) ->
|
|
{v[key], v for v in *tbl when not onlyTables or type(v) == "table" and v[key] != nil}
|
|
|
|
indexOf: (tbl, item, first = 1, last = #tbl, reverse) ->
|
|
return if #tbl == 0
|
|
first = #tbl - first + 1 if first < 0
|
|
last = #tbl - last + 1 if last < 0
|
|
|
|
if reverse
|
|
return i for i = last, first, -1 when tbl[i] == item
|
|
else
|
|
return i for i = first, last when tbl[i] == item
|
|
|
|
intersect: (tbl, ...) ->
|
|
others = {...}
|
|
otherCnt = #others
|
|
switch otherCnt
|
|
when 0 then return list.copy tbl
|
|
when 1
|
|
set = list.makeSet tbl
|
|
return [v for v in *others[1] when set[v]]
|
|
else
|
|
intersection = {v, 0 for v in *tbl}
|
|
|
|
for i, other in ipairs others
|
|
intersection[v] = i for v in *other when intersection[v] == i-1
|
|
|
|
return [k for k, v in pairs intersection when v == otherCnt]
|
|
|
|
join: (...) ->
|
|
joined, j, tbls = {}, 0, {...}
|
|
return if #tbls == 0
|
|
|
|
for tbl in *tbls
|
|
for v in *tbl
|
|
j += 1
|
|
joined[j] = v
|
|
|
|
return joined, j
|
|
|
|
joinInto: (target, ...) ->
|
|
t, tbls = #target, {...}
|
|
|
|
for tbl in *tbls
|
|
for v in *tbl
|
|
t += 1
|
|
target[t] = v
|
|
|
|
return target, t
|
|
|
|
lastIndexOf: (tbl, item, first = 1, last = #tbl) ->
|
|
list.indexOf tbl, item, first, last, true
|
|
|
|
map: (tbl, selector = _function.identity, compact = false, remapNumKeys = false) ->
|
|
mapped, m, n = {}, 0, 0
|
|
for i, v in ipairs tbl
|
|
mapVal, mapKey = selector v, i, tbl
|
|
continue if compact and mapVal == nil
|
|
if mapKey == nil or remapNumKeys and mapKey == type "number"
|
|
m += 1
|
|
mapped[m] = mapVal
|
|
else
|
|
mapped[mapKey] = mapVal
|
|
n += 1
|
|
|
|
return mapped, m + n
|
|
|
|
mapCompact: (tbl, selector = _function.identity, remapNumKeys = false) ->
|
|
return list.map tbl, selector, true, remapNumKeys
|
|
|
|
pluck: (tbl, key, onlyTables = true) ->
|
|
[v[key] for v in *tbl when not onlyTables or "table" == type v]
|
|
|
|
reduce: (tbl, initial = nil, iteratee = _function.identity) ->
|
|
haveInitial = initial != nil
|
|
reduced = initial if haveInitial else tbl[1]
|
|
|
|
reduced = iteratee(reduced, v, i, tbl) for i, v in ipairs tbl when haveInitial or i > 1
|
|
return reduced
|
|
|
|
removeRange: (tbl, first, last = -1) ->
|
|
len, removed = #tbl, {}
|
|
first += len+1 if first < 0
|
|
last += len+1 if last < 0
|
|
rmCnt = last - first + 1
|
|
for i = first, last
|
|
tbl[i], removed[i-first+1] = tbl[i+rmCnt], tbl[i]
|
|
for i = last + 1, len
|
|
tbl[i] = tbl[i+rmCnt]
|
|
|
|
return removed, rmCnt
|
|
|
|
removeIndices: (tbl, indices = {}) ->
|
|
removed, shift = {}, 0
|
|
indexCnt = #indices
|
|
|
|
switch indexCnt
|
|
when 0 then return removed, 0
|
|
when 1 then return {table.remove tbl, indices[1]}, 1
|
|
else
|
|
i, tblLen, indexSet = 1, #tbl, list.makeSet indices
|
|
while i <= tblLen + shift
|
|
if i <= tblLen and indexSet[i]
|
|
shift += 1
|
|
removed[shift] = tbl[i]
|
|
elseif shift > 0
|
|
tbl[i-shift] = tbl[i]
|
|
i += 1
|
|
return removed, shift
|
|
|
|
removeValues: (tbl, ...) ->
|
|
values, tblLen, i, removed, shift = {...}, #tbl, 1, {}, 0
|
|
valCnt, valueSet = #values, list.makeSet values
|
|
return removed, 0 if valCnt == 0
|
|
|
|
while i <= tblLen + shift
|
|
if i <= tblLen and valueSet[tbl[i]]
|
|
shift += 1
|
|
removed[shift] = tbl[i]
|
|
elseif shift > 0
|
|
tbl[i-shift] = tbl[i]
|
|
i += 1
|
|
return removed, shift
|
|
|
|
removeWhere: (tbl, predicate = _function.true, sparse = false) ->
|
|
removeAll = predicate == _function.true
|
|
removed, r = {}, 0
|
|
for i, v in ipairs tbl
|
|
if removeAll or predicate v, i, tbl
|
|
r += 1
|
|
if sparse
|
|
removed[r], tbl[i] = v
|
|
else removed[r] = i
|
|
|
|
if sparse
|
|
return removed, r
|
|
else
|
|
return list.removeIndexes(tbl, removed), r
|
|
|
|
reverse: (tbl) ->
|
|
reversed, r = {}, #tbl
|
|
for v in *tbl
|
|
reversed[r] = v
|
|
r -= 1
|
|
return reversed
|
|
|
|
slice: (tbl, first = 1, last = -1) ->
|
|
len = #tbl
|
|
first += len+1 if first < 0
|
|
last += len+1 if last < 0
|
|
|
|
return [v for v in *tbl[first, last]]
|
|
|
|
trim: (tbl, first = 1, last = -1) ->
|
|
len = #tbl
|
|
first += len+1 if first < 0
|
|
last += len+1 if last < 0
|
|
removed = {}
|
|
|
|
if first > 1
|
|
removed[first + i - last - 1] = tbl[i] for i = last + 1, len
|
|
removed[i] = tbl[i] for i = 1, first - 1
|
|
|
|
tbl[i] = tbl[i+first-1] for i = 1, last - first + 1
|
|
tbl[i] = nil for i = last - first + 2, len
|
|
|
|
elseif last < len
|
|
for i = last + 1, len
|
|
removed[i-last], tbl[i] = tbl[i]
|
|
|
|
return removed, len - last + first - 1
|
|
|
|
uniq: (tbl, selector = _function.identity) -> -- TODO: optimization for sorted lists
|
|
values, unique, u = {}, {}, 0
|
|
identitySel = selector == _function.identity
|
|
|
|
for i, v in ipairs tbl
|
|
cmp = identitySel and v or not identitySel and selector v, i, tbl
|
|
continue if cmp == nil or values[cmp]
|
|
u += 1
|
|
unique[u], values[cmp] = v, true
|
|
|
|
return unique, u
|
|
|
|
}, { __call: (_, tbl = {}) -> setmetatable tbl, listMeta }
|
|
|
|
_math = {
|
|
degrees: (rad) ->
|
|
return rad * 180/math.pi
|
|
|
|
isInt: (num) ->
|
|
type_ = type num
|
|
return type_ == "number" and num == math.floor(num), type_
|
|
|
|
assertInt: (num, varName = "Number") ->
|
|
isInt, type_ = _math.isInt num
|
|
logger\assert isInt, "%s must be an integer, got a %s.", varName, type_
|
|
|
|
inRange: (num, min, max, checkInt) ->
|
|
type(num) == "number" and num >= min and num <= max and (not checkInt or math.floor(num) == num)
|
|
|
|
assertInRange: (num, min, max, checkInt, varName = "Number") ->
|
|
logger\assert _math.inRange(num, min, max, checkInt), "%s must be %sin range %d-%d, got %s.", varName,
|
|
checkInt and "an integer " or "", min, max, tostring(num)
|
|
|
|
nan: 0/0
|
|
|
|
randomFloat: (min = 0, max = 1) ->
|
|
return min + math.random! * (max - min)
|
|
|
|
round: (num, idp = 0) ->
|
|
return num if idp == math.huge
|
|
fac = 10^idp
|
|
return math.floor(num * fac + 0.5) / fac
|
|
|
|
roundMany: (idp = 0, ...) ->
|
|
fac = 10^idp
|
|
return unpack for i = 1, select '#', ...
|
|
math.floor(select(i, ...) * fac + 0.5) / fac
|
|
|
|
seedRNG: (seed = false) ->
|
|
if "boolean" == type seed
|
|
return rngSeed if seed == false and rngSeed
|
|
rngSeed = os.time!
|
|
else rngSeed = seed
|
|
|
|
math.randomseed rngSeed
|
|
return rngSeed
|
|
|
|
sign: (num, signedZero) ->
|
|
return _math.nan if num != num
|
|
return 1 if num > 0
|
|
return -1 if num < 0
|
|
|
|
if signedZero
|
|
return #tostring(num/math.abs num) > 1 and -1 or 1
|
|
return 0
|
|
|
|
sum: (num, ...) ->
|
|
num += n for n in *{...}
|
|
return num
|
|
|
|
toStrings: (...) -> unpack [tostring n for n in *table.pack ...]
|
|
|
|
vector2: {
|
|
distance: (x1, y1, x2, y2) ->
|
|
return math.sqrt (x2-x1)^2 + (y2-y1)^2
|
|
|
|
normalize: (x, y, length = 1) ->
|
|
return _math.nan, _math.nan if x == 0 and y == 0
|
|
|
|
fac = length / _math.vector2.distance 0, 0, x, y
|
|
return x*fac, y*fac
|
|
}
|
|
}
|
|
|
|
formatError = (args, a, opts, type_, msg) ->
|
|
"failed to format arg #{a} (#{tostring args[a]}) to #{opts}#{type_}: #{msg}"
|
|
|
|
_string = {
|
|
escLuaExp: (str) -> str\gsub "([%%%(%)%[%]%.%*%-%+%?%$%^])", "%%%1"
|
|
|
|
escRegExp: (str) -> str\gsub "([\\/%^%$%.|%?%*%+%(%)%[%]%{%}])", "\\%1"
|
|
|
|
split: (str, sep = " ", init = 1, plain = true, limit = -1) ->
|
|
first, last = str\find sep, init, plain
|
|
-- fast return if there's nothing to split - saves one str.sub()
|
|
return {str}, 1 if not first or limit == 0
|
|
|
|
splits, s = {}, 1
|
|
while first and s != limit + 1
|
|
splits[s] = str\sub init, first - 1
|
|
s += 1
|
|
init = last + 1
|
|
first, last = str\find sep, init, plain
|
|
|
|
splits[s] = str\sub init
|
|
return splits, s
|
|
|
|
formatEx: (fmtStr, ...) ->
|
|
args, a = table.pack(...), 1
|
|
local errors
|
|
str = fmtStr\gsub "(%%[%+%- 0]*%d*.?%d*[hlLzjtI]*)([aABcedEfFgGcnNopiuAsuxX])", (opts, type_) ->
|
|
repl = switch type_
|
|
when "N" -- nicely formatted float (no trailing zeroes)
|
|
success, result = pcall string.format, "#{opts}f", args[a]
|
|
if success
|
|
tonumber result
|
|
else
|
|
errors or= {}
|
|
errors[#errors+1] = formatError args, a, opts, type_, result
|
|
|
|
when "B" -- trueish/falsish as 1 and 0
|
|
args[a] and 1 or 0
|
|
else
|
|
success, result = pcall string.format, opts..type_, args[a]
|
|
if success
|
|
result
|
|
else
|
|
errors or= {}
|
|
errors[#errors+1] = formatError args, a, opts, type_, result
|
|
a += 1
|
|
return repl
|
|
|
|
if errors
|
|
return nil, table.concat errors, '; '
|
|
return str
|
|
|
|
pad: (str, charCnt, padStr = "0", right = false) ->
|
|
repCnt = charCnt - #str
|
|
return str if repCnt < 1
|
|
padding = padStr\rep(math.ceil repCnt / #padStr)\sub 1, repCnt
|
|
return right and str .. padding or padding .. str
|
|
|
|
trim: (str) -> str\gsub "^%s*(.-)%s*$", "%1"
|
|
|
|
trimLeft: (str) -> str\gsub "^%s*(.+)$", "%1"
|
|
|
|
trimRight: (str) -> str\gsub "^(.-)%s*$", "%1"
|
|
|
|
toNumbers: (base, ...) ->
|
|
numbers, n = {}, 1
|
|
if type(base) != "number"
|
|
numbers[1], n = base, 2
|
|
base = 10
|
|
|
|
for str in table.pack ...
|
|
numbers[n] = tonumber str, base
|
|
n += 1
|
|
|
|
return numbers, n-1
|
|
|
|
}
|
|
|
|
_table = {
|
|
addDefaults: (tbl, defaults, predicate) ->
|
|
addedCnt = 0
|
|
|
|
for k, v in pairs defaults
|
|
if not predicate and tbl[k] == nil or predicate and predicate tbl[k], k, tbl
|
|
addedCnt += 1
|
|
tbl[k] = v
|
|
|
|
return addedCnt
|
|
|
|
|
|
compare: (a, b, target, iteratee) ->
|
|
if not iteratee and "function" == type target
|
|
target, iteratee = a, target
|
|
|
|
seenKeys, s = {}, 0
|
|
for k, v in pairs b
|
|
target[k] = iteratee a[k], v, k, a, b
|
|
seenKeys[k] = true
|
|
s += 1
|
|
|
|
for k, v in pairs a
|
|
unless seenKeys[k]
|
|
target[k] = iteratee v, b[k], k, a, b
|
|
s += 1
|
|
|
|
return target, s
|
|
|
|
compareLeft: (a, b, target, iteratee) ->
|
|
if not iteratee and "function" == type target
|
|
target, iteratee = a, target
|
|
|
|
s = 0
|
|
for k, v in pairs a
|
|
target[k] = iteratee v, b[k], k, a, b
|
|
s += 1
|
|
|
|
return target, s
|
|
|
|
copy: (tbl) -> {k, v for k, v in pairs tbl}
|
|
|
|
copy_deep: util.deep_copy
|
|
|
|
deepCopy: util.deep_copy
|
|
|
|
-- selects key/value pairs from left which are different from the values found at the same key in right
|
|
diff: (left, right, sparse = false, comparator = _function.identical) ->
|
|
diff, d = {}, 0
|
|
identicalComp = comparator == _function.identical
|
|
|
|
for k, vLeft in pairs left
|
|
vRight = right[k]
|
|
continue if sparse and vRight == nil
|
|
if identicalComp and vRight != vLeft or not identicalComp and not comparator vLeft, vRight, k
|
|
diff[k] = vLeft
|
|
d += 1
|
|
|
|
return diff, d
|
|
|
|
equals: (a, b) ->
|
|
return _util.equals a, b, "table", "table"
|
|
|
|
filter: (tbl, predicate) ->
|
|
filtered, f = {}, 0
|
|
for k, v in pairs tbl
|
|
if predicate v, k, tbl
|
|
filtered[k] = v
|
|
f += 1
|
|
|
|
return filtered, f
|
|
|
|
find: (tbl, predicate) ->
|
|
for k, v in pairs tbl
|
|
return v, k if predicate v, k, tbl
|
|
|
|
findKey: (tbl, value) ->
|
|
for k, v in pairs tbl
|
|
return k if v == value
|
|
|
|
intersect: (...) ->
|
|
tbls = table.pack ...
|
|
return _table.intersectEqual tbls, _function.identical, tbls.n
|
|
|
|
intersectEqual: (tbls, comparator = _table.equals, tblCnt = #tbls) ->
|
|
first = tbls[1]
|
|
intersection, i = {}, 0
|
|
return nil if tblCnt == 0
|
|
identicalComp = comparator == nil or comparator == _function.identical
|
|
|
|
for k, v in pairs first
|
|
allEqual = true
|
|
for j = 2, tblCnt
|
|
if identicalComp and tbls[j][k] != v or not identicalComp and comparator v, tbls[j][k], k
|
|
allEqual = false
|
|
break
|
|
|
|
if allEqual
|
|
intersection[k] = v
|
|
i += 1
|
|
|
|
return intersection, i
|
|
|
|
invert: (tbl, multiValue) ->
|
|
unless multiValue
|
|
return {v, k for k, v in pairs tbl}
|
|
|
|
inverted = {}
|
|
for k, v in pairs tbl
|
|
if inverted[v]
|
|
inverted[v].n += 1
|
|
inverted[v][inverted[v].n] = k
|
|
else inverted[v] = {k, n: 1}
|
|
|
|
return inverted
|
|
|
|
isList: (tbl) ->
|
|
len = _table.length tbl
|
|
return #tbl == len, len
|
|
|
|
keys: (tbl, except) ->
|
|
keys, k = {}, 0
|
|
|
|
exceptSet = switch type except
|
|
when "table" then list.makeSet except
|
|
when "nil" then nil
|
|
else {[except]: true}
|
|
|
|
for key, _ in pairs tbl
|
|
if except == nil or not exceptSet[key]
|
|
k += 1
|
|
keys[k] = key
|
|
|
|
return keys, k
|
|
|
|
length: (tbl) ->
|
|
n = 0
|
|
n += 1 for _, _ in pairs tbl
|
|
return n
|
|
|
|
continuous: (tbl) ->
|
|
continuous, c = {}, 1
|
|
for k, v in pairs tbl
|
|
if "number" == type k
|
|
continuous[c], c = v, c + 1
|
|
else continuous[k] = v
|
|
|
|
return continuous
|
|
|
|
map: (tbl, selector = _function.identity, compact = true, remapNumKeys = false) ->
|
|
mapped, m, n = {}, 0, 0
|
|
for k, v in pairs tbl
|
|
mapVal, mapKey = selector v, k, tbl
|
|
continue if compact and mapVal == nil
|
|
if mapKey == nil or remapNumKeys and mapKey == type "number"
|
|
m += 1
|
|
mapped[m] = mapVal
|
|
else
|
|
mapped[mapKey] = mapVal
|
|
n += 1
|
|
|
|
return mapped, m + n
|
|
|
|
merge: (target, source, overwrite = true) ->
|
|
mergeCnt = 0
|
|
for k, v in pairs source
|
|
if overwrite or target[k] == nil
|
|
target[k] = v
|
|
mergeCnt += 1
|
|
return target, mergeCnt
|
|
|
|
pick: (tbl, selector) ->
|
|
picked, p = {}, 0
|
|
|
|
switch type selector
|
|
when "table"
|
|
keySet = list.makeSet selector
|
|
for k, v in pairs tbl
|
|
if keySet[k]
|
|
picked[k] = v
|
|
p += 1
|
|
|
|
when "function"
|
|
for k, v in pairs tbl
|
|
if selector v, k tbl
|
|
picked[k] = v
|
|
p += 1
|
|
|
|
return picked, p
|
|
|
|
pluck: (tbl, key, onlyTables = true) ->
|
|
[v[key] for _,v in pairs tbl when not onlyTables or "table" == type v]
|
|
|
|
-- fast in-place intersect
|
|
purgeDiff: (target, ...) ->
|
|
tbls = table.pack ...
|
|
tblCnt, intCnt = tbls.n, 0
|
|
|
|
for k, v in pairs target
|
|
allEqual = true
|
|
for i = 1, tblCnt
|
|
if tbls[i][k] != v
|
|
allEqual = false
|
|
break
|
|
|
|
if allEqual
|
|
intCnt += 1
|
|
else target[k] = nil
|
|
|
|
return intCnt
|
|
|
|
reduce: (tbl, iteratee = _function.identity, initial) ->
|
|
reduced = initial
|
|
reduced = iteratee(reduced, v, i, tbl) for i, v in pairs tbl
|
|
return reduced
|
|
|
|
removeAll: (tbl) ->
|
|
return _table.removeWhere tbl, _function.true
|
|
|
|
removeKeys: (tbl, keys, exceptMode) ->
|
|
keySet = list.makeSet keys
|
|
removed, r = {}, 0
|
|
|
|
for k, v in pairs tbl
|
|
if not exceptMode and keySet[k] or exceptMode and not keySet[k]
|
|
r += 1
|
|
removed, tbl[k] = tbl[k]
|
|
|
|
return removed, r
|
|
|
|
removeKeysExcept: (tbl, keys) ->
|
|
return _table.removeKeys tbl, keys, true
|
|
|
|
removeWhere: (tbl, predicate) ->
|
|
removeAll = predicate == _function.true
|
|
removed, r = {}, 0
|
|
for k, v in pairs tbl
|
|
if removeAll or predicate v, k, tbl
|
|
r += 1
|
|
removed[k], tbl[k] = v
|
|
|
|
return removed, r
|
|
|
|
transform: (tbl, iteratee = _function.identity) ->
|
|
return _table.reduce tbl, iteratee, {}
|
|
|
|
union: (...) ->
|
|
tbls = table.pack ...
|
|
union, u = {}, 0
|
|
|
|
for i = 1, tbls.n
|
|
for k, v in pairs tbls[i]
|
|
if union[k] == nil
|
|
union[k] = v
|
|
u += 1
|
|
|
|
return union, u
|
|
|
|
uniq: (tbl, selector = _function.identity) -> -- TODO: optimization for sorted lists
|
|
values, unique, u = {}, {}, 0
|
|
identitySel = selector == _function.identity
|
|
|
|
for k, v in pairs tbl
|
|
cmp = identitySel and v or not identitySel and selector v, k, tbl
|
|
continue if cmp == nil or values[cmp]
|
|
u += 1
|
|
unique[k], values[cmp] = v, true
|
|
|
|
return unique, u
|
|
|
|
values: (tbl, sortComp) ->
|
|
values, i = {}, 0
|
|
for _, v in pairs tbl
|
|
i += 1
|
|
values[i] = v
|
|
|
|
if sortComp == true
|
|
table.sort values
|
|
elseif sortComp
|
|
table.sort values, sortComp
|
|
|
|
return values, i
|
|
}
|
|
|
|
_util = {
|
|
equals: DependencyControl.UnitTestSuite.UnitTest.equals
|
|
itemsEqual: DependencyControl.UnitTestSuite.UnitTest.itemsEqual
|
|
|
|
formatTimecode: (time, format) ->
|
|
splits = _util.splitTimestamp time
|
|
|
|
return _re.replace format, "(h+|m+|s+|f+)", (flag) ->
|
|
_string.pad tostring(splits[flag\sub 1, 1]), #flag
|
|
|
|
splitTimestamp: (time) ->
|
|
splitTime = (time, div) ->
|
|
split = time % div
|
|
return split, (time - split) / div
|
|
|
|
splits = {}
|
|
splits.f, time = splitTime time, 1000
|
|
splits.s, time = splitTime time, 60
|
|
splits.m, time = splitTime time, 60
|
|
splits.h = time
|
|
return splits
|
|
|
|
getScriptInfo: (sub) ->
|
|
infoBlockSeen, scriptInfo = false, {}
|
|
for line in *sub
|
|
if line.class == "info"
|
|
infoBlockSeen = true
|
|
scriptInfo[line.key] = line.value
|
|
elseif infoBlockSeen
|
|
break
|
|
return scriptInfo
|
|
|
|
RGB_to_HSV: (r, g, b) ->
|
|
r, g, b = util.clamp(r, 0, 255), util.clamp(g, 0, 255), util.clamp(b, 0, 255)
|
|
v = math.max r, g, b
|
|
delta = v - math.min r, g, b
|
|
if delta == 0
|
|
return 0, 0, v/255
|
|
else
|
|
s = delta/v
|
|
h = 60*(r == v and (g-b)/delta or g == v and (b-r)/delta + 2 or (r-g)/delta + 4)
|
|
return h > 0 and h or h+360, s, v/255
|
|
|
|
assTimecode2ms: (tc) ->
|
|
num = tonumber
|
|
split = {tc\match "^(%d):(%d%d):(%d%d)%.(%d%d)$"}
|
|
if #split != 4
|
|
return nil, "invalid ASS timecode"
|
|
return ((num(split[1])*60 + num(split[2]))*60 + num(split[3]))*1000 + num(split[4])*10
|
|
|
|
ms2AssTimecode: (time) ->
|
|
{:h, :m, :s, :f} = _util.splitTimestamp time
|
|
if h > 9
|
|
return nil, "value too large to create an ASS timecode"
|
|
return string.format("%01d:%02d:%02d.%02d", h, m, s, f/10)
|
|
|
|
uuid: ->
|
|
-- https://gist.github.com/jrus/3197011
|
|
"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"\gsub "[xy]", (c) ->
|
|
v = c == "x" and math.random(0, 0xf) or math.random 8, 0xb
|
|
return "%x"\format v
|
|
}
|
|
|
|
_unicode = {
|
|
toCharTable: (s) ->
|
|
charNum, charStart, uniChars = 1, 1, {}
|
|
while charStart <= #s
|
|
charEnd = charStart - 1 + unicode.charwidth s\sub charStart, charStart
|
|
uniChars[charNum] = s\sub charStart, charEnd
|
|
charStart, charNum = charEnd+1, charNum+1
|
|
|
|
return uniChars
|
|
|
|
reverse: (s) -> table.concat list.reverse _unicode.toCharTable s
|
|
|
|
sub: (s, i = 1, j) ->
|
|
uniChars = _unicode.toCharTable s
|
|
charCnt = #uniChars
|
|
j or= charCnt
|
|
|
|
i = i < 0 and math.max(charCnt+i+1,1) or util.clamp i, 1, charCnt
|
|
j = j < 0 and math.max(charCnt+j+1,1) or util.clamp j, 1, charCnt
|
|
return table.concat uniChars, "", i, j
|
|
}
|
|
|
|
_re = {
|
|
matches: (str, pattern, ...) ->
|
|
regex = re.compile pattern, ...
|
|
chars = _unicode.toCharTable str
|
|
charCnt, last = #chars, 0
|
|
->
|
|
return if last >= charCnt
|
|
matches = regex\match table.concat chars, "", last+1, charCnt
|
|
matchCnt = #matches
|
|
return unless matches
|
|
last += matches[1].last
|
|
start = matchCnt == 1 and 1 or 2
|
|
unpack [matches[i].str for i = start, matchCnt]
|
|
|
|
replace: (str, pattern, callback, ...) ->
|
|
regex = if type(pattern) == "table" and type(pattern._regex) == "cdata"
|
|
pattern
|
|
else re.compile pattern, ...
|
|
|
|
chars = _unicode.toCharTable str
|
|
charCnt, last, replacements, r = #chars, 0, {}, 1
|
|
-- since we can only ever get one match at a time out of re.match
|
|
-- we need to run recursively over the not-yet-matched substring
|
|
-- until we either hit the end of string or no more matches are found
|
|
while last < charCnt
|
|
matches = regex\match table.concat chars, "", last+1, charCnt
|
|
-- stop if no further matches can be found
|
|
break unless matches
|
|
matchCnt = #matches
|
|
|
|
-- discard the overall match table when there are subgroups
|
|
start = matchCnt == 1 and 1 or 2
|
|
-- pass the matches to the callback, and insert a replacement table
|
|
-- to the global list of replacements for every return value that is a string
|
|
rep = {callback unpack [matches[i].str for i = start, matchCnt]}
|
|
|
|
for i = start, matchCnt
|
|
continue if "string" != type rep[i+1-start]
|
|
-- add offset to make first/last index into the source string
|
|
-- rather than the current substring
|
|
replacements[r] = first: matches[i].first+last, last: matches[i].last+last, str: rep[i+1-start]
|
|
r += 1
|
|
|
|
last += matches[1].last
|
|
|
|
-- splice together the result using the replacement strings
|
|
-- as well as the original unicode characters for the gaps inbetween their indexes
|
|
fragments, f, last = {}, 0, 0
|
|
|
|
for rep in *replacements
|
|
fragments[f+c] = chars[c+last] for c = 1, rep.first-last-1
|
|
f += rep.first - last
|
|
fragments[f], last = rep.str, rep.last
|
|
|
|
-- don't forget the tail after the last replacement
|
|
fragments[f+c] = chars[c+last] for c = 1, #chars-last
|
|
|
|
return table.concat fragments
|
|
}
|
|
|
|
return version\register {
|
|
function: _function
|
|
:list
|
|
List: list
|
|
string: _table.union string, _string
|
|
math: _table.union math, _math
|
|
table: _table.union table, _table
|
|
util: _table.union util, _util
|
|
:version
|
|
re: _table.union re, _re
|
|
unicode: _table.union unicode, _unicode
|
|
}
|