This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,63 @@
--[[
Create an error object with a specified name and message.
In native Lua, errors can only be string values. At Roblox, we can take advantage of throwing
error objects to provide structured information about problems that occur.
The tags table stores serializable information about an error which can be provided when it is
thrown, and later passed to a logging endpoint.
Throwing an error instance captures its stack trace, avoiding the need to explicitly use xpcall.
@usage In general, errors should not be used during normal control flow.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local class = require(Dash.class)
local format = require(Dash.format)
local join = require(Dash.join)
--[[
Create a new Error instance.
@param name The name of the error
@param string A message for the error which will be formatted using Dash.format
@param tags Any fixed tags
]]
local Error = class("Error", function(name: string, message: string, tags: Types.Table?)
return {
name = name,
message = message or "An error occurred",
tags = tags or {}
}
end)
function Error:toString(): string
return format("{}: {}\n{}", self.name, format(self.message, self.tags), self.stack)
end
--[[
Return a new error instance containing the tags provided joined to any existing tags of the
current error instance.
]]
function Error:joinTags(tags: Types.Table?): Error
return Error.new(self.name, self.message, join(self.tags, tags))
end
--[[
Throw an error.
The stack of the error is captured and stored.
If additional tags are provided, a new error is created with the joined tags of
this instance.
]]
function Error:throw(tags: Types.Table?)
local instance = self:joinTags(tags)
instance.stack = debug.traceback()
error(instance)
end
-- TODO Luau: Define class types automatically
export type Error = typeof(Error.new("", ""))
return Error
@@ -0,0 +1,13 @@
--[[
A symbol representing nothing, that can be used in place of nil as a key or value of a table,
where nil is illegal.
Utility functions can check for the None symbol and treat it like a nil value.
@usage Use cases include:
1. Creating an ordered list with undefined values in it
2. Creating a map with a key pointing to a nil value
]]
local Symbol = require(script.Parent.Symbol)
local None = Symbol.new("None")
return None
@@ -0,0 +1,32 @@
--[[
Create a symbol with a specified name. Upper snake-case is recommended as the symbol is a
constant, unless you are linking the symbol conceptually to a different string.
Symbols are useful when you want a value that isn't equal to any other type, for example if you
want to store a unique property on an object that won't be accidentally accessed with a simple
string lookup.
@example
local CHEESE = Symbol.new("CHEESE")
local FAKE_CHEESE = Symbol.new("CHEESE")
print(CHEESE == CHEESE) --> true
print(CHEESE == FAKE_CHEESE) --> false
print(tostring(CHEESE)) --> "Symbol.new(CHEESE)"
]]
local Dash = script.Parent
local class = require(Dash.class)
local Symbol = class("Symbol", function(name: string)
return {
name = name
}
end)
function Symbol:toString(): string
return ("Symbol(%s)"):format(self.name)
end
-- TODO Luau: Define class types automatically
export type Symbol = typeof(Symbol.new(""))
return Symbol
@@ -0,0 +1,19 @@
-- TODO Luau: Support these globally
-- A table with values of type _Value_ and numeric keys 1..n with no gaps
export type Array<Value> = {[number]: Value}
-- A table with values of type _Value_ and numeric keys, possibly with gaps
export type Args<Value> = {[number]: Value}
-- A table with keys of type _Key_ and values of type _Value_
export type Map<Key, Value> = {[Key]: Value}
-- A table with keys of a fixed type _Key_ and a boolean value representing membership of the set (default is false)
export type Set<Key> = {[Key]: boolean}
-- A table of any type
export type Table = {[any]: any}
-- A class has a constructor returning an instance of _Object_ type
export type Class<Object> = {
new: () -> Object
}
-- Represents a function which takes any arguments and returns any value
export type AnyFunction = () -> any
return {}
@@ -0,0 +1,31 @@
--[[
Adds new elements to the _target_ Array from subsequent Array arguments in left-to-right order.
Arguments which are `nil` or None are skipped.
@mutable target
]]
local Dash = script.Parent
local None = require(Dash.None)
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local forEachArgs = require(Dash.forEachArgs)
local forEach = require(Dash.forEach)
local insert = table.insert
-- TODO Luau: Add varags typings
local function append(target: Types.Array<any>, ...): Types.Array<any>
assertEqual(typeof(target), "table", [[Attempted to call Dash.append with argument #1 of type {left:?} not {right:?}]])
forEachArgs(function(list: Types.Table?)
if list == None or list == nil then
return
end
forEach(list, function(value: any)
insert(target, value)
end)
end, ...)
return target
end
return append
@@ -0,0 +1,23 @@
--[[
Performs a simple equality check and throws an error if _left_ is not equal to _right_.
The formatted error message can be customized, which by default provides a serialization of
both inputs using Dash.pretty.
The `left` and `right` values are available to be referenced in the formatted message.
]]
local Dash = script.Parent
local function assertEqual(left: any, right: any, formattedErrorMessage: string?)
if left ~= right then
local Error = require(Dash.Error)
local TypeError = Error.new("AssertError", formattedErrorMessage or [[Left {left:?} does not equal right {right:?}]])
TypeError:throw({
left = left,
right = right
})
end
end
return assertEqual
@@ -0,0 +1,73 @@
--[[
Adds new values to _target_ from subsequent Table arguments in left-to-right order.
The None symbol can be used to remove existing elements in the target.
@param ... any number of other tables
@example
local characters = {
Frodo = {
name = "Frodo Baggins",
team = "blue"
},
Boromir = {
score = 5
}
}
local otherCharacters = {
Frodo = {
team = "red",
score = 10
},
Bilbo = {
team = "yellow",
},
Boromir = {
score = {1, 2, 3}
}
}
local result = assign(characters, otherCharacters)
print(result) --> {
Frodo = {
team = "red",
score = 10
},
Bilbo = {
team = "yellow"
},
Boromir = {
score = {1, 2, 3}
}
}
]]
local Dash = script.Parent
local None = require(Dash.None)
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local forEach = require(Dash.forEach)
local forEachArgs = require(Dash.forEachArgs)
-- TODO Luau: Support typing varargs
-- TODO Luau: Support function generics
local function assign(target: Types.Table, ...): Types.Table
assertEqual(typeof(target), "table", [[Attempted to call Dash.assign with argument #1 of type {left:?} not {right:?}]])
-- Iterate through the varags in order
forEachArgs(function(input: Types.Table?)
-- Ignore items which are not defined
if input == nil or input == None then
return
end
-- Iterate through each key of the input and assign to target at the same key
forEach(input, function(value, key)
if value == None then
target[key] = nil
else
target[key] = value
end
end)
end, ...)
return target
end
return assign
@@ -0,0 +1,288 @@
--[[
Create a class called _name_ with the specified _constructor_. The constructor should return a
plain table which will be turned into an instance of _Class_ from a call to `Class.new(...)`.
@example
-- Create a simple Vehicle class
local Vehicle = class("Vehicle", function(wheelCount: number) return
{
speed = 0,
wheelCount = wheelCount
}
end)
function Vehicle:drive(speed)
self.speed = speed
end
-- Create a car instance
local car = Vehicle.new(4)
car.wheelCount --> 4
car.speed --> 0
-- Drive the car
car:drive(10)
car.speed --> 10
@usage When using Dash classes, private fields should be prefixed with `_` to avoid accidental access.
@usage A private field should only be accessed by a method of the class itself, though Rodash
does not restrict this in code.
@usage Public fields are recommended when there is no complex access logic e.g. `position.x`
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local function throwNotImplemented(tags: Types.Table)
local Error = require(Dash.Error)
local NotImplemented = Error.new("NotImplemented", [[The method "{methodName}" is not implemented on the class "{className}"]])
NotImplemented:throw(tags)
end
export type Constructor = () -> Types.Table
local function class(name: string, constructor: Constructor?)
constructor = constructor or function()
return {}
end
local Class = {
name = name
}
--[[
Return a new instance of the class, passing any arguments to the specified constructor.
@example
local Car = class("Car", function(speed)
return {
speed = speed
}
end)
local car = Car.new(5)
pretty(car) --> 'Car {speed = 5}'
]]
function Class.new(...)
local instance = constructor(...)
setmetatable(
instance,
{
__index = Class,
__tostring = Class.toString,
__eq = Class.equals,
__lt = Class.__lt,
__le = Class.__le,
__add = Class.__add,
__sub = Class.__sub,
__mul = Class.__mul,
__div = Class.__div,
__mod = Class.__mod
}
)
instance.Class = Class
instance:_init(...)
return instance
end
--[[
Run after the instance has been properly initialized, allowing methods on the instance to
be used.
@example
local Vehicle = dash.class("Vehicle", function(wheelCount) return
{
speed = 0,
wheelCount = wheelCount
}
end)
-- Let's define a static private function to generate a unique id for each vehicle.
function Vehicle._getNextId()
Vehicle._nextId = Vehicle._nextId + 1
return Vehicle._nextId
end
Vehicle._nextId = 0
-- A general purpose init function may call other helper methods
function Vehicle:_init()
self._id = self:_generateId()
end
-- Assign an id to the new instance
function Vehicle:_generateId()
return format("#{}: {} wheels", Vehicle._getNextId(), self.wheelCount)
end
-- Return the id if the instance is represented as a string
function Vehicle:toString()
return self._id
end
local car = Vehicle.new(4)
tostring(car) --> "#1: 4 wheels"
]]
function Class:_init()
end
--[[
Returns `true` if _value_ is an instance of _Class_ or any sub-class.
@example
local Vehicle = dash.class("Vehicle", function(wheelCount) return
{
speed = 0,
wheelCount = wheelCount
}
end)
local Car = Vehicle:extend("Vehicle", function()
return Vehicle.constructor(4)
end)
local car = Car.new()
car.isInstance(Car) --> true
car.isInstance(Vehicle) --> true
car.isInstance(Bike) --> false
]]
function Class.isInstance(value)
local ok, isInstance = pcall(function()
local metatable = getmetatable(value)
while metatable do
if metatable.__index == Class then
return true
end
metatable = getmetatable(metatable.__index)
end
return false
end)
return ok and isInstance
end
--[[
Create a subclass of _Class_ with a new _name_ that inherits the metatable of _Class_,
optionally overriding the _constructor_ and providing additional _decorators_.
The super-constructor can be accessed with `Class.constructor`.
Super methods can be accessed using `Class.methodName` and should be called with self.
@example
local Vehicle = dash.class("Vehicle", function(wheelCount) return
{
speed = 0,
wheelCount = wheelCount
}
end)
-- Let's define a static private function to generate a unique id for each vehicle.
function Vehicle._getNextId()
Vehicle._nextId = Vehicle._nextId + 1
return Vehicle._nextId
end
Vehicle._nextId = 0
-- A general purpose init function may call other helper methods
function Vehicle:_init()
self.id = self:_generateId()
end
-- Assign an id to the new instance
function Vehicle:_generateId()
return dash.format("#{}: {} wheels", Vehicle._getNextId(), self.wheelCount)
end
-- Let's make a Car class which has a special way to generate ids
local Car = Vehicle:extend("Vehicle", function()
return Vehicle.constructor(4)
end)
-- Uses the super method to generate a car-specific id
function Car:_generateId()
self.id = dash.format("Car {}", Vehicle._generateId(self))
end
local car = Car.new()
car.id --> "Car #1: 4 wheels"
]]
function Class:extend(name: string, constructor)
local SubClass = class(name, constructor or Class.new)
setmetatable(SubClass, {__index = self})
return SubClass
end
--[[
Return a string representation of the instance. By default this is the _name_ field (or the
Class name if this is not defined), but the method can be overridden.
@example
local Car = class("Car", function(name)
return {
name = name
}
end)
local car = Car.new()
car:toString() --> 'Car'
tostring(car) --> 'Car'
print("Hello " .. car) -->> Hello Car
local bob = Car.new("Bob")
bob:toString() --> 'Bob'
tostring(bob) --> 'Bob'
print("Hello " .. bob) -->> Hello Bob
@example
local NamedCar = class("NamedCar", function(name)
return {
name = name
}
end)
function NamedCar:toString()
return "Car called " .. self.name
end
local bob = NamedCar.new("Bob")
bob:toString() --> 'Car called Bob'
tostring(bob) --> 'Car called Bob'
print("Hello " .. bob) -->> Hello Car called Bob
]]
function Class:toString()
return self.name
end
--[[
Returns `true` if `self` is considered equal to _other_. This replaces the `==` operator
on instances of this class, and can be overridden to provide a custom implementation.
]]
function Class:equals(other)
return rawequal(self, other)
end
--[[
Returns `true` if `self` is considered less than _other_. This replaces the `<` operator
on instances of this class, and can be overridden to provide a custom implementation.
]]
function Class:__lt(other)
throwNotImplemented({
methodName = "__lt",
className = name
})
end
--[[
Returns `true` if `self` is considered less than or equal to _other_. This replaces the
`<=` operator on instances of this class, and can be overridden to provide a custom
implementation.
]]
function Class:__le(other)
throwNotImplemented({
methodName = "__le",
className = name
})
end
function Class:__add()
throwNotImplemented({
methodName = "__add",
className = name
})
end
function Class:__sub()
throwNotImplemented({
methodName = "__sub",
className = name
})
end
function Class:__mul()
throwNotImplemented({
methodName = "__mul",
className = name
})
end
function Class:__div()
throwNotImplemented({
methodName = "__div",
className = name
})
end
function Class:__mod()
throwNotImplemented({
methodName = "__mod",
className = name
})
end
return Class
end
return class
@@ -0,0 +1,28 @@
--[[
Collect returns a new Table derived from _input_ by iterating through its pairs and calling
the handler on each `(key, child)` tuple.
The handler should return a new `(newKey, value)` tuple to be inserted into the returned Table,
or `nil` if no value should be added.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local iterator = require(Dash.iterator)
-- TODO Luau: support generic function definitions
export type CollectHandler = () -> (any, any)
local function collect(input: Types.Table, handler: CollectHandler): Types.Map<any, any>
assertEqual(typeof(input), "table", [[Attempted to call Dash.collect with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(handler), "function", [[Attempted to call Dash.collect with argument #2 of type {left:?} not {right:?}]])
local result = {}
for key, child in iterator(input) do
local outputKey, outputValue = handler(key, child)
if outputKey ~= nil then
result[outputKey] = outputValue
end
end
return result
end
return collect
@@ -0,0 +1,28 @@
--[[
Collect returns a new Array derived from _input_ by iterating through its pairs and calling
the handler on each `(key, child)` tuple.
The handler should return a new value to be pushed onto the end of the result array, or `nil`
if no value should be added.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local iterator = require(Dash.iterator)
local insert = table.insert
-- TODO Luau: Support generic functions
local function collectArray(input: Types.Table, handler: Types.AnyFunction)
assertEqual(typeof(input), "table", [[Attempted to call Dash.collectArray with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(handler), "function", [[Attempted to call Dash.collectArray with argument #2 of type {left:?} not {right:?}]])
local result = {}
for key, child in iterator(input) do
local outputValue = handler(key, child)
if outputValue ~= nil then
insert(result, outputValue)
end
end
return result
end
return collectArray
@@ -0,0 +1,29 @@
--[[
Build a set from the entries of the _input_ Table, calling _handler_ on each entry and using
the returned value as an element to add to the set.
If _handler_ is not provided, values of `input` are used as elements.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local iterator = require(Dash.iterator)
-- TODO Luau: Support generic functions
local function collectSet(input: Types.Table, handler: Types.AnyFunction?)
assertEqual(typeof(input), "table", [[Attempted to call Dash.collectSet with argument #1 of type {left:?} not {right:?}]])
local result = {}
for key, child in iterator(input) do
local outputValue
if handler == nil then
outputValue = child
else
outputValue = handler(key, child)
end
if outputValue ~= nil then
result[outputValue] = true
end
end
return result
end
return collectSet
@@ -0,0 +1,36 @@
--[[
Returns a function that calls the argument functions in left-right order on an input, passing
the return of the previous function as argument(s) to the next.
@example
local function fry(item)
return "fried " .. item
end
local function cheesify(item)
return "cheesy " .. item
end
local prepare = compose(fry, cheesify)
prepare("nachos") --> "cheesy fried nachos"
]]
-- TODO Luau: Support generic functions
-- TODO Luau: Support varargs
--: <A>((...A -> ...A)[]) -> ...A -> A
local Dash = script.Parent
local identity = require(Dash.identity)
local function compose(...)
local fnCount = select("#", ...)
if fnCount == 0 then
return identity
end
local fns = {...}
return function(...)
local result = {fns[1](...)}
for i = 2, fnCount do
result = {fns[i](unpack(result))}
end
return unpack(result)
end
end
return compose
@@ -0,0 +1,13 @@
--[[
Returns a shallow copy of the _input_ Table.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assign = require(Dash.assign)
local assertEqual = require(Dash.assertEqual)
local function copy(input: Types.Table): Types.Table
assertEqual(typeof(input), "table", [[Attempted to call Dash.copy with argument #1 of type {left:?} not {right:?}]])
return assign({}, input)
end
return copy
@@ -0,0 +1,90 @@
--[[
Get information about the number of times references to the same table values appear in a data structure.
Operates on cyclic structures, and returns a Cycles object for a given _value_ by walking it recursively.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local includes = require(Dash.includes)
local join = require(Dash.join)
local keys = require(Dash.keys)
local sort = table.sort
export type Cycles = {
-- A set of tables which were visited recursively
visited: Types.Set<Types.Table>,
-- A map from table to unique index in visit order
refs: Types.Map<Types.Table, number>,
-- The number to use for the next unique table visited
nextRef: number,
-- An array of keys which should not be visited
omit: Types.Array<any>,
}
local function getDefaultCycles(): Cycles
return {
visited = {},
refs = {},
nextRef = 1,
omit = {},
}
end
-- TODO Luau: Improve type inference to a point that this definition does not produce so many type errors
-- TYPED: local function cycles(value: any, depth: number?, initialCycles: Cycles?): Cycles
local function cycles(input: any, depth: number?, initialCycles: any): Cycles?
if depth == -1 then
return initialCycles
end
if typeof(input) == "table" then
local childCycles = initialCycles or getDefaultCycles()
if childCycles.visited[input] then
-- We have already visited the table, so check if it has a reference
if not childCycles.refs[input] then
-- If not, create one as it is present at least twice
childCycles.refs[input] = childCycles.nextRef
childCycles.nextRef += 1
end
return nil
else
-- We haven't yet visited the table, so recurse
childCycles.visited[input] = true
-- Visit in order to preserve reference consistency
local inputKeys = keys(input)
sort(inputKeys, function(left, right)
if typeof(left) == "number" and typeof(right) == "number" then
return left < right
else
return tostring(left) < tostring(right)
end
end)
for _, key in ipairs(inputKeys) do
local value = input[key]
if includes(childCycles.omit, key) then
-- Don't visit omitted keys
continue
end
-- TODO Luau: support type narrowring with "and"
-- TYPED: cycles(key, depth and depth - 1 or nil, childCycles)
-- TYPED: cycles(value, depth and depth - 1 or nil, childCycles)
-- Recurse through both the keys and values of the table
if depth then
cycles(key, depth - 1, childCycles)
cycles(value, depth - 1, childCycles)
else
cycles(key, nil, childCycles)
cycles(value, nil, childCycles)
end
end
end
return childCycles
else
-- Non-tables do not have cycles
return nil
end
end
return cycles
@@ -0,0 +1,15 @@
--[[
Checks if _input_ ends with the string _suffix_.
@example endsWith("Fun Roblox Games", "Games") --> true
@example endsWith("Bad Roblox Memes", "Games") --> false
]]
local Dash = script.Parent
local assertEqual = require(Dash.assertEqual)
local function endsWith(input: string, suffix: string)
assertEqual(typeof(input), "string", [[Attempted to call Dash.endsWith with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(suffix), "string", [[Attempted to call Dash.endsWith with argument #2 of type {left:?} not {right:?}]])
return input:sub(-suffix:len()) == suffix
end
return endsWith
@@ -0,0 +1,27 @@
--[[
Filter the _input_ Table by calling the handler on each `(child, index)` tuple.
For an Array input, the order of elements is prevered in the output.
The handler should return truthy to preserve the value in the resulting Table.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local iterator = require(Dash.iterator)
-- TODO Luau: support generic function definitions
export type FilterHandler = (any, any) -> boolean
local function filter(input: Types.Table, handler: FilterHandler): Types.Array<any>
assertEqual(typeof(input), "table", [[Attempted to call Dash.filter with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(handler), "function", [[Attempted to call Dash.filter with argument #2 of type {left:?} not {right:?}]])
local result = {}
for index, child in iterator(input) do
if handler(child, index) then
table.insert(result, child)
end
end
return result
end
return filter
@@ -0,0 +1,28 @@
--[[
Returns an element in the _input_ Table that the handler returns `true` for, when passed the
`(child, key)` entry.
Returns nil if no entires satisfy the condition.
For an Array, this first matching element is returned.
For a Map, an arbitrary matching element is returned if it exists.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local iterator = require(Dash.iterator)
-- TODO Luau: support generic function definitions
export type FindHandler = (any, any) -> boolean
local function find(input: Types.Table, handler: FindHandler)
assertEqual(typeof(input), "table", [[Attempted to call Dash.find with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(handler), "function", [[Attempted to call Dash.find with argument #2 of type {left:?} not {right:?}]])
for key, child in iterator(input) do
if handler(child, key) then
return child
end
end
end
return find
@@ -0,0 +1,23 @@
--[[
Returns the index of the first element in the _input_ Array that the handler returns `true` for,
when passed the `(child, key)` entry.
Returns nil if no entires satisfy the condition.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
-- TODO Luau: support generic function definitions
export type FindHandler = (any, any) -> boolean
local function findIndex(input: Types.Array<any>, handler: FindHandler)
assertEqual(typeof(input), "table", [[Attempted to call Dash.findIndex with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(handler), "function", [[Attempted to call Dash.findIndex with argument #2 of type {left:?} not {right:?}]])
for key, child in ipairs(input) do
if handler(child, key) then
return key
end
end
end
return findIndex
@@ -0,0 +1,22 @@
--[[
Flattens the input array by a single level.
Outputs a new Array of elements merged from the _input_ Array arguments in left-to-right order.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local append = require(Dash.append)
local assertEqual = require(Dash.assertEqual)
local forEach = require(Dash.forEach)
-- TODO Luau: Support function generics
local function flat(input: Types.Array<Types.Array<any>>): Types.Array<any>
assertEqual(typeof(input), "table", [[Attempted to call Dash.flat with argument #1 of type {left:?} not {right:?}]])
local result = {}
forEach(input, function(childArray: Types.Array<any>)
append(result, childArray)
end)
return result
end
return flat
@@ -0,0 +1,25 @@
--[[
Iterates through the elements of the _input_ Table.
If the table is an Array, it iterates in order 1..n.
If the table is a Map, the keys are visited in an arbitrary order.
Calls the _handler_ for each entry.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local iterator = require(Dash.iterator)
export type ForEachHandler<Value> = (Value, number) -> ()
-- TODO Luau: Support function generics
--local function forEach<Value>(input: Types.Array<Value>, handler: ForEachHandler<Value>)
local function forEach(input: Types.Table, handler: Types.AnyFunction)
assertEqual(typeof(input), "table", [[Attempted to call Dash.forEach with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(handler), "function", [[Attempted to call Dash.forEach with argument #2 of type {left:?} not {right:?}]])
for key, value in iterator(input) do
handler(value, key)
end
end
return forEach
@@ -0,0 +1,20 @@
--[[
Iterates through the tail arguments in order, including nil values up to the argument list length.
Calls the _handler_ for each entry.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
export type ForEachArgsHandler<Value> = (Value, number) -> ()
-- TODO Luau: Support function generics
-- TODO Luau: Support vararg types
--local function forEachArgs<Value>(handler: ForEachArgsHandler<Value>, ...: Types.Args<Value>)
local function forEachArgs(handler: Types.AnyFunction, ...)
assertEqual(typeof(handler), "function", [[Attempted to call Dash.forEachArgs with argument #1 of type {left:?} not {right:?}]])
for index = 1, select('#', ...) do
handler(select(index, ...), index)
end
end
return forEachArgs
@@ -0,0 +1,88 @@
--[[
Returns the _format_ string with placeholders `{...}` substituted with readable representations
of the subsequent arguments.
This function is a simpler & more powerful version of `string.format`, inspired by `format!`
in Rust.
* `{}` formats and prints the next argument using `:format()` if available, or a suitable
default representation depending on its type.
* `{blah}` formats and prints the key "blah" of the 1st argument
* `{2}` formats and prints the 2nd argument.
* `{#2}` prints the length of the 2nd argument.
Display parameters can be combined after a `:` in the curly braces. Any format parameters used
in `string.format` can be used here, along with these extras:
* `{:?}` formats any value using `pretty`.
* `{:#?}` formats any value using multiline `pretty`.
@example
local props = {"teeth", "claws", "whiskers", "tail"}
format("{:?} is in {:#?}", "whiskers", props)
-> '"whiskers" is in {"teeth", "claws", "whiskers", "tail"}'
@example
format("The time is {:02}:{:02}", 2, 4) -> "The time is 02:04"
@example
format("The color blue is #{:06X}", 255) -> "The color blue is #0000FF"
@usage Escape `{` with `{{` and `}` similarly with `}}`.
@usage See [https://developer.roblox.com/articles/Format-String](https://developer.roblox.com/articles/Format-String)
for complete list of formating options and further use cases.
]]
local Dash = script.Parent
local assertEqual = require(Dash.assertEqual)
local formatValue = require(Dash.formatValue)
local splitOn = require(Dash.splitOn)
local startsWith = require(Dash.startsWith)
local concat = table.concat
local insert = table.insert
local function format(formatString: string, ...)
assertEqual(typeof(formatString), "string", [[Attempted to call Dash.format with argument #1 of type {left:?} not {right:?}]])
local args = {...}
local argIndex = 1
local texts, subs = splitOn(formatString, "{[^{}]*}")
local result = {}
-- Iterate through possible curly-brace matches, ignoring escaped and substituting valid ones
for i, text in pairs(texts) do
local unescaped = text:gsub("{{", "{"):gsub("}}", "}")
insert(result, unescaped)
local placeholder = subs[i] and subs[i]:sub(2, -2)
if placeholder then
-- Ensure that the curly braces have not been escaped
local escapeMatch = text:gmatch("{+$")()
local isEscaped = escapeMatch and #escapeMatch % 2 == 1
if not isEscaped then
-- Split the placeholder into left & right parts pivoting on the central ":"
local placeholderSplit = splitOn(placeholder, ":")
local isLength = startsWith(placeholderSplit[1], "#")
local argString = isLength and placeholderSplit[1]:sub(2) or placeholderSplit[1]
local nextIndex = tonumber(argString)
local displayString = placeholderSplit[2]
local arg = "nil"
if nextIndex then
-- Return the next argument
arg = args[nextIndex]
elseif argString:len() > 0 then
-- Print a child key of the 1st argument
local argChild = args[1] and args[1][argString]
if argChild ~= nil then
arg = argChild
end
else
arg = args[argIndex]
argIndex = argIndex + 1
end
if isLength then
arg = #arg
end
-- Format the selected value
insert(result, formatValue(arg, displayString or ""))
else
local unescapedSub = placeholder
insert(result, unescapedSub)
end
end
end
return concat(result, "")
end
return format
@@ -0,0 +1,43 @@
--[[
Format a specific _value_ using the specified _displayString_.
@example
formatValue(255, "06X") --> "0000FF"
@example
formatValue(255.5) --> "255.5"
@see `format` - for a full description of valid display strings.
]]
local Dash = script.Parent
local assertEqual = require(Dash.assertEqual)
local function formatValue(value: any, displayString: string): string
displayString = displayString or ""
assertEqual(typeof(displayString), "string", [[Attempted to call Dash.formatValue with argument #2 of type {left:?} not {right:?}]])
-- Inline require to prevent infinite require cycle
local displayTypeStart, displayTypeEnd = displayString:find("[A-Za-z#?]+")
if displayTypeStart then
local displayType = displayString:sub(displayTypeStart, displayTypeEnd)
local formatAsString =
"%" .. displayString:sub(1, displayTypeStart - 1) .. displayString:sub(displayTypeEnd + 1) .. "s"
-- Pretty print values
local pretty = require(Dash.pretty)
if displayType == "#?" then
-- Multiline print a value
return formatAsString:format(pretty(value, {multiline = true}))
elseif displayType == "?" then
-- Inspect a value
return formatAsString:format(pretty(value))
end
return ("%" .. displayString):format(value)
else
local displayType = "s"
if type(value) == "number" then
-- Correctly display floats or integers
local _, fraction = math.modf(value)
displayType = fraction == 0 and "d" or "f"
end
return ("%" .. displayString .. displayType):format(tostring(value))
end
end
return formatValue
@@ -0,0 +1,77 @@
--[[
Returns a new read-only view of _object_ which prevents any values from being changed.
@param name The name of the object for improved error message readability.
@param throwIfMissing If `true` then access to a missing key will also throw.
@note
Unfortunately you cannot iterate using `pairs` or `ipairs` on frozen objects because Luau
doesn't support defining these custom iterators in metatables.
@example
local drink = freeze("Ice Cream", {
flavor = "mint",
topping = "sprinkles"
}, true)
print(drink.flavor) --> "mint"
drink.flavor = "vanilla"
--!> ReadonlyKey: Attempt to write to readonly key "flavor" (a string) of frozen object "Ice Cream"`
print(drink.syrup) --> nil
--!> `MissingKey: Attempt to read missing key "syrup" (a string) of frozen object "Ice Cream"`
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local Error = require(Dash.Error)
local assertEqual = require(Dash.assertEqual)
local format = require(Dash.format)
-- TODO Luau: Improve type inference to make these not need to be any
local ReadonlyKey: any = Error.new("ReadonlyKey", "Attempted to write to readonly key {key:?} of frozen object {objectName:?}")
local MissingKey: any = Error.new("MissingKey", "Attempted to read missing key {key:?} of frozen object {objectName:?}")
-- TODO Luau: Support generic functions
-- TODO Luau: Support generic extends syntax
-- TYPED: local function freeze<T extends Types.Table>(objectName: string, object: T, throwIfMissing: boolean?): T
local function freeze(objectName: string, object: Types.Table, throwIfMissing: boolean?)
assertEqual(typeof(objectName), "string", [[Attempted to call Dash.freeze with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(object), "table", [[Attempted to call Dash.freeze with argument #2 of type {left:?} not {right:?}]])
-- We create a proxy so that the underlying object is not affected
local proxy = {}
setmetatable(
proxy,
{
__index = function(_, key: any)
local value = object[key]
if value == nil and throwIfMissing then
-- Tried to read a key which isn't present in the underlying object
MissingKey:throw({
key = key,
objectName = objectName
})
end
return value
end,
__newindex = function(_, key: any)
-- Tried to write to any key
ReadonlyKey:throw({
key = key,
objectName = objectName
})
end,
__len = function()
return #object
end,
__tostring = function()
return format("Frozen({})", objectName)
end,
__call = function(_, ...)
-- TODO Luau: Gated check for if a function has a __call value
local callable: any = object
return callable(...)
end
}
)
return proxy
end
return freeze
@@ -0,0 +1,25 @@
--[[
Returns a value of the _input_ Table at the _key_ provided.
If the key is missing, the value is acquired from the _getValue_ handler,
added to the _input_ Table and returned.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local Error = require(Dash.Error)
local assertEqual = require(Dash.assertEqual)
local format = require(Dash.format)
-- TODO Luau: Support generic functions
export type GetValueHandler = (any) -> any
local function getOrSet(input: Types.Table, key: any, getValue: GetValueHandler)
assertEqual(typeof(input), "table", [[Attempted to call Dash.getOrSet with argument #1 of type {left:?} not {right:?}]])
assertEqual(key == nil, false, [[Attempted to call Dash.getOrSet with a nil key argument]])
assertEqual(typeof(getValue), "function", [[Attempted to call Dash.getOrSet with argument #3 of type {left:?} not {right:?}]])
if input[key] == nil then
input[key] = getValue(input, key)
end
return input[key]
end
return getOrSet
@@ -0,0 +1,39 @@
--[[
Groups values in the _input_ Table by their _getKey_ value.
Each value of the result Table is an Array of values from the _input_ Table which were assigned
the corresponding key.
If _getKey_ is a function, it is called with each `(child, key)` entry and uses the return
value as the corresponding key to insert at in the result Table. Otherwise, the _getKey_ value
is used directly as the key itself.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local insert = table.insert
-- TODO Luau: Support generic functions
local function groupBy(input: Dash.Table, getKey: any)
assertEqual(typeof(input), "table", [[Attempted to call Dash.groupBy with argument #1 of type {left:?} not {right:?}]])
assertEqual(getKey == nil, false, [[Attempted to call Dash.groupBy with a nil getKey argument]])
local result = {}
for key, child in pairs(input) do
local groupKey
if typeof(getKey) == "function" then
groupKey = getKey(child, key)
else
groupKey = child[getKey]
end
if groupKey ~= nil then
if result[groupKey] ~= nil then
insert(result[groupKey], child)
else
result[groupKey] = {child}
end
end
end
return result
end
return groupBy
@@ -0,0 +1,11 @@
--[[
The identity function, which simply returns its input parameters.
Can be used to make it clear that a handler returns its inputs.
]]
-- TODO Luau: Support typing generic return tuple
local function identity(...)
return ...
end
return identity
@@ -0,0 +1,23 @@
--[[
Returns `true` if the _item_ exists as a value in the _input_ table.
A nil _item_ will always return `false`.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
-- TODO Luau: Support generic functions
local function includes(input: Types.Table, item: any): boolean
assertEqual(typeof(input), "table", [[Attempted to call Dash.includes with argument #1 of type {left:?} not {right:?}]])
if item == nil then
return false
end
for _, child in pairs(input) do
if child == item then
return true
end
end
return false
end
return includes
@@ -0,0 +1,8 @@
local Dash = {}
-- Require and add the Dash functions to the Dash table
for _, fn in pairs(script:GetChildren()) do
Dash[fn.Name] = require(fn)
end
return Dash.freeze("Dash", Dash, true)
@@ -0,0 +1,9 @@
--[[
Returns `true` if the value can be called i.e. you can write `value(...)`.
]]
local function isCallable(value: any): boolean
return type(value) == "function" or
(type(value) == "table" and getmetatable(value) and getmetatable(value).__call ~= nil) or false
end
return isCallable
@@ -0,0 +1,19 @@
--[[
Returns `true` if the first character of _input_ is a lower-case character.
Throws if the _input_ is not a string or it is the empty string.
Our current version of Lua unfortunately does not support upper or lower-case detection outside
the english alphabet. This function has been implemented to return the expected result once
this has been corrected.
]]
local Dash = script.Parent
local assertEqual = require(Dash.assertEqual)
local function isLowercase(input: string)
assertEqual(typeof(input), "string", [[Attempted to call Dash.isLowercase with argument #1 of type {left:?} not {right:?}]])
assertEqual(#input > 0, true, [[Attempted to call Dash.isLowercase with an empty string]])
local firstLetter = input:sub(1, 1)
return firstLetter == firstLetter:lower()
end
return isLowercase
@@ -0,0 +1,19 @@
--[[
Returns `true` if the first character of _input_ is an upper-case character.
Throws if the _input_ is not a string or it is the empty string.
Our current version of Lua unfortunately does not support upper or lower-case detection outside
the english alphabet. This function has been implemented to return the expected result once
this has been corrected.
]]
local Dash = script.Parent
local assertEqual = require(Dash.assertEqual)
local function isUppercase(input: string)
assertEqual(typeof(input), "string", [[Attempted to call Dash.isUppercase with argument #1 of type {left:?} not {right:?}]])
assertEqual(#input > 0, true, [[Attempted to call Dash.isUppercase with an empty string]])
local firstLetter = input:sub(1, 1)
return firstLetter == firstLetter:upper()
end
return isUppercase
@@ -0,0 +1,43 @@
--[[
Creates a stateful iterator for the _input_ Table, first visting ordered numeric keys 1..n
and then the remaining unordered keys in any order.
@see Dash.iterator - for an iterator that can iterate over an iterable.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local function iterable(input: Types.Table): Types.AnyFunction
local currentIndex = 1
local inOrderedKeys = true
local currentKey
local iterateFn
iterateFn = function()
if inOrderedKeys then
local value = input[currentIndex]
if value == nil then
inOrderedKeys = false
else
local index = currentIndex
currentIndex += 1
return index, value
end
end
while true do
currentKey = next(input, currentKey)
-- Don't re-visit ordered keys 1..n
if typeof(currentKey) == "number" and currentKey > 0 and currentKey < currentIndex and currentKey % 1 == 0 then
continue
end
if currentKey == nil then
return nil
else
return currentKey, input[currentKey]
end
end
end
return iterateFn
end
return iterable
@@ -0,0 +1,29 @@
--[[
Iterates using a `pairs` iterator for an _input_ Table if zero length, otherwise an `ipairs`
iterator for an Array.
If _input_ is a function it is used as a stateful iterator instead.
This function can be used to build behaviour that iterates over both Arrays and Maps.
@see Dash.iterable if you want to iterate over a Table with numeric but un-ordered keys.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local function iterator(input: Types.Table | Types.AnyFunction): Types.AnyFunction
if typeof(input) == "function" then
return input
elseif typeof(input) == "table" then
if #input > 0 then
return ipairs(input)
else
return pairs(input)
end
else
return nil
end
end
return iterator
@@ -0,0 +1,18 @@
--[[
Output a new Map from merging all the keys in the Map arguments in left-to-right order.
The None symbol can be used to remove existing elements.
@param ... any number of tables
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assign = require(Dash.assign)
-- TODO Luau: Support typing varargs
-- TODO Luau: Support function generics
local function join(...): Types.Map<any, any>
return assign({}, ...)
end
return join
@@ -0,0 +1,41 @@
--[[
Creates a shallow clone of the _source_ Map, and copies the values from the _delta_ Map
by key, like the join utility.
However, if any of the values are tables themselves, the joinDeep function is called
recursively to produce a new table at the specified key.
The purpose of this function is to merge nested immutable data using as few table
creation operations as possible, making it appropriate for updating state in a reducer.
The None symbol can be used to remove an existing value.
]]
local Dash = script.Parent
local None = require(Dash.None)
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local forEach = require(Dash.forEach)
local copy = require(Dash.copy)
-- TODO Luau: Support typing varargs
-- TODO Luau: Support function generics
local function joinDeep(source: Types.Table, delta: Types.Table): Types.Table
assertEqual(typeof(source), "table", [[Attempted to call Dash.joinDeep with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(delta), "table", [[Attempted to call Dash.joinDeep with argument #2 of type {left:?} not {right:?}]])
local result = copy(source)
-- Iterate through each key of the input and assign to target at the same key
forEach(delta, function(value, key)
if typeof(source[key]) == "table" and typeof(value) == "table" then
-- Only merge tables
result[key] = joinDeep(source[key], value)
elseif value == None then
-- Remove none values
result[key] = nil
else
result[key] = value
end
end)
return result
end
return joinDeep
@@ -0,0 +1,31 @@
--[[
Assigns values in the _input_ Table by their _getKey_ value.
If _getKey_ is a function, it is called with each `(child, key)` entry and uses the return
value as the corresponding key to assign to in the result Table. Otherwise, the _getKey_ value
is used directly as the key itself.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local collect = require(Dash.collect)
local insert = table.insert
-- TODO Luau: Support generic functions
local function keyBy(input: Types.Table, getKey: any): Types.Table
assertEqual(typeof(input), "table", [[Attempted to call Dash.keyBy with argument #1 of type {left:?} not {right:?}]])
assertEqual(getKey == nil, false, [[Attempted to call Dash.keyBy with a nil getKey argument]])
return collect(input, function(key, child)
local newKey
if typeof(getKey) == "function" then
newKey = getKey(child, key)
else
newKey = child[getKey]
end
return newKey, child
end)
end
return keyBy
@@ -0,0 +1,24 @@
--[[
Returns an Array of the keys in the _input_ Table.
If the input is an Array, ordering is preserved.
If the input is a Map, elements are returned in an arbitrary order.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local iterator = require(Dash.iterator)
local insert = table.insert
-- TODO Luau: Support generic functions
local function keys(input: Types.Table): Types.Array<any>
assertEqual(typeof(input), "table", [[Attempted to call Dash.keys with argument #1 of type {left:?} not {right:?}]])
local result = {}
for key, _ in iterator(input) do
insert(result, key)
end
return result
end
return keys
@@ -0,0 +1,29 @@
--[[
Returns the last element in the _input_ Array that the handler returns `true` for, when
passed the `(child, index)` entry.
Returns nil if no entires satisfy the condition.
If handler is not defined, the function simply returns the last element of the Array.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
-- TODO Luau: support generic function definitions
export type FindHandler = (any, any) -> boolean
local function last(input: Types.Array<any>, handler: FindHandler?)
assertEqual(typeof(input), "table", [[Attempted to call Dash.last with argument #1 of type {left:?} not {right:?}]])
for index = #input, 1, -1 do
local child = input[index]
if not handler then
return child
end
if handler(child, index) then
return child
end
end
end
return last
@@ -0,0 +1,24 @@
--[[
Makes a string of `length` from `input` by repeating characters from `prefix` at the start of the string.
@example leftPad("toast", 6) --> " toast"
@example leftPad("2", 2, "0") --> "02"
@example leftPad("toast", 10, ":)") --> ":):):toast"
@param prefix (default = `" "`)
]]
local Dash = script.Parent
local assertEqual = require(Dash.assertEqual)
local function leftPad(input: string, length: number, prefix: string?): string
assertEqual(typeof(input), "string", [[Attempted to call Dash.leftPad with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(length), "number", [[Attempted to call Dash.leftPad with argument #2 of type {left:?} not {right:?}]])
local definedPrefix = prefix or " "
assertEqual(typeof(definedPrefix), "string", [[Attempted to call Dash.leftPad with argument #3 of type {left:?} not {right:?}]])
local padLength = length - input:len()
local remainder = padLength % definedPrefix:len()
local repetitions = (padLength - remainder) / definedPrefix:len()
return string.rep(definedPrefix or " ", repetitions) .. definedPrefix:sub(1, remainder) .. input
end
return leftPad
@@ -0,0 +1,38 @@
--[[
Iterates through the elements of the _input_ Table.
For an Array input, the elements are visted in order 1..n.
For a Map input, the elements are visited in an arbitrary order.
Calls the _handler_ for each entry and constructs a new Table using the same keys but replacing
the values with new ones returned from the handler.
Values returned by _handler_ must be defined.
@see Dash.collectArray if you want to return nil values.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local iterator = require(Dash.iterator)
-- TODO Luau: Support generic functions
export type MapHandler = (any, number) -> any
-- TYPED: export type MapHandler<Input, Output> = (Value, number) -> Output
-- TYPED: local function map<Input, Output>(input: Types.Array<Input>, fn: MapFn<Input, Output>)<Output>
local function map(input: Types.Array<any>, handler: MapHandler): Types.Array<any>
assertEqual(typeof(input), "table", [[Attempted to call Dash.map with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(handler), "function", [[Attempted to call Dash.map with argument #2 of type {left:?} not {right:?}]])
local result = {}
for key, child in iterator(input) do
local value = handler(child, key)
assertEqual(value == nil, false, [[Returned nil from a Dash.map handler]])
result[key] = value
end
return result
end
return map
@@ -0,0 +1,23 @@
--[[
Iterates through the elements of the _input_ Array in order 1..n.
Calls the _handler_ for each entry and returns the first non-nil value returned by the handler.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
-- TODO Luau: Support generic functions
export type MapHandler = (any, number) -> any
local function mapFirst(input: Types.Array<any>, handler: MapHandler)
assertEqual(typeof(input), "table", [[Attempted to call Dash.mapFirst with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(handler), "function", [[Attempted to call Dash.mapFirst with argument #2 of type {left:?} not {right:?}]])
for index, child in ipairs(input) do
local output = handler(child, index)
if output ~= nil then
return output
end
end
end
return mapFirst
@@ -0,0 +1,24 @@
--[[
Iterates through the elements of the _input_ Array in reverse in order n..1.
Calls the _handler_ for each entry and returns the first non-nil value returned by the handler.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
-- TODO Luau: Support generic functions
export type MapHandler = (any, number) -> any
local function mapLast(input: Types.Array<any>, handler: MapHandler)
assertEqual(typeof(input), "table", [[Attempted to call Dash.mapLast with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(handler), "function", [[Attempted to call Dash.mapLast with argument #2 of type {left:?} not {right:?}]])
for key = #input, 1, -1 do
local child = input[key]
local output = handler(child, key)
if output ~= nil then
return output
end
end
end
return mapLast
@@ -0,0 +1,29 @@
--[[
Iterates through the elements of the _input_ Table in no particular order.
Calls the _handler_ for each entry and returns the first non-nil value returned by the handler.
If _handler_ is nil, the first value visited is returned.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
-- TODO Luau: Support generic functions
export type MapHandler = (any, number) -> any
local function mapOne(input: Types.Table, handler: MapHandler?)
assertEqual(typeof(input), "table", [[Attempted to call Dash.mapOne with argument #1 of type {left:?} not {right:?}]])
for key, child in pairs(input) do
local output
if handler then
output = handler(child, key)
else
output = child
end
if output ~= nil then
return output
end
end
end
return mapOne
@@ -0,0 +1,10 @@
--[[
A function which does nothing.
Can be used to make it clear that a handler has no function.
]]
local function noop()
end
return noop
@@ -0,0 +1,30 @@
--[[
Return a new Table made from entries in the _input_ Table whose key is not in the _keys_ Array.
If the input is an Array, ordering is preserved.
If the input is a Map, elements are returned in an arbitrary order.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local collectSet = require(Dash.collectSet)
local forEach = require(Dash.forEach)
-- TODO Luau: Support generic functions, then substitute type signature
-- TYPED: local function omit<Key, Value>(input: Types.Map<Key, Value>, keys: Types.Array<Key>): Value
local function omit(input: Types.Table, keys: Types.Array<any>): Types.Table
assertEqual(typeof(input), "table", [[Attempted to call Dash.omit with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(keys), "table", [[Attempted to call Dash.omit with argument #2 of type {left:?} not {right:?}]])
local output = {}
local keySet = collectSet(keys)
-- TYPED: forEach(input, function(child: Value, key: Key)
forEach(input, function(child, key)
if not keySet[key] then
output[key] = input[key]
end
end)
return output
end
return omit
@@ -0,0 +1,26 @@
--[[
Pick entries in the _input_ Table which should remain in the output by calling the handler on
each `(child, index)` tuple.
The handler should return truthy to preserve the value in the resulting Table.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local iterator = require(Dash.iterator)
-- TODO Luau: support generic function definitions
export type PickHandler = (any, any) -> boolean
local function pick(input: Types.Map<any, any>, handler: PickHandler): Types.Map<any, any>
assertEqual(typeof(input), "table", [[Attempted to call Dash.pick with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(handler), "function", [[Attempted to call Dash.pick with argument #2 of type {left:?} not {right:?}]])
local result = {}
for key, child in iterator(input) do
if handler(child, key) then
result[key] = child
end
end
return result
end
return pick
@@ -0,0 +1,189 @@
--[[
Return a pretty string serialization of _object_.
This implementation deals with cycles in tables and can neatly display metatables.
Optionally use an indented multiline string, limit the depth of tables, omit or pick keys.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local append = require(Dash.append)
local assign = require(Dash.assign)
local cycles = require(Dash.cycles)
local includes = require(Dash.includes)
local join = require(Dash.join)
local map = require(Dash.map)
local keys = require(Dash.keys)
local slice = require(Dash.slice)
local concat = table.concat
local insert = table.insert
local sort = table.sort
export type PrettyOptions = {
-- The maximum depth of ancestors of a table to display (default = 2)
depth: number?,
-- An array of keys which should not be visited
omit: Types.Array<any>?,
-- Whether to use multiple lines (default = false)
multiline: boolean?,
-- Whether to show the length of any array in front of its content
arrayLength: boolean?,
-- The maximum length of a line (default = 80)
maxLineLength: number?,
-- Whether to drop the quotation marks around strings. By default, this is true for table keys
noQuotes: boolean?,
-- The indent string to use (default = "\t")
indent: string?,
-- A set of tables which have already been visited and should be referred to by reference
visited: Types.Set<Types.Table>?,
-- A cycles object returned from `cycles` to aid reference display
cycles: cycles.Cycles?,
}
local function indentLines(lines: Types.Array<string>, indent: string)
return map(lines, function(line: string)
return indent .. line
end)
end
local pretty
-- TODO Luau: Improve type inference to a point that this definition does not produce so many type errors
-- local function prettyLines(object: any, options: PrettyOptions?): Types.Array<string>
local function prettyLines(object: any, options: any): Types.Array<string>
options = options or {}
if type(object) == "table" then
-- A table needs to be serialized recusively
-- Construct the options for recursive calls for the table values
local valueOptions = assign({
visited = {},
indent = "\t",
depth = 2
}, options, {
-- Depth is reduced until we shouldn't recurse any more
depth = options.depth and options.depth - 1 or nil,
cycles = options.cycles or cycles(object, options.depth, {
visited = {},
refs = {},
nextRef = 0,
depth = options.depth,
omit = options.omit or {}
})
})
if valueOptions.depth == -1 then
-- Indicate there is more information available beneath the maximum depth
return {"..."}
end
if valueOptions.visited[object] then
-- Indicate this table has been printed already, so print a ref number instead of
-- printing it multiple times
return {"&" .. valueOptions.cycles.refs[object]}
end
valueOptions.visited[object] = true
local multiline = valueOptions.multiline
local comma = multiline and "," or ", "
-- If the table appears multiple times in the output, mark it with a ref prefix so it can
-- be identified if it crops up later on
local ref = valueOptions.cycles.refs[object]
local refTag = ref and ("<%s>"):format(ref) or ""
local lines = {refTag .. "{"}
-- Build the options for the recursive call for the table keys
local keyOptions = join(valueOptions, {
noQuotes = true,
multiline = false
})
-- Compact numeric keys into a simpler array style
local maxConsecutiveIndex = 0
local first = true
for index, value in ipairs(object) do
if valueOptions.omit and includes(valueOptions.omit, index) then
-- Don't include keys which are omitted
continue
end
if first then
first = false
else
lines[#lines] = lines[#lines] .. comma
end
if valueOptions.multiline then
local indendedValue = indentLines(prettyLines(value, valueOptions), valueOptions.indent)
append(lines, indendedValue)
else
lines[#lines] = lines[#lines] .. pretty(value, valueOptions)
end
maxConsecutiveIndex = index
end
if #object > 0 and valueOptions.arrayLength then
lines[1] = ("#%d %s"):format(#object, lines[1])
end
-- Ensure keys are printed in order to guarantee consistency
local objectKeys = keys(object)
sort(objectKeys, function(left, right)
if typeof(left) == "number" and typeof(right) == "number" then
return left < right
else
return tostring(left) < tostring(right)
end
end)
for _, key in ipairs(objectKeys) do
local value = object[key]
-- We printed a key if it's an index e.g. an integer in the range 1..n.
if typeof(key) == "number" and key % 1 == 0 and key >= 1 and key <= maxConsecutiveIndex then
continue
end
if valueOptions.omit and includes(valueOptions.omit, key) then
-- Don't include keys which are omitted
continue
end
if first then
first = false
else
lines[#lines] = lines[#lines] .. comma
end
if valueOptions.multiline then
local keyLines = prettyLines(key, keyOptions)
local indentedKey = indentLines(keyLines, valueOptions.indent)
local valueLines = prettyLines(value, valueOptions)
local valueTail = slice(valueLines, 2)
local indendedValueTail = indentLines(valueTail, valueOptions.indent)
-- The last line of the key and first line of the value are concatenated together
indentedKey[#indentedKey] = ("%s = %s"):format(indentedKey[#indentedKey], valueLines[1])
append(lines, indentedKey)
append(lines, indendedValueTail)
else
lines[#lines] = ("%s%s = %s"):format(lines[#lines], pretty(key, keyOptions), pretty(value, valueOptions))
end
end
if valueOptions.multiline then
if first then
-- An empty table is just represented as {}
lines[#lines] = lines[#lines] .. "}"
else
insert(lines, "}")
end
else
lines[#lines] = ("%s}"):format(lines[#lines])
end
return lines
elseif type(object) == "string" and not options.noQuotes then
return {('"%s"'):format(object)}
else
return {tostring(object)}
end
end
-- TODO Luau: Improve type inference to a point that this definition does not produce so many type errors
-- pretty = function(object: any, options: PrettyOptions?): string
pretty = function(object: any, options: any): string
return concat(prettyLines(object, options), "\n")
end
return pretty
@@ -0,0 +1,26 @@
--[[
Iterate through the elements of the _input_ Array in order 1..n.
Call the _handler_ for each element, passing the return of the previous call as the first argument.
The _initial_ value is passed into the first call, and the final value returned by the function.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
-- TODO Luau: Support generic functions
export type ReduceHandler = (any, any, any) -> any
local function reduce(input: Types.Array<any>, handler: ReduceHandler, initial: any)
assertEqual(typeof(input), "table", [[Attempted to call Dash.reduce with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(handler), "function", [[Attempted to call Dash.reduce with argument #2 of type {left:?} not {right:?}]])
local result = initial
for i = 1, #input do
result = handler(result, input[i], i)
end
return result
end
return reduce
@@ -0,0 +1,19 @@
--[[
Reverse the order of the elements in the _input_ Array.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local insert = table.insert
local function reverse(input: Types.Array<any>): Types.Array<any>
assertEqual(typeof(input), "table", [[Attempted to call Dash.reverse with argument #1 of type {left:?} not {right:?}]])
local output = {}
for i = #input, 1, -1 do
insert(output, input[i])
end
return output
end
return reverse
@@ -0,0 +1,26 @@
--[[
Makes a string of `length` from `input` by repeating characters from `suffix` at the end of the string.
By default, suffix is " ".
@example rightPad("toast", 6) --> "toast "
@example rightPad("2", 2, "!") --> "2!"
@example rightPad("toast", 10, ":)") --> "toast:):):"
]]
local Dash = script.Parent
local assertEqual = require(Dash.assertEqual)
local function rightPad(input: string, length: number, suffix: string?): string
assertEqual(typeof(input), "string", [[Attempted to call Dash.rightPad with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(length), "number", [[Attempted to call Dash.rightPad with argument #2 of type {left:?} not {right:?}]])
local definedSuffix = suffix or " "
assertEqual(typeof(definedSuffix), "string", [[Attempted to call Dash.rightPad with argument #3 of type {left:?} not {right:?}]])
local padLength = length - input:len()
local remainder = padLength % definedSuffix:len()
local repetitions = (padLength - remainder) / definedSuffix:len()
return input .. string.rep(suffix or " ", repetitions) .. definedSuffix:sub(1, remainder)
end
return rightPad
@@ -0,0 +1,28 @@
--[[
Returns `true` if the _left_ and _right_ values are equal (by the equality operator) or the
inputs are tables, and all their keys are equal.
]]
local function shallowEqual(left: any, right: any)
if left == right then
return true
end
if typeof(left) ~= "table" or typeof(right) ~= "table" or #left ~= #right then
return false
end
if left == nil or right == nil then
return false
end
for key, value in pairs(left) do
if right[key] ~= value then
return false
end
end
for key, value in pairs(right) do
if left[key] ~= value then
return false
end
end
return true
end
return shallowEqual
@@ -0,0 +1,41 @@
--[[
Return a portion of the _input_ Array starting with the element at the _left_ index and ending
with the element at the _right_ index (i.e. an inclusive range)
If _left_ is not defined, it defaults to 1.
If _right_ is not defined, it defaults to the length of the array (i.e. the last element)
If _left_ is `-n`, the slice starts with the element `n` places from the last one.
If _right_ is `-n`, the slice ends with the element `n` places from the last one.
An empty array is returned if the slice has no or negative length.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local insert = table.insert
local function slice(input: Types.Array<any>, left: number?, right: number?)
assertEqual(typeof(input), "table", [[Attempted to call Dash.slice with argument #1 of type {left:?} not {right:?}]])
local output = {}
-- Default values
left = left or 1
right = right or #input
assertEqual(typeof(left), "number", [[Attempted to call Dash.slice with argument #2 of type {left:?} not {right:?}]])
assertEqual(typeof(right), "number", [[Attempted to call Dash.slice with argument #3 of type {left:?} not {right:?}]])
if left < 0 then
left = #input + left
end
if right and right < 0 then
right = #input + right
end
for i = left, right do
insert(output, input[i])
end
return output
end
return slice
@@ -0,0 +1,24 @@
--[[
Iterates through the elements of the _input_ Table in no particular order.
Calls the _handler_ for each entry and returns `true` if the handler returns truthy for any
element which it is called with.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
-- TODO Luau: Support generic functions
export type SomeHandler = (any, any) -> boolean
local function some(input: Types.Map<any, any>, handler: SomeHandler?): boolean
assertEqual(typeof(input), "table", [[Attempted to call Dash.some with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(handler), "function", [[Attempted to call Dash.some with argument #2 of type {left:?} not {right:?}]])
for key, child in pairs(input) do
if handler(child, key) then
return true
end
end
return false
end
return some
@@ -0,0 +1,33 @@
--[[
Splits _input_ into parts based on a _pattern_ delimiter and returns a Table of the parts,
followed by a Table of the matched delimiters.
]]
local Dash = script.Parent
local assertEqual = require(Dash.assertEqual)
local insert = table.insert
local function splitOn(input: string, pattern: string): Types.Array<string>
assertEqual(typeof(input), "string", [[Attempted to call Dash.splitOn with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(pattern), "string", [[Attempted to call Dash.splitOn with argument #2 of type {left:?} not {right:?}]])
local parts = {}
local delimiters = {}
local from = 1
if not pattern then
for i = 1, #input do
insert(parts, input:sub(i, i))
end
return parts
end
local delimiterStart, delimiterEnd = input:find(pattern, from)
while delimiterStart do
insert(delimiters, input:sub(delimiterStart, delimiterEnd))
insert(parts, input:sub(from, delimiterStart - 1))
from = delimiterEnd + 1
delimiterStart, delimiterEnd = input:find(pattern, from)
end
insert(parts, input:sub(from))
return parts, delimiters
end
return splitOn
@@ -0,0 +1,15 @@
--[[
Checks if _input_ starts with the string _start_.
@example startsWith("Fun Roblox Games", "Fun") --> true
@example startsWith("Chess", "Fun") --> false
]]
local Dash = script.Parent
local assertEqual = require(Dash.assertEqual)
local function startsWith(input: string, prefix: string): boolean
assertEqual(typeof(input), "string", [[Attempted to call Dash.startsWith with argument #1 of type {left:?} not {right:?}]])
assertEqual(typeof(prefix), "string", [[Attempted to call Dash.startsWith with argument #2 of type {left:?} not {right:?}]])
return input:sub(1, prefix:len()) == prefix
end
return startsWith
@@ -0,0 +1,12 @@
--[[
Remove any whitespace at the start and end of the _input_ string.
]]
local Dash = script.Parent
local assertEqual = require(Dash.assertEqual)
local function trim(input: string): string
assertEqual(typeof(input), "string", [[Attempted to call Dash.trim with argument #1 of type {left:?} not {right:?}]])
return input:match("^%s*(.-)%s*$")
end
return trim
@@ -0,0 +1,24 @@
--[[
Returns an Array of the values in the _input_ Map.
If the input is an Array, ordering is preserved.
If the input is a Map, elements are returned in an arbitrary order.
]]
local Dash = script.Parent
local Types = require(Dash.Types)
local assertEqual = require(Dash.assertEqual)
local iterator = require(Dash.iterator)
local insert = table.insert
-- TODO Luau: Support generic functions
local function values(input: Types.Map<any, any>): Types.Array<any>
assertEqual(typeof(input), "table", [[Attempted to call Dash.values with argument #1 of type {left:?} not {right:?}]])
local result = {}
for _, value in iterator(input) do
insert(result, value)
end
return result
end
return values
@@ -0,0 +1,5 @@
# Generated by Rotriever. Format subject to change in future releases.
name = "roblox/dash"
version = "0.1.7"
commit = "03aaa4ab36e34e5d4c683c3ec337af80cf70a546"
source = "url+https://github.com/roblox/dash"