├── .editorconfig ├── .gitignore ├── .lestrc ├── .prettierrc ├── CHANGELOG.md ├── Docs ├── .nojekyll ├── api.md ├── extra.css └── index.md ├── LICENSE.txt ├── README.md ├── TestPlace.rbxl ├── default.project.json ├── default.rbxp ├── foreman.toml ├── mkdocs.yml ├── place.project.json ├── rotriever.lock ├── rotriever.toml ├── selene.toml ├── src ├── Error.lua ├── None.lua ├── Symbol.lua ├── Types.lua ├── all.lua ├── append.lua ├── assertEqual.lua ├── assign.lua ├── chain.lua ├── class.lua ├── collect.lua ├── collectArray.lua ├── collectSet.lua ├── compose.lua ├── copy.lua ├── count.lua ├── cycles.lua ├── endsWith.lua ├── filter.lua ├── find.lua ├── findIndex.lua ├── flat.lua ├── forEach.lua ├── forEachArgs.lua ├── format.lua ├── formatValue.lua ├── freeze.lua ├── frequencies.lua ├── getOrSet.lua ├── groupBy.lua ├── identity.lua ├── includes.lua ├── init.lua ├── isCallable.lua ├── isLowercase.lua ├── isUppercase.lua ├── iterable.lua ├── iterator.lua ├── join.lua ├── joinDeep.lua ├── keyBy.lua ├── keys.lua ├── last.lua ├── leftPad.lua ├── map.lua ├── mapFirst.lua ├── mapLast.lua ├── mapOne.lua ├── max.lua ├── min.lua ├── noop.lua ├── omit.lua ├── pick.lua ├── pretty.lua ├── product.lua ├── reduce.lua ├── reverse.lua ├── rightPad.lua ├── shallowEqual.lua ├── slice.lua ├── some.lua ├── splitOn.lua ├── startsWith.lua ├── sum.lua ├── trim.lua ├── values.lua └── zip.lua ├── stylua.toml ├── suites ├── jest.config.lua ├── run-tests.lua ├── tests.json └── unit-test-place.rbxp └── tests ├── Error.spec.lua ├── None.spec.lua ├── Symbol.spec.lua ├── all.spec.lua ├── append.spec.lua ├── assertEqual.spec.lua ├── assign.spec.lua ├── chain.spec.lua ├── class.spec.lua ├── collect.spec.lua ├── collectArray.spec.lua ├── collectSet.spec.lua ├── compose.spec.lua ├── copy.spec.lua ├── count.spec.lua ├── customMatchers.lua ├── cycles.spec.lua ├── endsWith.spec.lua ├── filter.spec.lua ├── find.spec.lua ├── findIndex.spec.lua ├── flat.spec.lua ├── forEach.spec.lua ├── forEachArgs.spec.lua ├── format.spec.lua ├── formatValue.spec.lua ├── freeze.spec.lua ├── frequencies.spec.lua ├── getOrSet.spec.lua ├── groupBy.spec.lua ├── identity.spec.lua ├── includes.spec.lua ├── isCallable.spec.lua ├── isLowercase.spec.lua ├── isUppercase.spec.lua ├── iterator.spec.lua ├── join.spec.lua ├── joinDeep.spec.lua ├── keyBy.spec.lua ├── keys.spec.lua ├── last.spec.lua ├── leftPad.spec.lua ├── map.spec.lua ├── mapFirst.spec.lua ├── mapLast.spec.lua ├── mapOne.spec.lua ├── max.spec.lua ├── min.spec.lua ├── noop.spec.lua ├── omit.spec.lua ├── pick.spec.lua ├── pretty.spec.lua ├── product.spec.lua ├── reduce.spec.lua ├── reverse.spec.lua ├── rightPad.spec.lua ├── shallowEqual.spec.lua ├── slice.spec.lua ├── some.spec.lua ├── splitOn.spec.lua ├── startsWith.spec.lua ├── sum.spec.lua ├── trim.spec.lua ├── values.spec.lua └── zip.spec.lua /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | indent_size = 4 9 | indent_style = space 10 | 11 | [*.{lua,json}] 12 | indent_style = tab 13 | indent_size = 4 14 | 15 | [*.yml] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.rbxl.lock 2 | .DS_Store 3 | /site/ 4 | /Packages/ -------------------------------------------------------------------------------- /.lestrc: -------------------------------------------------------------------------------- 1 | { 2 | "testsPath": "$configDir/suites", 3 | "testRunner": "robloxdev-cli", 4 | "fastFlags": ["default"], 5 | "build": false, 6 | "rerun": 0, 7 | "showPrefix": true 8 | } 9 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true 3 | } 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## V1.1.0 2 | 3 | - Added chain function to chain stateful iterators 4 | 5 | ## v1.0.0 6 | 7 | - Changed types to support generics. Type inference should become better. 8 | - Removed types assertions, you must rely on type checker support 9 | - Added zip function to iterate over pairs of elements of the given arrays 10 | - Added all function to check whether all elements conform to the condition 11 | - Added count function to count elements conforming to the condition 12 | - Added frequencies function to count how many times values are encountered in the table 13 | - Added max, min, sum, product functions 14 | 15 | ## v0.1.9 16 | 17 | - Changed package name to Dash 18 | 19 | ## v0.1.8 20 | 21 | - Added helper types for array/set/map/functions 22 | 23 | ## v0.1.7 24 | 25 | - Added stack to the `Error:toString()` (29335cd)[29335cd2249f9265ae388f05e01a95487d4b167a] 26 | 27 | ## v0.1.6 28 | 29 | - Move tests to separate folder so they are not included in runtime build 30 | 31 | ## v0.1.5 32 | 33 | - Move to Roblox repository 34 | 35 | ## v0.1.4 36 | 37 | - collect utilities use iterator 38 | - added Dash.pick 39 | 40 | ## v0.1.3 41 | 42 | - Add iterate and iterable to allow utilities to accept both maps and arrays 43 | 44 | ## v0.1.2 45 | 46 | - Attempt to fix rotriever package definition 47 | 48 | ## v0.1.1 49 | 50 | - Attempt to fix rotriever package definition 51 | 52 | ## v0.1.0 53 | 54 | - Initial release of Dash 55 | -------------------------------------------------------------------------------- /Docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roblox/dash/a87087335b7de9656db10fd721a6205d913a319d/Docs/.nojekyll -------------------------------------------------------------------------------- /Docs/extra.css: -------------------------------------------------------------------------------- 1 | .tags { 2 | float: right; 3 | margin-top: -40px; 4 | border: 1px solid #2094f3; 5 | font-size: 13px; 6 | padding: 0 10px; 7 | border-radius: 2px; 8 | } 9 | -------------------------------------------------------------------------------- /Docs/index.md: -------------------------------------------------------------------------------- 1 | # Dash 2 | 3 | A collection of core utilities expanding the capabilities of Lua in Roblox. 4 | 5 | - [View the source](https://github.com/Roblox/Dash) 6 | - [API Reference](api.md) 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2023 Roblox Corporation 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dash 2 | 3 | A collection of core utilities expanding the capabilities of Lua in Roblox. 4 | 5 | [Read the docs](https://roblox.github.io/dash-internal/) 6 | 7 | ## Install 8 | 9 | Dash can be installed using rotriever. For example: 10 | 11 | ```toml 12 | [dependencies] 13 | Dash = "github.com/roblox/dash-internal" 14 | ``` 15 | -------------------------------------------------------------------------------- /TestPlace.rbxl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Roblox/dash/a87087335b7de9656db10fd721a6205d913a319d/TestPlace.rbxl -------------------------------------------------------------------------------- /default.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TestHarness", 3 | "tree": { 4 | "$path": "src" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /default.rbxp: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": "1", 3 | "instanceHierarchy": { 4 | "name": "Dash", 5 | "tree": { 6 | "$className": "DataModel", 7 | "ReplicatedStorage": { 8 | "$className": "ReplicatedStorage", 9 | "Packages": { 10 | "$path": "Packages", 11 | "Dash": { 12 | "$path": "src", 13 | "Tests": { 14 | "$path": "tests" 15 | } 16 | } 17 | } 18 | } 19 | } 20 | }, 21 | "luau": { 22 | "languageMode": "strict", 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /foreman.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | selene = { source = "Roblox/Kampfkarren-selene", version = "=0.27.0" } 3 | stylua = { source = "Roblox/JohnnyMorganz-StyLua", version = "=0.18.1" } 4 | rotrieve = { source = "roblox/rotriever", version = "=0.5.13-alpha.5" } 5 | lest = { source = "Roblox/lest", version = "3.8.0" } 6 | rbx-aged-cli = { source = "Roblox/rbx-aged-tool", version = "=5.8.0" } 7 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: Dash 2 | extra_css: [extra.css] 3 | theme: 4 | name: material 5 | palette: 6 | primary: blue 7 | repo_url: https://github.com/Roblox/Dash 8 | 9 | nav: 10 | - Home: index.md 11 | - API Reference: api.md 12 | -------------------------------------------------------------------------------- /place.project.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dash Test Place", 3 | "tree": { 4 | "$className": "DataModel", 5 | "ReplicatedStorage": { 6 | "$className": "ReplicatedStorage", 7 | "Dash": { 8 | "$path": "src" 9 | } 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /rotriever.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by rotriever. 2 | # It is not intended for manual editing. 3 | lockfile_format_version = 4 4 | proxy = "https://github.com/roblox/rotriever-proxy-index" 5 | 6 | [[package]] 7 | name = "roblox/dash" 8 | version = "0.1.4" 9 | -------------------------------------------------------------------------------- /rotriever.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "Dash" 3 | authors = ["hcollins"] 4 | description = "A collection of core utilities expanding the capabilities of Lua in Roblox." 5 | repository = "https://github.com/Roblox/dash-internal" 6 | documentation = "https://roblox.github.io/dash-internal" 7 | keywords = ["utilities"] 8 | license = "MIT" 9 | version = "1.1.0" 10 | content_root = "src" 11 | 12 | [config] 13 | registry_index = true 14 | 15 | [dev_dependencies] 16 | JestGlobals = "github.com/Roblox/jest-roblox@3.8.0" 17 | Jest = "github.com/roblox/jest-roblox@3.8.0" 18 | -------------------------------------------------------------------------------- /selene.toml: -------------------------------------------------------------------------------- 1 | std = "roblox" 2 | 3 | exclude = [ 4 | ".git/*", 5 | "Docs/", 6 | ] 7 | 8 | [config] 9 | high_cyclomatic_complexity = { maximum_complexity = 20 } 10 | empty_if = { comments_count = true } 11 | unused_variable = { ignore_pattern = "result|ok|^_" } 12 | shadowing = { ignore_pattern = "result|ok|^_" } 13 | global_usage = { ignore_pattern = "^__" } 14 | -------------------------------------------------------------------------------- /src/Error.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Create an error object with a specified name and message. 3 | 4 | In native Lua, errors can only be string values. At Roblox, we can take advantage of throwing 5 | error objects to provide structured information about problems that occur. 6 | 7 | The tags table stores serializable information about an error which can be provided when it is 8 | thrown, and later passed to a logging endpoint. 9 | 10 | Throwing an error instance captures its stack trace, avoiding the need to explicitly use xpcall. 11 | 12 | @usage In general, errors should not be used during normal control flow. 13 | ]] 14 | local Dash = script.Parent 15 | local Types = require(Dash.Types) 16 | local class = require(Dash.class) 17 | local format = require(Dash.format) :: (string, T...) -> string 18 | local join = require(Dash.join) 19 | 20 | --[[ 21 | Create a new Error instance. 22 | @param name The name of the error 23 | @param string A message for the error which will be formatted using Dash.format 24 | @param tags Any fixed tags 25 | ]] 26 | local Error = class("Error", function(name: string, message: string, tags: Types.Table?) 27 | return { 28 | name = name, 29 | message = message or "An error occurred", 30 | tags = tags or {}, 31 | } 32 | end) 33 | 34 | function Error:toString(): string 35 | return format("{}: {}\n{}", self.name, format(self.message, self.tags), self.stack) 36 | end 37 | 38 | --[[ 39 | Return a new error instance containing the tags provided joined to any existing tags of the 40 | current error instance. 41 | ]] 42 | function Error:joinTags(tags: Types.Table?): Error 43 | return Error.new(self.name, self.message, join(self.tags, tags or {})) 44 | end 45 | 46 | --[[ 47 | Throw an error. 48 | 49 | The stack of the error is captured and stored. 50 | 51 | If additional tags are provided, a new error is created with the joined tags of 52 | this instance. 53 | ]] 54 | function Error:throw(tags: Types.Table?) 55 | local instance = self:joinTags(tags) 56 | instance.stack = debug.traceback() 57 | error(instance) 58 | end 59 | 60 | -- TODO Luau: Define class types automatically 61 | export type Error = typeof(Error.new("", "")) 62 | 63 | return Error 64 | -------------------------------------------------------------------------------- /src/None.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | A symbol representing nothing, that can be used in place of nil as a key or value of a table, 3 | where nil is illegal. 4 | 5 | Utility functions can check for the None symbol and treat it like a nil value. 6 | 7 | @usage Use cases include: 8 | 1. Creating an ordered list with undefined values in it 9 | 2. Creating a map with a key pointing to a nil value 10 | ]] 11 | local Symbol = require(script.Parent.Symbol) 12 | local None = Symbol.new("None") 13 | return None 14 | -------------------------------------------------------------------------------- /src/Symbol.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Create a symbol with a specified name. Upper snake-case is recommended as the symbol is a 3 | constant, unless you are linking the symbol conceptually to a different string. 4 | 5 | Symbols are useful when you want a value that isn't equal to any other type, for example if you 6 | want to store a unique property on an object that won't be accidentally accessed with a simple 7 | string lookup. 8 | 9 | @example 10 | local CHEESE = Symbol.new("CHEESE") 11 | local FAKE_CHEESE = Symbol.new("CHEESE") 12 | print(CHEESE == CHEESE) --> true 13 | print(CHEESE == FAKE_CHEESE) --> false 14 | print(tostring(CHEESE)) --> "Symbol.new(CHEESE)" 15 | ]] 16 | local Dash = script.Parent 17 | local class = require(Dash.class) 18 | 19 | local Symbol = class("Symbol", function(name: string) 20 | return { 21 | name = name, 22 | } 23 | end) 24 | 25 | function Symbol:toString(): string 26 | return ("Symbol(%s)"):format(self.name) 27 | end 28 | 29 | -- TODO Luau: Define class types automatically 30 | export type Symbol = typeof(Symbol.new("")) 31 | 32 | return Symbol 33 | -------------------------------------------------------------------------------- /src/Types.lua: -------------------------------------------------------------------------------- 1 | -- TODO Luau: Support these globally 2 | -- A table with values of type _Value_ and numeric keys 1..n with no gaps 3 | export type Array = { [number]: Value } 4 | -- A table with values of type _Value_ and numeric keys, possibly with gaps 5 | export type Args = { [number]: Value } 6 | -- A table with keys of type _Key_ and values of type _Value_ 7 | export type Map = { [Key]: Value } 8 | -- A table with keys of a fixed type _Key_ and a boolean value representing membership of the set (default is false) 9 | export type Set = { [Key]: boolean } 10 | -- A table of any type 11 | export type Table = { [any]: any } 12 | -- A class has a constructor returning an instance of _Object_ type 13 | export type Class = { 14 | new: () -> Object, 15 | } 16 | -- Represents a function which takes any arguments and returns any value 17 | export type AnyFunction = () -> any 18 | 19 | return {} 20 | -------------------------------------------------------------------------------- /src/all.lua: -------------------------------------------------------------------------------- 1 | local Dash = script.Parent 2 | local Types = require(Dash.Types) 3 | 4 | type AllHandler = (any, any) -> boolean 5 | 6 | local defaultHandler = function(value) 7 | return value 8 | end 9 | 10 | local function all(input: Types.Table, handler: AllHandler?): boolean 11 | local allHandler = handler or defaultHandler 12 | for key, value in input do 13 | if not allHandler(value, key) then 14 | return false 15 | end 16 | end 17 | return true 18 | end 19 | 20 | return all 21 | -------------------------------------------------------------------------------- /src/append.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Adds new elements to the _target_ array from subsequent array arguments in left-to-right order. 3 | 4 | Arguments which are `nil` or None are skipped. 5 | 6 | @mutable target 7 | ]] 8 | 9 | local Dash = script.Parent 10 | local None = require(Dash.None) 11 | local forEachArgs = require(Dash.forEachArgs) 12 | local forEach = require(Dash.forEach) 13 | local insert = table.insert 14 | 15 | local function append(target: { T }, ...: { T }?): { T } 16 | forEachArgs(function(list: { T }?) 17 | -- TODO (AleksandrSl 29/05/2024): Add wrapper function for this. 18 | if list == None or list == nil then 19 | return 20 | else 21 | forEach(list, function(value: T) 22 | insert(target, value) 23 | end) 24 | end 25 | end, ...) 26 | return target 27 | end 28 | 29 | return append 30 | -------------------------------------------------------------------------------- /src/assertEqual.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Performs a simple equality check and throws an error if _left_ is not equal to _right_. 3 | 4 | The formatted error message can be customized, which by default provides a serialization of 5 | both inputs using Dash.pretty. 6 | 7 | The `left` and `right` values are available to be referenced in the formatted message. 8 | ]] 9 | 10 | local Dash = script.Parent 11 | 12 | local function assertEqual(left: any, right: any, formattedErrorMessage: string?) 13 | if left ~= right then 14 | local Error = require(Dash.Error) 15 | local TypeError = 16 | Error.new("AssertError", formattedErrorMessage or `Left "{left}" does not equal right "{right}"`) 17 | TypeError:throw({ 18 | left = left, 19 | right = right, 20 | }) 21 | end 22 | end 23 | 24 | return assertEqual 25 | -------------------------------------------------------------------------------- /src/assign.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Adds new values to _target_ from subsequent Table arguments in left-to-right order. 3 | 4 | The None symbol can be used to remove existing elements in the target. 5 | 6 | @param ... any number of other tables 7 | @example 8 | local characters = { 9 | Frodo = { 10 | name = "Frodo Baggins", 11 | team = "blue" 12 | }, 13 | Boromir = { 14 | score = 5 15 | } 16 | } 17 | local otherCharacters = { 18 | Frodo = { 19 | team = "red", 20 | score = 10 21 | }, 22 | Bilbo = { 23 | team = "yellow", 24 | }, 25 | Boromir = { 26 | score = {1, 2, 3} 27 | } 28 | } 29 | local result = assign(characters, otherCharacters) 30 | print(result) --> { 31 | Frodo = { 32 | team = "red", 33 | score = 10 34 | }, 35 | Bilbo = { 36 | team = "yellow" 37 | }, 38 | Boromir = { 39 | score = {1, 2, 3} 40 | } 41 | } 42 | ]] 43 | local Dash = script.Parent 44 | local None = require(Dash.None) 45 | local Types = require(Dash.Types) 46 | local forEach = require(Dash.forEach) 47 | local forEachArgs = require(Dash.forEachArgs) 48 | 49 | local function assign(target: Types.Map, ...: Types.Map): Types.Map 50 | -- Iterate through the varags in order 51 | forEachArgs(function(input: Types.Map?) 52 | -- Ignore items which are not defined 53 | if input == nil or input == None then 54 | return 55 | else 56 | -- Iterate through each key of the input and assign to target at the same key 57 | forEach(input, function(value, key) 58 | if value == None then 59 | target[key] = nil 60 | else 61 | target[key] = value 62 | end 63 | end) 64 | end 65 | end, ...) 66 | return target 67 | end 68 | 69 | return assign 70 | -------------------------------------------------------------------------------- /src/chain.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns a stateful iterator that returns elements from the first iterable until it is exhausted, 3 | then proceeds to the next iterator, until all the iterators are exhausted. 4 | ]] 5 | 6 | local function chain(...: () -> T): () -> T? 7 | local iterators = { ... } 8 | local iterators_count = #iterators 9 | local current_iterator_index = 1 10 | 11 | return function() 12 | while current_iterator_index <= iterators_count do 13 | local iterator_function = iterators[current_iterator_index] 14 | local result = { iterator_function() } 15 | if result[1] ~= nil then 16 | return table.unpack(result) 17 | else 18 | current_iterator_index = current_iterator_index + 1 19 | end 20 | end 21 | return nil 22 | end 23 | end 24 | 25 | return chain 26 | -------------------------------------------------------------------------------- /src/class.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Create a class called _name_ with the specified _constructor_. The constructor should return a 3 | plain table which will be turned into an instance of _Class_ from a call to `Class.new(...)`. 4 | 5 | @example 6 | -- Create a simple Vehicle class 7 | local Vehicle = class("Vehicle", function(wheelCount: number) return 8 | { 9 | speed = 0, 10 | wheelCount = wheelCount 11 | } 12 | end) 13 | function Vehicle:drive(speed) 14 | self.speed = speed 15 | end 16 | -- Create a car instance 17 | local car = Vehicle.new(4) 18 | car.wheelCount --> 4 19 | car.speed --> 0 20 | -- Drive the car 21 | car:drive(10) 22 | car.speed --> 10 23 | 24 | @usage When using Dash classes, private fields should be prefixed with `_` to avoid accidental access. 25 | @usage A private field should only be accessed by a method of the class itself, though Rodash 26 | does not restrict this in code. 27 | @usage Public fields are recommended when there is no complex access logic e.g. `position.x` 28 | ]] 29 | local Dash = script.Parent 30 | local Types = require(Dash.Types) 31 | 32 | local function throwNotImplemented(tags: Types.Table) 33 | local Error = require(Dash.Error) :: { [string]: any } -- see type Error 34 | local NotImplemented = 35 | Error.new("NotImplemented", [[The method "{methodName}" is not implemented on the class "{className}"]]) 36 | NotImplemented:throw(tags) 37 | end 38 | 39 | export type Constructor = (T...) -> Types.Table 40 | 41 | local defaultConstructor: Constructor<> = function() 42 | return {} 43 | end 44 | 45 | local function class(name: string, constructor: Constructor?) 46 | local classConstructor = constructor or defaultConstructor 47 | local Class = { 48 | name = name, 49 | } 50 | --[[ 51 | Return a new instance of the class, passing any arguments to the specified constructor. 52 | @example 53 | local Car = class("Car", function(speed) 54 | return { 55 | speed = speed 56 | } 57 | end) 58 | local car = Car.new(5) 59 | pretty(car) --> 'Car {speed = 5}' 60 | ]] 61 | function Class.new(...) 62 | local instance = classConstructor(...) 63 | setmetatable(instance, { 64 | __index = Class, 65 | __tostring = Class.toString, 66 | __eq = Class.equals, 67 | __lt = Class.__lt, 68 | __le = Class.__le, 69 | __add = Class.__add, 70 | __sub = Class.__sub, 71 | __mul = Class.__mul, 72 | __div = Class.__div, 73 | __mod = Class.__mod, 74 | }) 75 | instance.Class = Class 76 | instance:_init(...) 77 | return instance 78 | end 79 | --[[ 80 | Run after the instance has been properly initialized, allowing methods on the instance to 81 | be used. 82 | @example 83 | local Vehicle = dash.class("Vehicle", function(wheelCount) return 84 | { 85 | speed = 0, 86 | wheelCount = wheelCount 87 | } 88 | end) 89 | -- Let's define a static private function to generate a unique id for each vehicle. 90 | function Vehicle._getNextId() 91 | Vehicle._nextId = Vehicle._nextId + 1 92 | return Vehicle._nextId 93 | end 94 | Vehicle._nextId = 0 95 | -- A general purpose init function may call other helper methods 96 | function Vehicle:_init() 97 | self._id = self:_generateId() 98 | end 99 | -- Assign an id to the new instance 100 | function Vehicle:_generateId() 101 | return format("#{}: {} wheels", Vehicle._getNextId(), self.wheelCount) 102 | end 103 | -- Return the id if the instance is represented as a string 104 | function Vehicle:toString() 105 | return self._id 106 | end 107 | local car = Vehicle.new(4) 108 | tostring(car) --> "#1: 4 wheels" 109 | ]] 110 | function Class:_init() end 111 | 112 | --[[ 113 | Returns `true` if _value_ is an instance of _Class_ or any sub-class. 114 | @example 115 | local Vehicle = dash.class("Vehicle", function(wheelCount) return 116 | { 117 | speed = 0, 118 | wheelCount = wheelCount 119 | } 120 | end) 121 | local Car = Vehicle:extend("Vehicle", function() 122 | return Vehicle.constructor(4) 123 | end) 124 | local car = Car.new() 125 | car.isInstance(Car) --> true 126 | car.isInstance(Vehicle) --> true 127 | car.isInstance(Bike) --> false 128 | ]] 129 | function Class.isInstance(value) 130 | local ok, isInstance = pcall(function() 131 | local metatable = getmetatable(value) 132 | while metatable do 133 | if metatable.__index == Class then 134 | return true 135 | end 136 | metatable = getmetatable(metatable.__index) 137 | end 138 | return false 139 | end) 140 | return ok and isInstance 141 | end 142 | 143 | --[[ 144 | Create a subclass of _Class_ with a new name _className_ that inherits the metatable of _Class_, 145 | optionally overriding the constructor with _classConstructor_ and providing additional _decorators_. 146 | The super-constructor can be accessed with `Class.constructor`. 147 | Super methods can be accessed using `Class.methodName` and should be called with self. 148 | @example 149 | local Vehicle = dash.class("Vehicle", function(wheelCount) return 150 | { 151 | speed = 0, 152 | wheelCount = wheelCount 153 | } 154 | end) 155 | -- Let's define a static private function to generate a unique id for each vehicle. 156 | function Vehicle._getNextId() 157 | Vehicle._nextId = Vehicle._nextId + 1 158 | return Vehicle._nextId 159 | end 160 | Vehicle._nextId = 0 161 | -- A general purpose init function may call other helper methods 162 | function Vehicle:_init() 163 | self.id = self:_generateId() 164 | end 165 | -- Assign an id to the new instance 166 | function Vehicle:_generateId() 167 | return dash.format("#{}: {} wheels", Vehicle._getNextId(), self.wheelCount) 168 | end 169 | -- Let's make a Car class which has a special way to generate ids 170 | local Car = Vehicle:extend("Vehicle", function() 171 | return Vehicle.constructor(4) 172 | end) 173 | -- Uses the super method to generate a car-specific id 174 | function Car:_generateId() 175 | self.id = dash.format("Car {}", Vehicle._generateId(self)) 176 | end 177 | local car = Car.new() 178 | car.id --> "Car #1: 4 wheels" 179 | ]] 180 | function Class:extend(subClassName: string, subClassConstructor) 181 | local SubClass = class(subClassName, subClassConstructor or Class.new) 182 | setmetatable(SubClass, { __index = self }) 183 | return SubClass 184 | end 185 | 186 | --[[ 187 | Return a string representation of the instance. By default this is the _name_ field (or the 188 | Class name if this is not defined), but the method can be overridden. 189 | @example 190 | local Car = class("Car", function(name) 191 | return { 192 | name = name 193 | } 194 | end) 195 | 196 | local car = Car.new() 197 | car:toString() --> 'Car' 198 | tostring(car) --> 'Car' 199 | print("Hello " .. car) -->> Hello Car 200 | local bob = Car.new("Bob") 201 | bob:toString() --> 'Bob' 202 | tostring(bob) --> 'Bob' 203 | print("Hello " .. bob) -->> Hello Bob 204 | @example 205 | local NamedCar = class("NamedCar", function(name) 206 | return { 207 | name = name 208 | } 209 | end) 210 | function NamedCar:toString() 211 | return "Car called " .. self.name 212 | end 213 | local bob = NamedCar.new("Bob") 214 | bob:toString() --> 'Car called Bob' 215 | tostring(bob) --> 'Car called Bob' 216 | print("Hello " .. bob) -->> Hello Car called Bob 217 | ]] 218 | function Class:toString() 219 | return self.name 220 | end 221 | 222 | --[[ 223 | Returns `true` if `self` is considered equal to _other_. This replaces the `==` operator 224 | on instances of this class, and can be overridden to provide a custom implementation. 225 | ]] 226 | function Class:equals(other) 227 | return rawequal(self, other) 228 | end 229 | 230 | --[[ 231 | Returns `true` if `self` is considered less than _other_. This replaces the `<` operator 232 | on instances of this class, and can be overridden to provide a custom implementation. 233 | ]] 234 | function Class:__lt(_) 235 | throwNotImplemented({ 236 | methodName = "__lt", 237 | className = name, 238 | }) 239 | end 240 | 241 | --[[ 242 | Returns `true` if `self` is considered less than or equal to _other_. This replaces the 243 | `<=` operator on instances of this class, and can be overridden to provide a custom 244 | implementation. 245 | ]] 246 | function Class:__le(_) 247 | throwNotImplemented({ 248 | methodName = "__le", 249 | className = name, 250 | }) 251 | end 252 | 253 | function Class:__add() 254 | throwNotImplemented({ 255 | methodName = "__add", 256 | className = name, 257 | }) 258 | end 259 | function Class:__sub() 260 | throwNotImplemented({ 261 | methodName = "__sub", 262 | className = name, 263 | }) 264 | end 265 | function Class:__mul() 266 | throwNotImplemented({ 267 | methodName = "__mul", 268 | className = name, 269 | }) 270 | end 271 | function Class:__div() 272 | throwNotImplemented({ 273 | methodName = "__div", 274 | className = name, 275 | }) 276 | end 277 | function Class:__mod() 278 | throwNotImplemented({ 279 | methodName = "__mod", 280 | className = name, 281 | }) 282 | end 283 | 284 | return Class 285 | end 286 | 287 | return class 288 | -------------------------------------------------------------------------------- /src/collect.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Collect returns a new Table derived from _input_ by iterating through its pairs and calling 3 | the handler on each `(key, child)` tuple. 4 | 5 | The handler should return a new `(newKey, value)` tuple to be inserted into the returned Table, 6 | or `nil` if no value should be added. 7 | ]] 8 | local Dash = script.Parent 9 | local Types = require(Dash.Types) 10 | 11 | export type CollectHandler = (key: Key, value: Value) -> (NewKey?, NewValue?) 12 | 13 | local function collect( 14 | input: Types.Map, 15 | handler: CollectHandler 16 | ): Types.Map 17 | local result = {} 18 | for key, child in input do 19 | local outputKey, outputValue = handler(key, child) 20 | if outputKey ~= nil then 21 | result[outputKey] = outputValue 22 | end 23 | end 24 | return result 25 | end 26 | return collect 27 | -------------------------------------------------------------------------------- /src/collectArray.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Collect returns a new array derived from _input_ by iterating through its pairs and calling 3 | the handler on each `(key, child)` tuple. 4 | 5 | The handler should return a new value to be pushed onto the end of the result array, or `nil` 6 | if no value should be added. 7 | ]] 8 | 9 | local insert = table.insert 10 | 11 | type CollectHandler = (key: Key, value: Value) -> NewValue? 12 | 13 | local function collectArray(input: { [Key]: Value }, handler: CollectHandler): { NewValue } 14 | local result = {} 15 | for key, child in input do 16 | local outputValue = handler(key, child) 17 | if outputValue ~= nil then 18 | insert(result, outputValue) 19 | end 20 | end 21 | return result 22 | end 23 | return collectArray 24 | -------------------------------------------------------------------------------- /src/collectSet.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Build a set from the entries of the _input_ Table, calling _handler_ on each entry and using 3 | the returned value as an element to add to the set. 4 | 5 | If _handler_ is not provided, values of `input` are used as elements. 6 | ]] 7 | local Dash = script.Parent 8 | local Types = require(Dash.Types) 9 | 10 | type CollectHandler = (key: Key, value: Value) -> NewValue? 11 | 12 | local function collectSet( 13 | input: { [Key]: Value }, 14 | handler: CollectHandler? 15 | ): Types.Set 16 | local result: Types.Set = {} 17 | for key, child in input do 18 | local outputValue 19 | if handler == nil then 20 | outputValue = child 21 | else 22 | outputValue = handler(key, child) 23 | end 24 | if outputValue ~= nil then 25 | result[outputValue] = true 26 | end 27 | end 28 | return result 29 | end 30 | return collectSet 31 | -------------------------------------------------------------------------------- /src/compose.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns a function that calls the argument functions in left-right order on an input, passing 3 | the return of the previous function as argument(s) to the next. 4 | 5 | @example 6 | local function fry(item) 7 | return "fried " .. item 8 | end 9 | local function cheesify(item) 10 | return "cheesy " .. item 11 | end 12 | local prepare = compose(fry, cheesify) 13 | prepare("nachos") --> "cheesy fried nachos" 14 | ]] 15 | -- TODO Luau: Support generic functions 16 | -- TODO Luau: Support varargs 17 | --: ((...A -> ...A)[]) -> ...A -> A 18 | local Dash = script.Parent 19 | local identity = require(Dash.identity) 20 | 21 | local function compose(...) 22 | local fnCount = select("#", ...) 23 | if fnCount == 0 then 24 | return identity 25 | end 26 | local fns = { ... } 27 | return function(...) 28 | local result = { fns[1](...) } 29 | for i = 2, fnCount do 30 | result = { fns[i](unpack(result)) } 31 | end 32 | return unpack(result) 33 | end 34 | end 35 | 36 | return compose 37 | -------------------------------------------------------------------------------- /src/copy.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns a shallow copy of the _input_ Table. 3 | ]] 4 | local Dash = script.Parent 5 | local Types = require(Dash.Types) 6 | local assign = require(Dash.assign) 7 | 8 | local function copy(input: Types.Table): Types.Table 9 | return assign({}, input) 10 | end 11 | return copy 12 | -------------------------------------------------------------------------------- /src/count.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | If no _handler_ is provided, return the number of elements in the _input_ [Table](#table). 3 | 4 | If the _handler_ is provided, increase the count for each element for which the handler returns true on the `(value, key)` pair. 5 | 6 | @example 7 | Dash.count({1, 2, 3, 4, 5}) --> 5 8 | 9 | @example 10 | -- Count the number of ids that start with 1 11 | Dash.count( 12 | {[12] = true, [24] = true, [153] = true, [199] = true}, 13 | function(value, id) return tostring(id):sub(1, 1) == "1" 14 | end) 15 | -- Output: 3 16 | 17 | @example 18 | -- Count the number of numbers divisible by 5 19 | Dash.count( 20 | {1, 1, 2, 3, 5, 8, 13, 21, 34, 55}, 21 | function(num) return num % 5 == 0 22 | end) 23 | 24 | -- Output: 2 25 | ]] 26 | 27 | type CountHandler = (Value, Key) -> boolean 28 | 29 | local defaultHandler = function() 30 | return true 31 | end 32 | 33 | local function count(input: { [Key]: Value }, handler: CountHandler?): number 34 | local counter = 0 35 | local countHandler = handler or defaultHandler 36 | for key, value in input do 37 | if countHandler(value, key) then 38 | counter += 1 39 | end 40 | end 41 | return counter 42 | end 43 | 44 | return count 45 | -------------------------------------------------------------------------------- /src/cycles.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Get information about the number of times references to the same table values appear in a data structure. 3 | 4 | Operates on cyclic structures, and returns a Cycles object for a given _value_ by walking it recursively. 5 | ]] 6 | local Dash = script.Parent 7 | local Types = require(Dash.Types) 8 | local includes = require(Dash.includes) 9 | local keys = require(Dash.keys) 10 | 11 | local sort = table.sort 12 | 13 | export type Cycles = { 14 | -- A set of tables which were visited recursively 15 | visited: Types.Set, 16 | -- A map from table to unique index in visit order 17 | refs: Types.Map, 18 | -- The number to use for the next unique table visited 19 | nextRef: number, 20 | -- An array of keys which should not be visited 21 | omit: { any }, 22 | } 23 | 24 | local function getDefaultCycles(): Cycles 25 | return { 26 | visited = {}, 27 | refs = {}, 28 | nextRef = 1, 29 | omit = {}, 30 | } 31 | end 32 | 33 | -- TODO Luau: Improve type inference to a point that this definition does not produce so many type errors 34 | -- TYPED: local function cycles(value: any, depth: number?, initialCycles: Cycles?): Cycles 35 | local function cycles(input: any, depth: number?, initialCycles: any): Cycles? 36 | if depth == -1 then 37 | return initialCycles 38 | end 39 | 40 | if typeof(input) == "table" then 41 | local childCycles = initialCycles or getDefaultCycles() 42 | 43 | if childCycles.visited[input] then 44 | -- We have already visited the table, so check if it has a reference 45 | if not childCycles.refs[input] then 46 | -- If not, create one as it is present at least twice 47 | childCycles.refs[input] = childCycles.nextRef 48 | childCycles.nextRef += 1 49 | end 50 | return nil 51 | else 52 | -- We haven't yet visited the table, so recurse 53 | childCycles.visited[input] = true 54 | -- Visit in order to preserve reference consistency 55 | local inputKeys = keys(input) 56 | sort(inputKeys, function(left, right) 57 | if typeof(left) == "number" and typeof(right) == "number" then 58 | return left < right 59 | else 60 | return tostring(left) < tostring(right) 61 | end 62 | end) 63 | for _, key in ipairs(inputKeys) do 64 | local value = input[key] 65 | if includes(childCycles.omit, key) then 66 | -- Don't visit omitted keys 67 | continue 68 | end 69 | -- TODO Luau: support type narrowring with "and" 70 | -- TYPED: cycles(key, depth and depth - 1 or nil, childCycles) 71 | -- TYPED: cycles(value, depth and depth - 1 or nil, childCycles) 72 | -- Recurse through both the keys and values of the table 73 | if depth then 74 | cycles(key, depth - 1, childCycles) 75 | cycles(value, depth - 1, childCycles) 76 | else 77 | cycles(key, nil, childCycles) 78 | cycles(value, nil, childCycles) 79 | end 80 | end 81 | end 82 | return childCycles 83 | else 84 | -- Non-tables do not have cycles 85 | return nil 86 | end 87 | end 88 | 89 | return cycles 90 | -------------------------------------------------------------------------------- /src/endsWith.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Checks if _input_ ends with the string _suffix_. 3 | @example endsWith("Fun Roblox Games", "Games") --> true 4 | @example endsWith("Bad Roblox Memes", "Games") --> false 5 | ]] 6 | local function endsWith(input: string, suffix: string) 7 | return input:sub(-suffix:len()) == suffix 8 | end 9 | 10 | return endsWith 11 | -------------------------------------------------------------------------------- /src/filter.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Filter the _input_ Table by calling the handler on each `(child, index)` tuple. 3 | 4 | For an array input, the order of elements is prevered in the output. 5 | 6 | The handler should return truthy to preserve the value in the resulting Table. 7 | ]] 8 | 9 | export type FilterHandler = (Value, Key) -> boolean 10 | 11 | local function filter(input: { [Key]: Value }, handler: FilterHandler): { Value } 12 | local result = {} 13 | for index, child in input do 14 | if handler(child, index) then 15 | table.insert(result, child) 16 | end 17 | end 18 | return result 19 | end 20 | return filter 21 | -------------------------------------------------------------------------------- /src/find.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns an element in the _input_ Table that the handler returns `true` for, when passed the 3 | `(child, key)` entry. 4 | 5 | Returns nil if no entires satisfy the condition. 6 | 7 | For an array, this first matching element is returned. 8 | 9 | For a Map, an arbitrary matching element is returned if it exists. 10 | ]] 11 | 12 | export type FindHandler = (Value, Key) -> boolean 13 | 14 | local function find(input: { [Key]: Value }, handler: FindHandler): Value? 15 | for key, child in input do 16 | if handler(child, key) then 17 | return child 18 | end 19 | end 20 | return nil 21 | end 22 | return find 23 | -------------------------------------------------------------------------------- /src/findIndex.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns the index of the first element in the _input_ array that the handler returns `true` for, 3 | when passed the `(child, key)` entry. 4 | 5 | Returns nil if no entires satisfy the condition. 6 | ]] 7 | 8 | export type FindHandler = (Value, number) -> boolean 9 | 10 | local function findIndex(input: { Value }, handler: FindHandler): number? 11 | for key, child in ipairs(input) do 12 | if handler(child, key) then 13 | return key 14 | end 15 | end 16 | return nil 17 | end 18 | return findIndex 19 | -------------------------------------------------------------------------------- /src/flat.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Flattens the input array by a single level. 3 | 4 | Outputs a new array of elements merged from the _input_ array arguments in left-to-right order. 5 | ]] 6 | local Dash = script.Parent 7 | local append = require(Dash.append) 8 | local forEach = require(Dash.forEach) 9 | 10 | -- TODO Luau: Support function generics 11 | local function flat(input: { any }): { any } 12 | local result = {} 13 | forEach(input, function(childArray: { any }) 14 | append(result, childArray) 15 | end) 16 | return result 17 | end 18 | 19 | return flat 20 | -------------------------------------------------------------------------------- /src/forEach.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Iterates through the elements of the _input_ Table. 3 | 4 | If the table is an array, it iterates in order 1..n. 5 | 6 | If the table is a Map, the keys are visited in an arbitrary order. 7 | 8 | Calls the _handler_ for each entry. 9 | ]] 10 | 11 | export type ForEachHandler = (Value, Key) -> () 12 | 13 | local function forEach(input: { [Key]: Value }, handler: ForEachHandler) 14 | for key, value in input do 15 | handler(value, key) 16 | end 17 | end 18 | return forEach 19 | -------------------------------------------------------------------------------- /src/forEachArgs.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Iterates through the tail arguments in order, including nil values up to the argument list length. 3 | Calls the _handler_ for each entry. 4 | ]] 5 | export type ForEachArgsHandler = (Value, number) -> () 6 | 7 | local function forEachArgs(handler: ForEachArgsHandler, ...: Value) 8 | for index = 1, select("#", ...) do 9 | handler(select(index, ...), index) 10 | end 11 | end 12 | return forEachArgs 13 | -------------------------------------------------------------------------------- /src/format.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns the _format_ string with placeholders `{...}` substituted with readable representations 3 | of the subsequent arguments. 4 | This function is a simpler & more powerful version of `string.format`, inspired by `format!` 5 | in Rust. 6 | 7 | * `{}` formats and prints the next argument using `:format()` if available, or a suitable 8 | default representation depending on its type. 9 | * `{blah}` formats and prints the key "blah" of the 1st argument 10 | * `{2}` formats and prints the 2nd argument. 11 | * `{#2}` prints the length of the 2nd argument. 12 | Display parameters can be combined after a `:` in the curly braces. Any format parameters used 13 | in `string.format` can be used here, along with these extras: 14 | * `{:?}` formats any value using `pretty`. 15 | * `{:#?}` formats any value using multiline `pretty`. 16 | @example 17 | local props = {"teeth", "claws", "whiskers", "tail"} 18 | format("{:?} is in {:#?}", "whiskers", props) 19 | -> '"whiskers" is in {"teeth", "claws", "whiskers", "tail"}' 20 | @example 21 | format("The time is {:02}:{:02}", 2, 4) -> "The time is 02:04" 22 | @example 23 | format("The color blue is #{:06X}", 255) -> "The color blue is #0000FF" 24 | @usage Escape `{` with `{{` and `}` similarly with `}}`. 25 | @usage See [https://developer.roblox.com/articles/Format-String](https://developer.roblox.com/articles/Format-String) 26 | for complete list of formating options and further use cases. 27 | ]] 28 | 29 | local Dash = script.Parent 30 | local formatValue = require(Dash.formatValue) 31 | local splitOn = require(Dash.splitOn) 32 | local startsWith = require(Dash.startsWith) 33 | 34 | local concat = table.concat 35 | local insert = table.insert 36 | 37 | local function format(formatString: string, ...) 38 | local args = { ... } 39 | local argIndex = 1 40 | local texts, subs = splitOn(formatString, "{[^{}]*}") 41 | local result = {} 42 | -- Iterate through possible curly-brace matches, ignoring escaped and substituting valid ones 43 | for i, text in pairs(texts) do 44 | local unescaped = text:gsub("{{", "{"):gsub("}}", "}") 45 | insert(result, unescaped) 46 | local placeholder = subs[i] and subs[i]:sub(2, -2) 47 | if placeholder then 48 | -- Ensure that the curly braces have not been escaped 49 | local escapeMatch = text:gmatch("{+$")() 50 | local isEscaped = escapeMatch and #escapeMatch % 2 == 1 51 | if not isEscaped then 52 | -- Split the placeholder into left & right parts pivoting on the central ":" 53 | local placeholderSplit = splitOn(placeholder, ":") 54 | local isLength = startsWith(placeholderSplit[1], "#") 55 | local argString = isLength and placeholderSplit[1]:sub(2) or placeholderSplit[1] 56 | local nextIndex = tonumber(argString) 57 | local displayString = placeholderSplit[2] 58 | local arg = "nil" 59 | if nextIndex then 60 | -- Return the next argument 61 | arg = args[nextIndex] 62 | elseif argString:len() > 0 then 63 | -- Print a child key of the 1st argument 64 | local argChild = args[1] and args[1][argString] 65 | if argChild ~= nil then 66 | arg = argChild 67 | end 68 | else 69 | arg = args[argIndex] 70 | argIndex = argIndex + 1 71 | end 72 | if isLength then 73 | arg = #arg 74 | end 75 | -- Format the selected value 76 | insert(result, formatValue(arg, displayString or "")) 77 | else 78 | local unescapedSub = placeholder 79 | insert(result, unescapedSub) 80 | end 81 | end 82 | end 83 | return concat(result, "") 84 | end 85 | 86 | return format 87 | -------------------------------------------------------------------------------- /src/formatValue.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Format a specific _value_ using the specified _displayString_. 3 | @example 4 | formatValue(255, "06X") --> "0000FF" 5 | @example 6 | formatValue(255.5) --> "255.5" 7 | @see `format` - for a full description of valid display strings. 8 | ]] 9 | 10 | local Dash = script.Parent 11 | 12 | local function formatValue(value: any, displayString: string): string 13 | displayString = displayString or "" 14 | -- Inline require to prevent infinite require cycle 15 | local displayTypeStart, displayTypeEnd = displayString:find("[A-Za-z#?]+") 16 | if displayTypeStart then 17 | local displayType = displayString:sub(displayTypeStart, displayTypeEnd) 18 | local formatAsString = "%" 19 | .. displayString:sub(1, displayTypeStart - 1) 20 | .. displayString:sub(displayTypeEnd + 1) 21 | .. "s" 22 | -- Pretty print values 23 | local pretty = require(Dash.pretty) 24 | if displayType == "#?" then 25 | -- Multiline print a value 26 | return formatAsString:format(pretty(value, { multiline = true })) 27 | elseif displayType == "?" then 28 | -- Inspect a value 29 | return formatAsString:format(pretty(value)) 30 | end 31 | return ("%" .. displayString):format(value) 32 | else 33 | local displayType = "s" 34 | if type(value) == "number" then 35 | -- Correctly display floats or integers 36 | local _, fraction = math.modf(value) 37 | displayType = fraction == 0 and "d" or "f" 38 | end 39 | return ("%" .. displayString .. displayType):format(tostring(value)) 40 | end 41 | end 42 | 43 | return formatValue 44 | -------------------------------------------------------------------------------- /src/freeze.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns a new read-only view of _object_ which prevents any values from being changed. 3 | 4 | @param name The name of the object for improved error message readability. 5 | @param throwIfMissing If `true` then access to a missing key will also throw. 6 | 7 | @note 8 | Unfortunately you cannot iterate using `pairs` or `ipairs` on frozen objects because Luau 9 | doesn't support defining these custom iterators in metatables. 10 | 11 | @example 12 | local drink = freeze("Ice Cream", { 13 | flavor = "mint", 14 | topping = "sprinkles" 15 | }, true) 16 | print(drink.flavor) --> "mint" 17 | drink.flavor = "vanilla" 18 | --!> ReadonlyKey: Attempt to write to readonly key "flavor" (a string) of frozen object "Ice Cream"` 19 | print(drink.syrup) --> nil 20 | --!> `MissingKey: Attempt to read missing key "syrup" (a string) of frozen object "Ice Cream"` 21 | ]] 22 | local Dash = script.Parent 23 | local Types = require(Dash.Types) 24 | local Error = require(Dash.Error) 25 | local format = require(Dash.format) 26 | 27 | -- TODO Luau: Improve type inference to make these not need to be any 28 | local ReadonlyKey: any = 29 | Error.new("ReadonlyKey", "Attempted to write to readonly key {key:?} of frozen object {objectName:?}") 30 | local MissingKey: any = Error.new("MissingKey", "Attempted to read missing key {key:?} of frozen object {objectName:?}") 31 | 32 | -- TODO Luau: Support generic functions 33 | -- TODO Luau: Support generic extends syntax 34 | -- TODO: Can this be moved to under Tables? 35 | -- TYPED: local function freeze(objectName: string, object: T, throwIfMissing: boolean?): T 36 | local function freeze(objectName: string, object: Types.Table, throwIfMissing: boolean?) 37 | -- We create a proxy so that the underlying object is not affected 38 | local proxy = {} 39 | setmetatable(proxy, { 40 | __index = function(_, key: any) 41 | local value = object[key] 42 | if value == nil and throwIfMissing then 43 | -- Tried to read a key which isn't present in the underlying object 44 | MissingKey:throw({ 45 | key = key, 46 | objectName = objectName, 47 | }) 48 | end 49 | return value 50 | end, 51 | __newindex = function(_, key: any) 52 | -- Tried to write to any key 53 | ReadonlyKey:throw({ 54 | key = key, 55 | objectName = objectName, 56 | }) 57 | end, 58 | __len = function() 59 | return #object 60 | end, 61 | __tostring = function() 62 | return format("Frozen({})", objectName) 63 | end, 64 | __call = function(_, ...) 65 | -- TODO Luau: Gated check for if a function has a __call value 66 | local callable: any = object 67 | return callable(...) 68 | end, 69 | }) 70 | return proxy 71 | end 72 | 73 | return freeze 74 | -------------------------------------------------------------------------------- /src/frequencies.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | If no handler is provided, returns a [Map](#map) with keys as unique values of the _input_ [Table](#table) and values as the count of each value. 3 | 4 | If a handler is provided, returns a [Map](#map) with keys as unique elements given by the _handler_ and values as the count of each mapped key. 5 | 6 | @example 7 | Dash.frequencies({"Red", "Black", "Orange", "Red"}) 8 | Output: { 9 | ["Black"] = 1, 10 | ["Orange"] = 1, 11 | ["Red"] = 2 12 | } 13 | 14 | @example 15 | -- Count colors by simple definition of neutral and non-neutral 16 | function toNeutral(color) 17 | return if Dash.includes({ "Black", "Gray", "White" }, color) then "Neutral" else "Non-neutral" 18 | end 19 | 20 | Dash.frequencies({ "Black", "Orange", "Blue", "Gray", "White" }, function(color) 21 | return toNeutral(color) 22 | end) 23 | 24 | Output: { 25 | ["Neutral"] = 3, 26 | ["Non-neutral"] = 2 27 | } 28 | ]] 29 | local Dash = script.Parent 30 | local reduce = require(Dash.reduce) 31 | 32 | type FrequenciesHandler = (Value, Key) -> NewKey 33 | 34 | local function frequencies( 35 | input: { [Key]: Value }, 36 | handler: FrequenciesHandler? 37 | ): { [NewKey | Value]: number } 38 | return reduce(input, function(acc, value, key) 39 | local newKey = if handler then handler(value, key) else value 40 | if acc[newKey] == nil then 41 | acc[newKey] = 1 42 | else 43 | acc[newKey] += 1 44 | end 45 | return acc 46 | end, {}) 47 | end 48 | 49 | return frequencies 50 | -------------------------------------------------------------------------------- /src/getOrSet.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns a value of the _input_ Table at the _key_ provided. 3 | 4 | If the key is missing, the value is acquired from the _getValue_ handler, 5 | added to the _input_ Table and returned. 6 | ]] 7 | 8 | export type GetValueHandler = ({ [Key]: Value }, Key) -> Value 9 | 10 | local function getOrSet(input: { [Key]: Value }, key: Key, getValue: GetValueHandler): Value 11 | if input[key] == nil then 12 | input[key] = getValue(input, key) 13 | end 14 | return input[key] 15 | end 16 | return getOrSet 17 | -------------------------------------------------------------------------------- /src/groupBy.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Groups values in the _input_ Table by their _getKey_ value. 3 | 4 | Each value of the result Table is an array of values from the _input_ Table which were assigned 5 | the corresponding key. 6 | 7 | If _getKey_ is a function, it is called with each `(child, key)` entry and uses the return 8 | value as the corresponding key to insert at in the result Table. Otherwise, the _getKey_ value 9 | is used directly as the key itself. 10 | ]] 11 | 12 | local insert = table.insert 13 | 14 | export type GroupByHandler = (Value, Key) -> GroupKey 15 | 16 | local function groupBy( 17 | input: { [Key]: Value }, 18 | getKey: GroupByHandler | GroupKey 19 | ): { [GroupKey]: { Value } } 20 | local result = {} 21 | for key, child in pairs(input) do 22 | local groupKey 23 | if typeof(getKey) == "function" then 24 | groupKey = getKey(child, key) 25 | else 26 | groupKey = child[getKey] 27 | end 28 | if groupKey ~= nil then 29 | if result[groupKey] ~= nil then 30 | insert(result[groupKey], child) 31 | else 32 | result[groupKey] = { child } 33 | end 34 | end 35 | end 36 | return result 37 | end 38 | return groupBy 39 | -------------------------------------------------------------------------------- /src/identity.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | The identity function, which simply returns its input parameters. 3 | 4 | Can be used to make it clear that a handler returns its inputs. 5 | ]] 6 | local function identity(...: T...): T... 7 | return ... 8 | end 9 | 10 | return identity 11 | -------------------------------------------------------------------------------- /src/includes.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns `true` if the _item_ exists as a value in the _input_ table. 3 | 4 | A nil _item_ will always return `false`. 5 | ]] 6 | local function includes(input: { [unknown]: Item }, item: Item?): boolean 7 | if item == nil then 8 | return false 9 | end 10 | for _, child in input do 11 | if child == item then 12 | return true 13 | end 14 | end 15 | return false 16 | end 17 | 18 | return includes 19 | -------------------------------------------------------------------------------- /src/init.lua: -------------------------------------------------------------------------------- 1 | local Dash = {} 2 | 3 | local Types = require(script.Types) 4 | 5 | export type Array = Types.Array 6 | export type Args = Types.Args 7 | export type Map = Types.Map 8 | export type Set = Types.Set 9 | export type Table = Types.Table 10 | export type Class = Types.Class 11 | export type AnyFunction = Types.AnyFunction 12 | 13 | -- Require and add the Dash functions to the Dash table 14 | for _, fn in pairs(script:GetChildren()) do 15 | if fn.ClassName == "ModuleScript" then 16 | Dash[fn.Name] = require(fn) 17 | end 18 | end 19 | 20 | return Dash.freeze("Dash", Dash, true) 21 | -------------------------------------------------------------------------------- /src/isCallable.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns `true` if the value can be called i.e. you can write `value(...)`. 3 | ]] 4 | local function isCallable(value: T): boolean 5 | return type(value) == "function" 6 | or (type(value) == "table" and getmetatable(value) and getmetatable(value).__call ~= nil) 7 | or false 8 | end 9 | 10 | return isCallable 11 | -------------------------------------------------------------------------------- /src/isLowercase.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns `true` if the first character of _input_ is a lower-case character. 3 | 4 | Throws if the _input_ is not a string or it is the empty string. 5 | 6 | Our current version of Lua unfortunately does not support upper or lower-case detection outside 7 | the english alphabet. This function has been implemented to return the expected result once 8 | this has been corrected. 9 | ]] 10 | local Dash = script.Parent 11 | local assertEqual = require(Dash.assertEqual) 12 | 13 | local function isLowercase(input: string) 14 | assertEqual(#input > 0, true, [[Attempted to call Dash.isLowercase with an empty string]]) 15 | local firstLetter = input:sub(1, 1) 16 | return firstLetter == firstLetter:lower() 17 | end 18 | return isLowercase 19 | -------------------------------------------------------------------------------- /src/isUppercase.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns `true` if the first character of _input_ is an upper-case character. 3 | 4 | Throws if the _input_ is not a string or it is the empty string. 5 | 6 | Our current version of Lua unfortunately does not support upper or lower-case detection outside 7 | the english alphabet. This function has been implemented to return the expected result once 8 | this has been corrected. 9 | ]] 10 | local Dash = script.Parent 11 | local assertEqual = require(Dash.assertEqual) 12 | 13 | local function isUppercase(input: string) 14 | assertEqual(#input > 0, true, [[Attempted to call Dash.isUppercase with an empty string]]) 15 | local firstLetter = input:sub(1, 1) 16 | return firstLetter == firstLetter:upper() 17 | end 18 | return isUppercase 19 | -------------------------------------------------------------------------------- /src/iterable.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Creates a stateful iterator for the _input_ Table, first visting ordered numeric keys 1..n 3 | and then the remaining unordered keys in any order. 4 | 5 | @see Dash.iterator - for an iterator that can iterate over an iterable. 6 | ]] 7 | 8 | local Dash = script.Parent 9 | local Types = require(Dash.Types) 10 | 11 | local function iterable(input: Types.Table): Types.AnyFunction 12 | local currentIndex = 1 13 | local inOrderedKeys = true 14 | local currentKey 15 | local iterateFn 16 | iterateFn = function() 17 | if inOrderedKeys then 18 | local value = input[currentIndex] 19 | if value == nil then 20 | inOrderedKeys = false 21 | else 22 | local index = currentIndex 23 | currentIndex += 1 24 | return index, value 25 | end 26 | end 27 | while true do 28 | currentKey = next(input, currentKey) 29 | -- Don't re-visit ordered keys 1..n 30 | if 31 | typeof(currentKey) == "number" 32 | and currentKey > 0 33 | and currentKey < currentIndex 34 | and currentKey % 1 == 0 35 | then 36 | continue 37 | end 38 | if currentKey == nil then 39 | return nil 40 | else 41 | return currentKey, input[currentKey] 42 | end 43 | end 44 | end 45 | return iterateFn 46 | end 47 | 48 | return iterable 49 | -------------------------------------------------------------------------------- /src/iterator.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Iterates using a `pairs` iterator for an _input_ Table if zero length, otherwise an `ipairs` 3 | iterator for an array. 4 | 5 | If _input_ is a function it is used as a stateful iterator instead. 6 | 7 | This function can be used to build behaviour that iterates over both arrays and Maps. 8 | 9 | @see Dash.iterable if you want to iterate over a Table with numeric but un-ordered keys. 10 | ]] 11 | 12 | local Dash = script.Parent 13 | local Types = require(Dash.Types) 14 | 15 | local function iterator(input: Types.Table | Types.AnyFunction) 16 | if typeof(input) == "function" then 17 | return input 18 | elseif typeof(input) == "table" then 19 | if #input > 0 then 20 | return ipairs(input) 21 | else 22 | return pairs(input) 23 | end 24 | else 25 | return nil 26 | end 27 | end 28 | 29 | return iterator 30 | -------------------------------------------------------------------------------- /src/join.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Output a new Map from merging all the keys in the Map arguments in left-to-right order. 3 | 4 | The None symbol can be used to remove existing elements. 5 | 6 | @param ... any number of tables 7 | ]] 8 | local Dash = script.Parent 9 | local Types = require(Dash.Types) 10 | local assign = require(Dash.assign) 11 | 12 | local function join(...: Types.Map): Types.Map 13 | return assign({}, ...) 14 | end 15 | 16 | return join 17 | -------------------------------------------------------------------------------- /src/joinDeep.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Creates a shallow clone of the _source_ Map, and copies the values from the _delta_ Map 3 | by key, like the join utility. 4 | 5 | However, if any of the values are tables themselves, the joinDeep function is called 6 | recursively to produce a new table at the specified key. 7 | 8 | The purpose of this function is to merge nested immutable data using as few table 9 | creation operations as possible, making it appropriate for updating state in a reducer. 10 | 11 | The None symbol can be used to remove an existing value. 12 | ]] 13 | local Dash = script.Parent 14 | local None = require(Dash.None) 15 | local Types = require(Dash.Types) 16 | local forEach = require(Dash.forEach) 17 | local copy = require(Dash.copy) 18 | 19 | -- TODO Luau: Support typing varargs 20 | -- TODO Luau: Support function generics 21 | local function joinDeep(source: Types.Table, delta: Types.Table): Types.Table 22 | local result = copy(source) 23 | -- Iterate through each key of the input and assign to target at the same key 24 | forEach(delta, function(value, key) 25 | if typeof(source[key]) == "table" and typeof(value) == "table" then 26 | -- Only merge tables 27 | result[key] = joinDeep(source[key], value) 28 | elseif value == None then 29 | -- Remove none values 30 | result[key] = nil 31 | else 32 | result[key] = value 33 | end 34 | end) 35 | return result 36 | end 37 | 38 | return joinDeep 39 | -------------------------------------------------------------------------------- /src/keyBy.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Assigns values in the _input_ Table by their _getKey_ value. 3 | 4 | If _getKey_ is a function, it is called with each `(child, key)` entry and uses the return 5 | value as the corresponding key to assign to in the result Table. Otherwise, the _getKey_ value 6 | is used directly as the key itself. 7 | ]] 8 | 9 | local Dash = script.Parent 10 | local collect = require(Dash.collect) 11 | 12 | export type KeyByHandler = (Value, Key) -> NewKey 13 | 14 | local function keyBy(input: { [Key]: Value }, getKey: KeyByHandler | NewKey): { [NewKey]: Value } 15 | return collect(input, function(key, child) 16 | local newKey 17 | if typeof(getKey) == "function" then 18 | newKey = getKey(child, key) 19 | else 20 | newKey = child[getKey] 21 | end 22 | return newKey, child 23 | end) 24 | end 25 | return keyBy 26 | -------------------------------------------------------------------------------- /src/keys.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns an array of the keys in the _input_ Table. 3 | 4 | If the input is an array, ordering is preserved. 5 | 6 | If the input is a Map, elements are returned in an arbitrary order. 7 | ]] 8 | 9 | local insert = table.insert 10 | 11 | local function keys(input: { [Key]: Value }): { Key } 12 | local result = {} 13 | for key, _ in input do 14 | insert(result, key) 15 | end 16 | return result 17 | end 18 | return keys 19 | -------------------------------------------------------------------------------- /src/last.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns the last element in the _input_ array that the handler returns `true` for, when 3 | passed the `(child, index)` entry. 4 | 5 | Returns nil if no entires satisfy the condition. 6 | 7 | If handler is not defined, the function simply returns the last element of the array. 8 | ]] 9 | 10 | export type FindHandler = (Value, number) -> boolean 11 | 12 | local function last(input: { Value }, handler: FindHandler?): Value? 13 | for index = #input, 1, -1 do 14 | local child = input[index] 15 | if not handler then 16 | return child 17 | end 18 | if handler and handler(child, index) then 19 | return child 20 | end 21 | end 22 | return nil 23 | end 24 | 25 | return last 26 | -------------------------------------------------------------------------------- /src/leftPad.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Makes a string of `length` from `input` by repeating characters from `prefix` at the start of the string. 3 | @example leftPad("toast", 6) --> " toast" 4 | @example leftPad("2", 2, "0") --> "02" 5 | @example leftPad("toast", 10, ":)") --> ":):):toast" 6 | @param prefix (default = `" "`) 7 | ]] 8 | local function leftPad(input: string, length: number, prefix: string?): string 9 | local definedPrefix = prefix or " " 10 | 11 | local padLength = length - input:len() 12 | local remainder = padLength % definedPrefix:len() 13 | local repetitions = (padLength - remainder) / definedPrefix:len() 14 | return string.rep(definedPrefix or " ", repetitions) .. definedPrefix:sub(1, remainder) .. input 15 | end 16 | 17 | return leftPad 18 | -------------------------------------------------------------------------------- /src/map.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Iterates through the elements of the _input_ Table. 3 | 4 | For an array input, the elements are visted in order 1..n. 5 | 6 | For a Map input, the elements are visited in an arbitrary order. 7 | 8 | Calls the _handler_ for each entry and constructs a new Table using the same keys but replacing 9 | the values with new ones returned from the handler. 10 | 11 | Values returned by _handler_ must be defined. 12 | 13 | @see Dash.collectArray if you want to return nil values. 14 | ]] 15 | 16 | local Dash = script.Parent 17 | local assertEqual = require(Dash.assertEqual) 18 | 19 | export type MapHandler = (Value, Key) -> NewValue 20 | 21 | local function map(input: { [Key]: Value }, handler: MapHandler): { [Key]: NewValue } 22 | local result = {} 23 | for key, child in input do 24 | local value = handler(child, key) 25 | assertEqual(value == nil, false, [[Returned nil from a Dash.map handler]]) 26 | result[key] = value 27 | end 28 | return result 29 | end 30 | return map 31 | -------------------------------------------------------------------------------- /src/mapFirst.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Iterates through the elements of the _input_ array in order 1..n. 3 | 4 | Calls the _handler_ for each entry and returns the first non-nil value returned by the _handler_. 5 | If all returned from the _handler_ values are `nil`, `nil` is returned. 6 | ]] 7 | 8 | export type MapHandler = (Value, number) -> NewValue? 9 | 10 | local function mapFirst(input: { Value }, handler: MapHandler): NewValue? 11 | for index, child in ipairs(input) do 12 | local output = handler(child, index) 13 | if output ~= nil then 14 | return output 15 | end 16 | end 17 | return nil 18 | end 19 | return mapFirst 20 | -------------------------------------------------------------------------------- /src/mapLast.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Iterates through the elements of the _input_ array in reverse in order n..1. 3 | 4 | Calls the _handler_ for each entry and returns the first non-nil value returned by the _handler_. 5 | If all returned from the _handler_ values are `nil`, `nil` is returned. 6 | ]] 7 | 8 | export type MapHandler = (Value, number) -> NewValue? 9 | 10 | local function mapLast(input: { Value }, handler: MapHandler): NewValue? 11 | for key = #input, 1, -1 do 12 | local child = input[key] 13 | local output = handler(child, key) 14 | if output ~= nil then 15 | return output 16 | end 17 | end 18 | return nil 19 | end 20 | return mapLast 21 | -------------------------------------------------------------------------------- /src/mapOne.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Iterates through the elements of the _input_ Table in no particular order. 3 | 4 | Calls the _handler_ for each entry and returns the first non-nil value returned by the _handler_. 5 | If all returned from the _handler_ values are `nil`, `nil` is returned. 6 | 7 | If _handler_ is `nil`, the first value visited is returned. 8 | ]] 9 | 10 | export type MapHandler = (Value, Key) -> NewValue? 11 | 12 | local function mapOne(input: { [Key]: Value }, handler: MapHandler?): NewValue? 13 | for key, child in pairs(input) do 14 | local output 15 | if handler then 16 | output = handler(child, key) 17 | else 18 | output = child 19 | end 20 | if output ~= nil then 21 | return output 22 | end 23 | end 24 | return nil 25 | end 26 | return mapOne 27 | -------------------------------------------------------------------------------- /src/max.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns the maximum value in the _input_ [Table](#table). By default, this returns the greatest value in the table as defined by the `>` operator. 3 | 4 | If a _handler_ is provided, elements will first be transformed by `handler(value, key)` before being used for comparison. 5 | 6 | Elements will be compared using the _comparator_ if it is provided. The comparator should take two elements and return `true` if the first one is greater than the second one, and `false` otherwise. 7 | 8 | If the provided _input_ has no elements, return `nil`. 9 | 10 | @example 11 | Dash.max({1, 2, 3}) --> 3 12 | 13 | @example 14 | Dash.max({"1", "2", "10"}) --> "2" 15 | 16 | @example 17 | Dash.max({1, 2, 3}, function(num) return -num end) --> 1 18 | 19 | @example 20 | Dash.max( 21 | {"blue", "red", "orange"}, 22 | nil, 23 | function(word1, word2) return word1:len() > word2:len() end 24 | ) --> "orange" 25 | ]] 26 | 27 | local Dash = script.Parent 28 | local Types = require(Dash.Types) 29 | local reduce = require(Dash.reduce) 30 | 31 | -- Return true if a > b 32 | type MaxComparator = (any, any) -> boolean 33 | type MaxHandler = (any, any) -> any 34 | 35 | local defaultComparator = function(a, b) 36 | return a > b 37 | end 38 | 39 | local function max(input: Types.Table, handler: MaxHandler?, comparator: MaxComparator?): number 40 | local comparatorFn = comparator or defaultComparator 41 | 42 | if handler then 43 | local accumulator = reduce(input, function(acc, value, key) 44 | local newValue = handler(value, key) 45 | if not acc then 46 | return { value = newValue, original = value } 47 | end 48 | return if comparatorFn(newValue, acc.value) then { value = newValue, original = value } else acc 49 | end, nil) 50 | return accumulator.original 51 | end 52 | 53 | return reduce(input, function(acc, value) 54 | if not acc then 55 | return value 56 | end 57 | return if comparatorFn(value, acc) then value else acc 58 | end, nil) 59 | end 60 | 61 | return max 62 | -------------------------------------------------------------------------------- /src/min.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns the minimum value in the _input_ [Table](#table). By default, this returns the least value in the table as defined by the `<` operator. 3 | 4 | If a _handler_ is provided, elements will first be transformed by `handler(value, key)` before being used for comparison. 5 | 6 | Elements will be compared using the _comparator_ if it is provided. The comparator should take two elements and return `true` if the first one is less than the second one, and `false` otherwise. 7 | 8 | If the provided _input_ has no elements, return `nil`. 9 | 10 | @wxample 11 | Dash.min({1, 2, 3}) --> 1 12 | 13 | @wxample 14 | Dash.min({"2", "20", "10"}) --> "10" 15 | 16 | @wxample 17 | Dash.min({3, 2, 1}, function(num) return -num end) --> 3 18 | 19 | @wxample 20 | Dash.min( 21 | {"blue", "red", "orange"}, 22 | nil, 23 | function(word1, word2) return word1:len() < word2:len() end 24 | ) --> "red" 25 | ]] 26 | local Dash = script.Parent 27 | local Types = require(Dash.Types) 28 | local reduce = require(Dash.reduce) 29 | 30 | -- Return true if a < b 31 | type MinComparator = (any, any) -> boolean 32 | type MinHandler = (any, any) -> any 33 | 34 | local defaultComparator = function(a, b) 35 | return a < b 36 | end 37 | 38 | -- TODO (AleksandrSl 03/06/2024): Can be further "optimized" by writing a compare function that will do all the stuff and pass default comparator 39 | local function min(input: Types.Table, handler: MinHandler?, comparator: MinComparator?): number 40 | local comparatorFn = comparator or defaultComparator 41 | 42 | if handler then 43 | local accumulator = reduce(input, function(acc, value, key) 44 | local newValue = handler(value, key) 45 | if not acc then 46 | return { value = newValue, original = value } 47 | end 48 | return if comparatorFn(newValue, acc.value) then { value = newValue, original = value } else acc 49 | end, nil) 50 | return accumulator.original 51 | end 52 | 53 | return reduce(input, function(acc, value) 54 | if not acc then 55 | return value 56 | end 57 | return if comparatorFn(value, acc) then value else acc 58 | end, nil) 59 | end 60 | 61 | return min 62 | -------------------------------------------------------------------------------- /src/noop.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | A function which does nothing. 3 | 4 | Can be used to make it clear that a handler has no function. 5 | ]] 6 | local function noop() end 7 | 8 | return noop 9 | -------------------------------------------------------------------------------- /src/omit.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Return a new Table made from entries in the _input_ Table whose key is not in the _keys_ array. 3 | 4 | If the input is an array, ordering is preserved. 5 | 6 | If the input is a Map, elements are returned in an arbitrary order. 7 | ]] 8 | local Dash = script.Parent 9 | local collectSet = require(Dash.collectSet) 10 | local forEach = require(Dash.forEach) 11 | 12 | local function omit(input: { [Key]: Value }, keys: { Key }): { [Key]: Value } 13 | local output = {} 14 | local keySet = collectSet(keys) 15 | forEach(input, function(_, key) 16 | if not keySet[key] then 17 | output[key] = input[key] 18 | end 19 | end) 20 | return output 21 | end 22 | 23 | return omit 24 | -------------------------------------------------------------------------------- /src/pick.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Pick entries in the _input_ Table which should remain in the output by calling the handler on 3 | each `(child, index)` tuple. 4 | 5 | The handler should return truthy to preserve the value in the resulting Table. 6 | ]] 7 | 8 | export type PickHandler = (Value, Key) -> boolean 9 | 10 | local function pick(input: { [Key]: Value }, handler: PickHandler): { [Key]: Value } 11 | local result = {} 12 | for key, child in input do 13 | if handler(child, key) then 14 | result[key] = child 15 | end 16 | end 17 | return result 18 | end 19 | return pick 20 | -------------------------------------------------------------------------------- /src/pretty.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Return a pretty string serialization of _object_. 3 | 4 | This implementation deals with cycles in tables and can neatly display metatables. 5 | 6 | Optionally use an indented multiline string, limit the depth of tables, omit or pick keys. 7 | ]] 8 | local Dash = script.Parent 9 | local Types = require(Dash.Types) 10 | 11 | local append = require(Dash.append) 12 | local assign = require(Dash.assign) 13 | local cycles = require(Dash.cycles) 14 | local includes = require(Dash.includes) 15 | local join = require(Dash.join) 16 | local map = require(Dash.map) 17 | local keys = require(Dash.keys) 18 | local slice = require(Dash.slice) 19 | 20 | local concat = table.concat 21 | local insert = table.insert 22 | local sort = table.sort 23 | 24 | export type PrettyOptions = { 25 | -- The maximum depth of ancestors of a table to display (default = 2) 26 | depth: number?, 27 | -- An array of keys which should not be visited 28 | omit: { any } | nil, 29 | -- Whether to use multiple lines (default = false) 30 | multiline: boolean?, 31 | -- Whether to show the length of any array in front of its content 32 | arrayLength: boolean?, 33 | -- The maximum length of a line (default = 80) 34 | maxLineLength: number?, 35 | -- Whether to drop the quotation marks around strings. By default, this is true for table keys 36 | noQuotes: boolean?, 37 | -- The indent string to use (default = "\t") 38 | indent: string?, 39 | -- A set of tables which have already been visited and should be referred to by reference 40 | visited: Types.Set?, 41 | -- A cycles object returned from `cycles` to aid reference display 42 | cycles: cycles.Cycles?, 43 | } 44 | 45 | local function indentLines(lines: { string }, indent: string) 46 | return map(lines, function(line: string) 47 | return indent .. line 48 | end) 49 | end 50 | 51 | local pretty 52 | 53 | -- TODO Luau: Improve type inference to a point that this definition does not produce so many type errors 54 | -- local function prettyLines(object: any, options: PrettyOptions?): { string } 55 | local function prettyLines(object: any, options: any): { string } 56 | options = options or {} 57 | if type(object) == "table" then 58 | -- A table needs to be serialized recusively 59 | -- Construct the options for recursive calls for the table values 60 | local valueOptions = assign( 61 | { 62 | visited = {}, 63 | indent = "\t", 64 | depth = 2, 65 | }, 66 | options, 67 | { 68 | -- Depth is reduced until we shouldn't recurse any more 69 | depth = options.depth and options.depth - 1 or nil, 70 | cycles = options.cycles or cycles(object, options.depth, { 71 | visited = {}, 72 | refs = {}, 73 | nextRef = 0, 74 | depth = options.depth, 75 | omit = options.omit or {}, 76 | }), 77 | } 78 | ) 79 | if valueOptions.depth == -1 then 80 | -- Indicate there is more information available beneath the maximum depth 81 | return { "..." } 82 | end 83 | if valueOptions.visited[object] then 84 | -- Indicate this table has been printed already, so print a ref number instead of 85 | -- printing it multiple times 86 | return { "&" .. valueOptions.cycles.refs[object] } 87 | end 88 | 89 | valueOptions.visited[object] = true 90 | 91 | local multiline = valueOptions.multiline 92 | local comma = multiline and "," or ", " 93 | 94 | -- If the table appears multiple times in the output, mark it with a ref prefix so it can 95 | -- be identified if it crops up later on 96 | local ref = valueOptions.cycles.refs[object] 97 | local refTag = ref and ("<%s>"):format(ref) or "" 98 | local lines = { refTag .. "{" } 99 | 100 | -- Build the options for the recursive call for the table keys 101 | local keyOptions = join(valueOptions, { 102 | noQuotes = true, 103 | multiline = false, 104 | }) 105 | 106 | -- Compact numeric keys into a simpler array style 107 | local maxConsecutiveIndex = 0 108 | local first = true 109 | for index, value in ipairs(object) do 110 | if valueOptions.omit and includes(valueOptions.omit, index) then 111 | -- Don't include keys which are omitted 112 | continue 113 | end 114 | if first then 115 | first = false 116 | else 117 | lines[#lines] = lines[#lines] .. comma 118 | end 119 | if valueOptions.multiline then 120 | local indendedValue = indentLines(prettyLines(value, valueOptions), valueOptions.indent) 121 | append(lines, indendedValue) 122 | else 123 | lines[#lines] = lines[#lines] .. pretty(value, valueOptions) 124 | end 125 | maxConsecutiveIndex = index 126 | end 127 | if #object > 0 and valueOptions.arrayLength then 128 | lines[1] = ("#%d %s"):format(#object, lines[1]) 129 | end 130 | -- Ensure keys are printed in order to guarantee consistency 131 | local objectKeys = keys(object) 132 | sort(objectKeys, function(left, right) 133 | if typeof(left) == "number" and typeof(right) == "number" then 134 | return left < right 135 | else 136 | return tostring(left) < tostring(right) 137 | end 138 | end) 139 | for _, key in ipairs(objectKeys) do 140 | local value = object[key] 141 | -- We printed a key if it's an index e.g. an integer in the range 1..n. 142 | if typeof(key) == "number" and key % 1 == 0 and key >= 1 and key <= maxConsecutiveIndex then 143 | continue 144 | end 145 | if valueOptions.omit and includes(valueOptions.omit, key) then 146 | -- Don't include keys which are omitted 147 | continue 148 | end 149 | if first then 150 | first = false 151 | else 152 | lines[#lines] = lines[#lines] .. comma 153 | end 154 | if valueOptions.multiline then 155 | local keyLines = prettyLines(key, keyOptions) 156 | local indentedKey = indentLines(keyLines, valueOptions.indent) 157 | local valueLines = prettyLines(value, valueOptions) 158 | local valueTail = slice(valueLines, 2) 159 | local indendedValueTail = indentLines(valueTail, valueOptions.indent) 160 | -- The last line of the key and first line of the value are concatenated together 161 | indentedKey[#indentedKey] = ("%s = %s"):format(indentedKey[#indentedKey], valueLines[1]) 162 | append(lines, indentedKey) 163 | append(lines, indendedValueTail) 164 | else 165 | lines[#lines] = ("%s%s = %s"):format( 166 | lines[#lines], 167 | pretty(key, keyOptions), 168 | pretty(value, valueOptions) 169 | ) 170 | end 171 | end 172 | if valueOptions.multiline then 173 | if first then 174 | -- An empty table is just represented as {} 175 | lines[#lines] = lines[#lines] .. "}" 176 | else 177 | insert(lines, "}") 178 | end 179 | else 180 | lines[#lines] = ("%s}"):format(lines[#lines]) 181 | end 182 | return lines 183 | elseif type(object) == "string" and not options.noQuotes then 184 | return { ('"%s"'):format(object) } 185 | else 186 | return { tostring(object) } 187 | end 188 | end 189 | 190 | -- TODO Luau: Improve type inference to a point that this definition does not produce so many type errors 191 | -- pretty = function(object: any, options: PrettyOptions?): string 192 | pretty = function(object: any, options: any): string 193 | return concat(prettyLines(object, options), "\n") 194 | end 195 | 196 | return pretty 197 | -------------------------------------------------------------------------------- /src/product.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Multiplies all numbers in the _input_ array. 3 | 4 | If the _input_ has no elements, return `1`. 5 | 6 | @example 7 | Dash.product({3, 3, 2}) --> 18 8 | ]] 9 | local Dash = script.Parent 10 | local reduce = require(Dash.reduce) 11 | 12 | local function product(input: { number }): number 13 | return reduce(input, function(acc, num) 14 | return acc * num 15 | end, 1) 16 | end 17 | 18 | return product 19 | -------------------------------------------------------------------------------- /src/reduce.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Iterate through the elements of the _input_ array in order 1..n. 3 | 4 | Call the _handler_ for each element, passing the return of the previous call as the first argument. 5 | 6 | The _initial_ value is passed into the first call, and the final value returned by the function. 7 | ]] 8 | 9 | export type ReduceHandler = (Accumulator, Value, Key) -> Accumulator 10 | 11 | local function reduce( 12 | input: { [Key]: Value }, 13 | handler: ReduceHandler, 14 | initial: Accumulator 15 | ): Accumulator 16 | local result = initial 17 | for key, value in input do 18 | result = handler(result, value, key) 19 | end 20 | return result 21 | end 22 | 23 | return reduce 24 | -------------------------------------------------------------------------------- /src/reverse.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Reverse the order of the elements in the _input_ array. 3 | ]] 4 | 5 | local insert = table.insert 6 | 7 | local function reverse(input: { T }): { T } 8 | local output: { T } = {} 9 | for i = #input, 1, -1 do 10 | insert(output, input[i]) 11 | end 12 | return output 13 | end 14 | return reverse 15 | -------------------------------------------------------------------------------- /src/rightPad.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Makes a string of `length` from `input` by repeating characters from `suffix` at the end of the string. 3 | 4 | By default, suffix is " ". 5 | 6 | @example rightPad("toast", 6) --> "toast " 7 | @example rightPad("2", 2, "!") --> "2!" 8 | @example rightPad("toast", 10, ":)") --> "toast:):):" 9 | ]] 10 | local function rightPad(input: string, length: number, suffix: string?): string 11 | local definedSuffix = suffix or " " 12 | local padLength = length - input:len() 13 | local remainder = padLength % definedSuffix:len() 14 | local repetitions = (padLength - remainder) / definedSuffix:len() 15 | return input .. string.rep(suffix or " ", repetitions) .. definedSuffix:sub(1, remainder) 16 | end 17 | 18 | return rightPad 19 | -------------------------------------------------------------------------------- /src/shallowEqual.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns `true` if the _left_ and _right_ values are equal (by the equality operator) or the 3 | inputs are tables, and all their keys are equal. 4 | ]] 5 | local function shallowEqual(left: any, right: any) 6 | if left == right then 7 | return true 8 | end 9 | if typeof(left) ~= "table" or typeof(right) ~= "table" or #left ~= #right then 10 | return false 11 | end 12 | if left == nil or right == nil then 13 | return false 14 | end 15 | for key, value in pairs(left) do 16 | if right[key] ~= value then 17 | return false 18 | end 19 | end 20 | for key, value in pairs(right) do 21 | if left[key] ~= value then 22 | return false 23 | end 24 | end 25 | return true 26 | end 27 | 28 | return shallowEqual 29 | -------------------------------------------------------------------------------- /src/slice.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Return a portion of the _input_ array starting with the element at the _left_ index and ending 3 | with the element at the _right_ index (i.e. an inclusive range) 4 | 5 | If _left_ is not defined, it defaults to 1. 6 | If _right_ is not defined, it defaults to the length of the array (i.e. the last element) 7 | 8 | If _left_ is `-n`, the slice starts with the element `n` places from the last one. 9 | If _right_ is `-n`, the slice ends with the element `n` places from the last one. 10 | 11 | An empty array is returned if the slice has no or negative length. 12 | ]] 13 | local insert = table.insert 14 | 15 | local function slice(input: { T }, left: number?, right: number?): { T } 16 | local output: { T } = {} 17 | 18 | -- Default values 19 | local l: number = left or 1 20 | local r: number = right or #input 21 | 22 | if l < 0 then 23 | l = #input + l 24 | end 25 | if r and r < 0 then 26 | r = #input + r 27 | end 28 | for i = l, r do 29 | insert(output, input[i]) 30 | end 31 | return output 32 | end 33 | 34 | return slice 35 | -------------------------------------------------------------------------------- /src/some.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Iterates through the elements of the _input_ Table in no particular order. 3 | 4 | Calls the _handler_ for each entry and returns `true` if the handler returns truthy for any 5 | element which it is called with. 6 | ]] 7 | 8 | export type SomeHandler = (Value, Key) -> boolean 9 | 10 | local function some(input: { [Key]: Value }, handler: SomeHandler): boolean 11 | for key, child in input do 12 | if handler(child, key) then 13 | return true 14 | end 15 | end 16 | return false 17 | end 18 | return some 19 | -------------------------------------------------------------------------------- /src/splitOn.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Splits _input_ into parts based on a _pattern_ delimiter and returns a Table of the parts, 3 | followed by a Table of the matched delimiters. 4 | ]] 5 | local insert = table.insert 6 | 7 | local function splitOn(input: string, pattern: string): { string } 8 | local parts = {} 9 | local delimiters = {} 10 | local from = 1 11 | if not pattern then 12 | for i = 1, #input do 13 | insert(parts, input:sub(i, i)) 14 | end 15 | return parts 16 | end 17 | local delimiterStart, delimiterEnd = input:find(pattern, from) 18 | while delimiterStart do 19 | insert(delimiters, input:sub(delimiterStart, delimiterEnd)) 20 | insert(parts, input:sub(from, delimiterStart - 1)) 21 | from = delimiterEnd + 1 22 | delimiterStart, delimiterEnd = input:find(pattern, from) 23 | end 24 | insert(parts, input:sub(from)) 25 | return parts, delimiters 26 | end 27 | 28 | return splitOn 29 | -------------------------------------------------------------------------------- /src/startsWith.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Checks if _input_ starts with the string _start_. 3 | @example startsWith("Fun Roblox Games", "Fun") --> true 4 | @example startsWith("Chess", "Fun") --> false 5 | ]] 6 | local function startsWith(input: string, prefix: string): boolean 7 | return input:sub(1, prefix:len()) == prefix 8 | end 9 | 10 | return startsWith 11 | -------------------------------------------------------------------------------- /src/sum.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Sums all numbers in the _input_ array. 3 | 4 | If the input array has no elements, return `0`. 5 | 6 | @example 7 | Dash.sum({3, 2, 1}) --> 6 8 | ]] 9 | local Dash = script.Parent 10 | local reduce = require(Dash.reduce) 11 | 12 | local function sum(input: { number }): number 13 | return reduce(input, function(acc, num) 14 | return acc + num 15 | end, 0) 16 | end 17 | 18 | return sum 19 | -------------------------------------------------------------------------------- /src/trim.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Remove any whitespace at the start and end of the _input_ string. 3 | ]] 4 | local function trim(input: string): string 5 | return input:match("^%s*(.-)%s*$") :: string 6 | end 7 | 8 | return trim 9 | -------------------------------------------------------------------------------- /src/values.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns an array of the values in the _input_ Map. 3 | 4 | If the input is an array, ordering is preserved. 5 | 6 | If the input is a Map, elements are returned in an arbitrary order. 7 | ]] 8 | 9 | local insert = table.insert 10 | 11 | local function values(input: { [Key]: Value }): { Value } 12 | local result: { Value } = {} 13 | for _, value in input do 14 | insert(result, value) 15 | end 16 | return result 17 | end 18 | return values 19 | -------------------------------------------------------------------------------- /src/zip.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | Returns iterator over two given arrays that produces pair of elements with the same index from both arrays 3 | 4 | Iterator stops when the shortest array ends, i.e. when it encounters nil in one of the arrays 5 | ]] 6 | local function zip(array1: { Item1 }, array2: { Item2 }): () -> (Item1, Item2) 7 | local key1, key2, item1, item2 8 | return function() 9 | key1, item1 = next(array1, key1) 10 | key2, item2 = next(array2, key2) 11 | if not item1 or not item2 then 12 | return nil 13 | end 14 | return item1, item2 15 | end 16 | end 17 | 18 | return zip 19 | -------------------------------------------------------------------------------- /stylua.toml: -------------------------------------------------------------------------------- 1 | line_endings = "Unix" 2 | -------------------------------------------------------------------------------- /suites/jest.config.lua: -------------------------------------------------------------------------------- 1 | return { 2 | displayName = "Dash", 3 | testMatch = { 4 | "**/Tests/*.spec", 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /suites/run-tests.lua: -------------------------------------------------------------------------------- 1 | local ReplicatedStorage = game:GetService("ReplicatedStorage") 2 | local processServiceExists, ProcessService = pcall(function() 3 | return game:GetService("ProcessService") 4 | end) 5 | 6 | local Packages = ReplicatedStorage:FindFirstChild("Packages") 7 | if not Packages or not Packages:FindFirstChild("Dev") then 8 | game:GetService("TestService"):Error("Invalid Package configuration. Try running `rotrieve install` to remedy.") 9 | ProcessService:ExitAsync(1) 10 | end 11 | 12 | local runCLI = require(Packages.Dev.Jest).runCLI 13 | 14 | local status, result = runCLI(Packages.Dash, {}, { Packages.Dash }):awaitStatus() 15 | 16 | if status == "Rejected" then 17 | print(result) 18 | end 19 | 20 | if status == "Resolved" and result.results.numFailedTestSuites == 0 and result.results.numFailedTests == 0 then 21 | if processServiceExists then 22 | ProcessService:ExitAsync(0) 23 | end 24 | end 25 | 26 | if processServiceExists then 27 | ProcessService:ExitAsync(1) 28 | end 29 | 30 | local statusCode = if result.results.failureCount == 0 then 0 else 1 31 | 32 | ProcessService:ExitAsync(statusCode) 33 | -------------------------------------------------------------------------------- /suites/tests.json: -------------------------------------------------------------------------------- 1 | { 2 | "run": "${configFolder}/run-tests.lua", 3 | "load": { 4 | "place": "${configFolder}/unit-test-place.rbxp", 5 | "asRobloxScript": true 6 | }, 7 | "lua": { 8 | "globals": [ 9 | "__DEV__=true" 10 | ] 11 | }, 12 | "testService": { 13 | "errorExitCode": 201 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /suites/unit-test-place.rbxp: -------------------------------------------------------------------------------- 1 | { 2 | "fileVersion": 1, 3 | "instanceHierarchy": { 4 | "name": "Dash Test Place", 5 | "tree": { 6 | "$className": "DataModel", 7 | "ReplicatedStorage": { 8 | "$className": "ReplicatedStorage", 9 | "Packages": { 10 | "$path": "../Packages", 11 | "Dash": { 12 | "$path": "../src", 13 | "jest.config": { 14 | "$path": "jest.config.lua" 15 | }, 16 | "Tests": { 17 | "$path": "../tests" 18 | } 19 | } 20 | } 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/Error.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local Error = Dash.Error 9 | 10 | local customMatchers = require(script.Parent.customMatchers) 11 | expect.extend(customMatchers) 12 | 13 | describe("Error", function() 14 | it("should correctly create and throw an error", function() 15 | expect(function() 16 | local MyError = Error.new("MyError") 17 | MyError:throw() 18 | end).toThrowWithMessage([[MyError: An error occurred]]) 19 | end) 20 | 21 | it("should throw an error with a custom message", function() 22 | expect(function() 23 | local MyError = Error.new("MyError", "An error did sadly occur") 24 | MyError:throw() 25 | end).toThrowWithMessage([[MyError: An error did sadly occur]]) 26 | end) 27 | 28 | it("should throw an error with message formatted with tag values", function() 29 | expect(function() 30 | local MyError = Error.new("MyError", "An error happened for {name} due to {reason:?}") 31 | MyError:throw({ 32 | name = "Eggbert", 33 | reason = { 34 | hunger = 10, 35 | }, 36 | }) 37 | end).toThrowWithMessage([[MyError: An error happened for Eggbert due to {hunger = 10}]]) 38 | end) 39 | 40 | it("should capture a stack trace", function() 41 | local function throwError() 42 | local MyError = Error.new("MyError") 43 | MyError:throw() 44 | end 45 | local ok, problem = pcall(throwError) 46 | expect(ok).toEqual(false) 47 | expect(typeof(problem.stack)).toEqual("string") 48 | end) 49 | end) 50 | -------------------------------------------------------------------------------- /tests/None.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local Symbol = Dash.Symbol 9 | local None = Dash.None 10 | 11 | describe("None", function() 12 | it("should be represented by a symbol", function() 13 | expect(Symbol.isInstance(None)).toBe(true) 14 | expect(tostring(None)).toEqual("Symbol(None)") 15 | end) 16 | end) 17 | -------------------------------------------------------------------------------- /tests/Symbol.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local Symbol = Dash.Symbol 9 | 10 | describe("symbol", function() 11 | it("should give a unique object", function() 12 | local CHEESE = Symbol.new("CHEESE") 13 | local FAKE_CHEESE = Symbol.new("CHEESE") 14 | local FAKER_CHEESE = { 15 | name = "CHEESE", 16 | } 17 | local FAKEST_CHEESE = Symbol.new("NOT_EVEN_CHEESE") 18 | expect(CHEESE == CHEESE).toBe(true) 19 | expect(CHEESE == FAKE_CHEESE).toBe(false) 20 | expect(CHEESE == FAKER_CHEESE).toBe(false) 21 | expect(CHEESE == FAKEST_CHEESE).toBe(false) 22 | end) 23 | 24 | it("should have a string representation", function() 25 | local CHEESE = Symbol.new("CHEESE") 26 | expect(tostring(CHEESE)).toEqual("Symbol(CHEESE)") 27 | end) 28 | 29 | it("should be a Symbol instance", function() 30 | local CHEESE = Symbol.new("CHEESE") 31 | local FAKE_CHEESE = { 32 | name = "CHEESE", 33 | } 34 | expect(Symbol.isInstance(CHEESE)).toBe(true) 35 | expect(Symbol.isInstance(FAKE_CHEESE)).toBe(false) 36 | end) 37 | end) 38 | -------------------------------------------------------------------------------- /tests/all.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local all = Dash.all 9 | 10 | describe("all", function() 11 | it("should return true if all elements match", function() 12 | local input = { aa = 5, ab = 5, ac = 5, ad = 5 } 13 | local output = all(input, function(value, key) 14 | return string.sub(key, 1, 1) == "a" and value == 5 15 | end) 16 | expect(output).toBe(true) 17 | end) 18 | 19 | it("should return false if at least one element doesn't match", function() 20 | local input = { aa = 5, ab = 5, bc = 5, ad = 5 } 21 | local output = all(input, function(value, key) 22 | return string.sub(key, 1, 1) == "a" and value == 5 23 | end) 24 | expect(output).toBe(false) 25 | end) 26 | 27 | describe("condition function is not given", function() 28 | it("should return true if all values are truthy", function() 29 | local input = { a = 5, b = 5, c = 5, d = 0 } 30 | local output = all(input) 31 | expect(output).toBe(true) 32 | end) 33 | 34 | it("should return false if at least one value is falsy", function() 35 | local input = { a = 5, b = 5, c = 5, d = false } 36 | local output = all(input) 37 | expect(output).toBe(false) 38 | end) 39 | end) 40 | end) 41 | -------------------------------------------------------------------------------- /tests/append.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local append = Dash.append 9 | local None = Dash.None 10 | 11 | describe("append", function() 12 | it("should append to an empty list", function() 13 | expect(append({}, { 1, 2, 3 }, { 4, 5, 6 })).toEqual({ 1, 2, 3, 4, 5, 6 }) 14 | end) 15 | 16 | it("should append to a list with existing values", function() 17 | expect(append({ 1, 2 }, { 3 }, { 4, 5, 6 })).toEqual({ 1, 2, 3, 4, 5, 6 }) 18 | end) 19 | 20 | it("should skip nil arguments", function() 21 | expect(append({ 1, 2 }, nil, nil, { 4, 5, 6 })).toEqual({ 1, 2, 4, 5, 6 }) 22 | end) 23 | 24 | it("should skip None arguments", function() 25 | expect(append({ 1, 2 }, None, None, { 4, 5, 6 })).toEqual({ 1, 2, 4, 5, 6 }) 26 | end) 27 | 28 | it("should share a reference between input and output", function() 29 | local input = { 1, 2 } 30 | local output = append(input, { 1, 2, 3 }, { 4, 5, 6 }) 31 | expect(input).toEqual(output) 32 | end) 33 | end) 34 | -------------------------------------------------------------------------------- /tests/assertEqual.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local assertEqual = Dash.assertEqual 9 | 10 | local customMatchers = require(script.Parent.customMatchers) 11 | expect.extend(customMatchers) 12 | 13 | describe("assertEqual", function() 14 | it("should run correctly", function() 15 | assertEqual(510, 510) 16 | expect(function() 17 | assertEqual("duck", "goose") 18 | end).toThrowWithMessage('AssertError: Left "duck" does not equal right "goose"') 19 | end) 20 | end) 21 | -------------------------------------------------------------------------------- /tests/assign.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local assign = Dash.assign 9 | local None = Dash.None 10 | 11 | describe("assign", function() 12 | it("should assign to an empty table", function() 13 | local input = {} 14 | local output = assign(input, { a = 1 }, { b = 2 }) 15 | expect(output).toEqual({ 16 | a = 1, 17 | b = 2, 18 | }) 19 | expect(input).toEqual(output) 20 | end) 21 | 22 | it("should assign to a table with existing values, overwriting existing keys", function() 23 | expect(assign({ a = 1 }, { a = 2, b = 3 }, { b = 4 })).toEqual({ 24 | a = 2, 25 | b = 4, 26 | }) 27 | end) 28 | 29 | it("should skip nil arguments", function() 30 | expect(assign({ a = 1, b = 2 }, nil, nil, { c = 3 })).toEqual({ 31 | a = 1, 32 | b = 2, 33 | c = 3, 34 | }) 35 | end) 36 | 37 | it("should skip None arguments", function() 38 | expect(assign({ a = 1, b = 2 }, None, None, { c = 3 })).toEqual({ 39 | a = 1, 40 | b = 2, 41 | c = 3, 42 | }) 43 | end) 44 | 45 | it("should remove exisintg elements when new value is None", function() 46 | expect(assign({ a = 1, b = 2 }, { b = None })).toEqual({ 47 | a = 1, 48 | b = nil, 49 | }) 50 | end) 51 | end) 52 | -------------------------------------------------------------------------------- /tests/chain.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | local jest = JestGlobals.jest 7 | 8 | local Dash = require(Packages.Dash) 9 | local chain = Dash.chain 10 | 11 | describe.only("chain", function() 12 | it("should chain iterators in the correct order", function() 13 | local function statefulIterator(list: { T }) 14 | local index = 0 15 | local count = #list 16 | 17 | return function() 18 | index = index + 1 19 | if index <= count then 20 | return list[index] 21 | end 22 | return nil 23 | end 24 | end 25 | 26 | local iterator1 = statefulIterator({ 1, 2, 3 }) 27 | local iterator2 = statefulIterator({ 4, 5, 6 }) 28 | local iterator3 = statefulIterator({ -1, -2, -3 }) 29 | 30 | local values = {} 31 | 32 | for first in chain(iterator1, iterator2, iterator3) do 33 | table.insert(values, first) 34 | end 35 | 36 | expect(values).toEqual({ 1, 2, 3, 4, 5, 6, -1, -2, -3 }) 37 | end) 38 | 39 | it("should be lazy", function() 40 | local mockFn = jest.fn() 41 | local function statefulIterator(list: { T }) 42 | local index = 0 43 | local count = #list 44 | 45 | return function() 46 | mockFn() 47 | index = index + 1 48 | if index <= count then 49 | return list[index] 50 | end 51 | return nil 52 | end 53 | end 54 | 55 | local iterator1 = statefulIterator({ 1, 2, 3 }) 56 | local iterator2 = statefulIterator({ 4, 5, 6 }) 57 | local iterator3 = statefulIterator({ -1, -2, -3 }) 58 | 59 | for value in chain(iterator1, iterator2, iterator3) do 60 | if value == 2 then 61 | break 62 | end 63 | end 64 | 65 | expect(mockFn).toHaveBeenCalledTimes(2) 66 | end) 67 | 68 | it("should return all results for multiple return functions", function() 69 | local function statefulIterator(list: { T }) 70 | local index = 0 71 | local count = #list 72 | 73 | return function() 74 | index = index + 1 75 | if index <= count then 76 | return index, list[index] 77 | end 78 | return nil 79 | end 80 | end 81 | 82 | local iterator1 = statefulIterator({ 1, 2, 3 }) 83 | local iterator2 = statefulIterator({ 4, 5, 6 }) 84 | local iterator3 = statefulIterator({ -1, -2, -3 }) 85 | 86 | local values = {} 87 | 88 | for index, value in chain(iterator1, iterator2, iterator3) do 89 | table.insert(values, { index, value }) 90 | end 91 | 92 | expect(values).toEqual({ 93 | { 1, 1 }, 94 | { 2, 2 }, 95 | { 3, 3 }, 96 | { 1, 4 }, 97 | { 2, 5 }, 98 | { 3, 6 }, 99 | { 1, -1 }, 100 | { 2, -2 }, 101 | { 3, -3 }, 102 | }) 103 | end) 104 | end) 105 | -------------------------------------------------------------------------------- /tests/class.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local class = Dash.class 9 | local format = Dash.format 10 | 11 | local customMatchers = require(script.Parent.customMatchers) 12 | expect.extend(customMatchers) 13 | 14 | describe("class", function() 15 | it("should create a class with a default constructor", function() 16 | local Vehicle = class("Vehicle") 17 | function Vehicle:getWheelCount() 18 | return 4 19 | end 20 | local car = Vehicle.new() 21 | expect(car:getWheelCount()).toEqual(4) 22 | expect(tostring(car)).toEqual("Vehicle") 23 | end) 24 | 25 | it("should create a class with a custom constructor and name", function() 26 | local Vehicle = class("Vehicle", function(name: string, wheelCount: string) 27 | return { 28 | name = name, 29 | wheelCount = wheelCount, 30 | } 31 | end) 32 | function Vehicle:getWheelCount() 33 | return self.wheelCount 34 | end 35 | local car = Vehicle.new("Car", 8) 36 | expect(car:getWheelCount()).toEqual(8) 37 | expect(tostring(car)).toEqual("Car") 38 | end) 39 | 40 | it("can use a custom toString representation", function() 41 | local Vehicle = class("Vehicle", function(name: string, wheelCount: string) 42 | return { 43 | name = name, 44 | wheelCount = wheelCount, 45 | } 46 | end) 47 | function Vehicle:toString() 48 | return format("Vehicle({name})", self) 49 | end 50 | local car = Vehicle.new("Car", 8) 51 | expect(tostring(car)).toEqual("Vehicle(Car)") 52 | end) 53 | 54 | it("can use a custom equality operator", function() 55 | local Vehicle = class("Vehicle", function(name: string, wheelCount: string) 56 | return { 57 | name = name, 58 | wheelCount = wheelCount, 59 | } 60 | end) 61 | function Vehicle:equals(other) 62 | return self.wheelCount == other.wheelCount 63 | end 64 | function Vehicle:__lt(other) 65 | return self.wheelCount < other.wheelCount 66 | end 67 | function Vehicle:__gt(other) 68 | return self.wheelCount > other.wheelCount 69 | end 70 | local car = Vehicle.new("Car1", 4) 71 | local car2 = Vehicle.new("Car2", 8) 72 | local car3 = Vehicle.new("Car3", 8) 73 | expect(car == car2).toEqual(false) 74 | expect(car == car3).toEqual(false) 75 | expect(car2 == car3).toEqual(true) 76 | expect(car < car2).toEqual(true) 77 | expect(car < car3).toEqual(true) 78 | expect(car2 < car3).toEqual(false) 79 | expect(car > car2).toEqual(false) 80 | expect(car > car3).toEqual(false) 81 | expect(car2 > car3).toEqual(false) 82 | end) 83 | 84 | it("can use a custom math operators", function() 85 | local Vehicle = class("Vehicle") 86 | function Vehicle:getWheelCount() 87 | return 4 88 | end 89 | function Vehicle:__add(other) 90 | return self:getWheelCount() + other:getWheelCount() 91 | end 92 | function Vehicle:__mul(other) 93 | return self:getWheelCount() * other:getWheelCount() 94 | end 95 | function Vehicle:__sub(other) 96 | return self:getWheelCount() - other:getWheelCount() 97 | end 98 | function Vehicle:__div(other) 99 | return self:getWheelCount() / other:getWheelCount() 100 | end 101 | function Vehicle:__mod(other) 102 | return self:getWheelCount() % other:getWheelCount() 103 | end 104 | local car = Vehicle.new() 105 | local car2 = Vehicle.new() 106 | expect(car + car2).toEqual(8) 107 | expect(car * car2).toEqual(16) 108 | expect(car - car2).toEqual(0) 109 | expect(car / car2).toEqual(1) 110 | expect(car % car2).toEqual(0) 111 | end) 112 | 113 | it("throws for operators which are not implemented", function() 114 | local Vehicle = class("Vehicle") 115 | local car = Vehicle.new() 116 | local car2 = Vehicle.new() 117 | expect(function() 118 | return car + car2 119 | end).toThrowWithMessage([[NotImplemented: The method "__add" is not implemented on the class "Vehicle"]]) 120 | expect(function() 121 | return car * car2 122 | end).toThrowWithMessage([[NotImplemented: The method "__mul" is not implemented on the class "Vehicle"]]) 123 | expect(function() 124 | return car - car2 125 | end).toThrowWithMessage([[NotImplemented: The method "__sub" is not implemented on the class "Vehicle"]]) 126 | expect(function() 127 | return car / car2 128 | end).toThrowWithMessage([[NotImplemented: The method "__div" is not implemented on the class "Vehicle"]]) 129 | expect(function() 130 | return car % car2 131 | end).toThrowWithMessage([[NotImplemented: The method "__mod" is not implemented on the class "Vehicle"]]) 132 | end) 133 | 134 | it("can extend a class with independent methods", function() 135 | local Vehicle = class("Vehicle") 136 | function Vehicle:getWheelCount() 137 | return 4 138 | end 139 | local Bike = Vehicle:extend("Bike") 140 | function Bike:getWheelCount() 141 | return 2 142 | end 143 | function Bike:drive() end 144 | local car = Vehicle.new() 145 | local bike = Bike.new() 146 | expect(tostring(car)).toEqual("Vehicle") 147 | expect(car:getWheelCount()).toEqual(4) 148 | expect(typeof(car.drive)).toEqual("nil") 149 | expect(typeof(bike.drive)).toEqual("function") 150 | expect(tostring(bike)).toEqual("Bike") 151 | expect(bike:getWheelCount()).toEqual(2) 152 | end) 153 | 154 | it("can extend a subclass with independent methods", function() 155 | local Vehicle = class("Vehicle") 156 | function Vehicle:getWheelCount() 157 | return 4 158 | end 159 | local Bike = Vehicle:extend("Bike") 160 | function Bike:getWheelCount() 161 | return 2 162 | end 163 | function Bike:getColor() 164 | return "blue" 165 | end 166 | local RedBike = Bike:extend("RedBike") 167 | function RedBike:getColor() 168 | return "red" 169 | end 170 | local bike = Bike.new() 171 | local redBike = RedBike.new() 172 | expect(tostring(bike)).toEqual("Bike") 173 | expect(bike:getWheelCount()).toEqual(2) 174 | expect(bike:getColor()).toEqual("blue") 175 | expect(tostring(redBike)).toEqual("RedBike") 176 | expect(redBike:getWheelCount()).toEqual(2) 177 | expect(redBike:getColor()).toEqual("red") 178 | end) 179 | 180 | it("can use isInstance to detect for class membership", function() 181 | local Vehicle = class("Vehicle") 182 | local Bike = Vehicle:extend("Bike") 183 | local RedBike = Bike:extend("RedBike") 184 | local bike = Bike.new() 185 | local redBike = RedBike.new() 186 | expect(RedBike.isInstance(redBike)).toEqual(true) 187 | expect(Bike.isInstance(redBike)).toEqual(true) 188 | expect(Vehicle.isInstance(redBike)).toEqual(true) 189 | expect(RedBike.isInstance(bike)).toEqual(false) 190 | end) 191 | end) 192 | -------------------------------------------------------------------------------- /tests/collect.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local collect = Dash.collect 9 | 10 | describe("collect", function() 11 | it("should collect elements of an array", function() 12 | local input = { 10, 20, 30, 50 } 13 | local output = collect(input, function(key, value) 14 | return value, key 15 | end) 16 | expect(output).toEqual({ [10] = 1, [20] = 2, [30] = 3, [50] = 4 }) 17 | end) 18 | 19 | it("should collect elements of a map", function() 20 | local input = { a = 10, b = 20, c = 30, d = 50 } 21 | local output = collect(input, function(key, value) 22 | return value + 1, key .. "!" 23 | end) 24 | expect(output).toEqual({ [11] = "a!", [21] = "b!", [31] = "c!", [51] = "d!" }) 25 | end) 26 | end) 27 | -------------------------------------------------------------------------------- /tests/collectArray.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local collectArray = Dash.collectArray 9 | local sort = table.sort 10 | 11 | describe("collectArray", function() 12 | it("should collect elements from a map into a new array", function() 13 | local input = { a = 10, b = 20, c = 30, d = 50 } 14 | local output = collectArray(input, function(key, value) 15 | return value .. "=" .. key 16 | end) 17 | sort(output) 18 | expect(output).toEqual({ "10=a", "20=b", "30=c", "50=d" }) 19 | end) 20 | end) 21 | -------------------------------------------------------------------------------- /tests/collectSet.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local collectSet = Dash.collectSet 9 | 10 | describe("collectSet", function() 11 | it("should collect values from a map into a new set using the default handler", function() 12 | local input = { a = 10, b = 20, c = 30, d = 50 } 13 | local output = collectSet(input) 14 | expect(output).toEqual({ [10] = true, [20] = true, [30] = true, [50] = true }) 15 | end) 16 | 17 | it("should collect elements from a map into a new set", function() 18 | local input = { a = 10, b = 20, c = 30, d = 50 } 19 | local output = collectSet(input, function(key, value) 20 | return value .. "=" .. key 21 | end) 22 | expect(output).toEqual({ ["10=a"] = true, ["20=b"] = true, ["30=c"] = true, ["50=d"] = true }) 23 | end) 24 | end) 25 | -------------------------------------------------------------------------------- /tests/compose.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local compose = Dash.compose 9 | 10 | describe("compose", function() 11 | it("should return identity for no inputs", function() 12 | local fn = compose() 13 | expect(fn(126)).toEqual(126) 14 | end) 15 | it("should return single function for one input", function() 16 | local fn = compose(function(input) 17 | return input + 5 18 | end) 19 | expect(fn(126)).toEqual(131) 20 | end) 21 | it("should compose functions in correct order for multiple inputs", function() 22 | local fn = compose(function(input) 23 | return input + 5 24 | end, function(input) 25 | return input * 4 26 | end, function(input) 27 | return input + 0.2 28 | end) 29 | expect(fn(5)).toEqual(40.2) 30 | end) 31 | end) 32 | -------------------------------------------------------------------------------- /tests/copy.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local copy = Dash.copy 9 | local pretty = Dash.pretty 10 | 11 | describe("copy", function() 12 | it("should make a shallow copy of a table", function() 13 | local input = { a = 10, b = 20, c = 30, reference = { 50 } } 14 | local output = copy(input) 15 | expect(input).never.toBe(output) 16 | expect(pretty(input)).toEqual(pretty(output)) 17 | expect(input.reference).toBe(output.reference) 18 | end) 19 | end) 20 | -------------------------------------------------------------------------------- /tests/count.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local count = Dash.count 9 | 10 | describe("count", function() 11 | it("should return number of elements satisfying handler condition", function() 12 | local input = { a = 5, b = 6, c = 7, d = 8 } 13 | local output = count(input, function(value, key) 14 | return key == "c" or value == 5 15 | end) 16 | expect(output).toBe(2) 17 | end) 18 | 19 | describe("without handler", function() 20 | -- TODO (AleksandrSl 03/06/2024): Add a case where #input and count will have different output 21 | it("should count all elements", function() 22 | local input = { a = 5, b = 5, c = 5, d = 5 } 23 | local output = count(input) 24 | expect(output).toBe(4) 25 | end) 26 | end) 27 | end) 28 | -------------------------------------------------------------------------------- /tests/customMatchers.lua: -------------------------------------------------------------------------------- 1 | -- Checks that toString represenation of the error includes given message 2 | local function toThrowWithMessage(_, received, errorMessage) 3 | local ok, error = pcall(received) 4 | local pass = not ok and string.find(error:toString(), errorMessage) ~= nil 5 | local message 6 | if pass then 7 | message = function() 8 | return `expected {error} not to include {errorMessage}` 9 | end 10 | else 11 | message = function() 12 | return `expected {error} to include {errorMessage}` 13 | end 14 | end 15 | return { message = message, pass = pass } 16 | end 17 | 18 | return { 19 | toThrowWithMessage = toThrowWithMessage, 20 | } 21 | -------------------------------------------------------------------------------- /tests/cycles.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local cycles = Dash.cycles 9 | 10 | describe("cycles", function() 11 | it("should return nil for a primitive value", function() 12 | local output = cycles(57) 13 | expect(output).toBeNil() 14 | end) 15 | 16 | it("should return a cycles breakdown for a non-cyclic table", function() 17 | local output = cycles({ a = 2, b = 4, c = { a = 2 } }) 18 | expect(output.nextRef).toEqual(1) 19 | expect(output.refs).toEqual({}) 20 | end) 21 | 22 | it("should return a cycles breakdown for a cyclic table", function() 23 | local tableA = { a = 2, b = 4 } 24 | local tableB = { x = 5 } 25 | local tableC = { a = 5, child = tableB } 26 | tableA.c = tableC 27 | tableC.a = tableA 28 | local output = cycles({ a = tableA, c = tableC }) 29 | expect(output.nextRef).toEqual(3) 30 | expect(output.refs[tableA]).toEqual(1) 31 | expect(output.refs[tableB]).toBeNil() 32 | expect(output.refs[tableC]).toEqual(2) 33 | end) 34 | end) 35 | -------------------------------------------------------------------------------- /tests/endsWith.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local endsWith = Dash.endsWith 9 | 10 | describe("endsWith", function() 11 | it("should correctly test the string ending", function() 12 | expect(endsWith("Roblox Games", "Games")).toEqual(true) 13 | expect(endsWith("Roblox Games", "Hard Work")).toEqual(false) 14 | expect(endsWith("Roblox Games", "A string that is longer than the input")).toEqual(false) 15 | end) 16 | end) 17 | -------------------------------------------------------------------------------- /tests/filter.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local filter = Dash.filter 9 | 10 | local sort = table.sort 11 | 12 | describe("filter", function() 13 | it("should filter elements in an array", function() 14 | local input = { 1, 2, 3, 4 } 15 | local output = filter(input, function(index, value) 16 | return index == 2 or value == 3 17 | end) 18 | expect(output).toEqual({ 2, 3 }) 19 | end) 20 | 21 | it("should filter an element in a table by key or value", function() 22 | local input = { a = 10, b = 20, c = 30, d = 50 } 23 | local output = filter(input, function(value, key) 24 | return key == "b" or value == 30 25 | end) 26 | sort(output) 27 | expect(output).toEqual({ 20, 30 }) 28 | end) 29 | end) 30 | -------------------------------------------------------------------------------- /tests/find.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local find = Dash.find 9 | 10 | describe("find", function() 11 | it("should find the first element in an array by index", function() 12 | local input = { 10, 20, 30, 40 } 13 | local output = find(input, function(_value, index) 14 | return index > 2 15 | end) 16 | expect(output).toEqual(30) 17 | end) 18 | 19 | it("should find the first element in an array by value", function() 20 | local input = { 10, 20, 30, 40 } 21 | local output = find(input, function(value) 22 | return value > 20 23 | end) 24 | expect(output).toEqual(30) 25 | end) 26 | 27 | it("should find an element in a table by key", function() 28 | local input = { a = 10, b = 20, c = 30, d = 50 } 29 | local output = find(input, function(_value, key) 30 | return key == "b" 31 | end) 32 | expect(output).toEqual(20) 33 | end) 34 | 35 | it("should find an element in a table by value", function() 36 | local input = { a = 10, b = 20, c = 30, d = 50 } 37 | local output = find(input, function(value) 38 | return value == 20 39 | end) 40 | expect(output).toEqual(20) 41 | end) 42 | 43 | it("should not find an element which does not exist", function() 44 | local input = { a = 10, b = 20, c = 30, d = 50 } 45 | local output = find(input, function(_value, key) 46 | return key == "f" 47 | end) 48 | expect(output).toBeNil() 49 | end) 50 | end) 51 | -------------------------------------------------------------------------------- /tests/findIndex.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local findIndex = Dash.findIndex 9 | 10 | describe("findIndex", function() 11 | it("should find an element in a table by index", function() 12 | local input = { 10, 20, 30, 40 } 13 | local output = findIndex(input, function(_value, index) 14 | return index == 2 15 | end) 16 | expect(output).toEqual(2) 17 | end) 18 | 19 | it("should find an element in a table by value", function() 20 | local input = { 10, 20, 30, 40 } 21 | local output = findIndex(input, function(value) 22 | return value == 20 23 | end) 24 | expect(output).toEqual(2) 25 | end) 26 | 27 | it("should not find an element which does not exist", function() 28 | local input = { 10, 20, 30, 40 } 29 | local output = findIndex(input, function(_value, index) 30 | return index == 10 31 | end) 32 | expect(output).toBeNil() 33 | end) 34 | end) 35 | -------------------------------------------------------------------------------- /tests/flat.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local flat = Dash.flat 9 | 10 | describe("flat", function() 11 | it("should flatten an array correctly", function() 12 | local input = { { 10, 20 }, { 30, 40 }, { 50, { 60 } } } 13 | local output = flat(input) 14 | expect(output).toEqual({ 15 | 10, 16 | 20, 17 | 30, 18 | 40, 19 | 50, 20 | { 21 | 60, 22 | }, 23 | }) 24 | end) 25 | end) 26 | -------------------------------------------------------------------------------- /tests/forEach.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local forEach = Dash.forEach 9 | 10 | local insert = table.insert 11 | local sort = table.sort 12 | 13 | describe("forEach", function() 14 | it("should iterate through elements of an array", function() 15 | local output = {} 16 | forEach({ 1, 2, 3, 4, 5 }, function(value, key) 17 | insert(output, key + value) 18 | end) 19 | expect(output).toEqual({ 2, 4, 6, 8, 10 }) 20 | end) 21 | 22 | it("should iterate through elements of a map", function() 23 | local output = {} 24 | forEach({ a = 1, b = 2, c = 3 }, function(value, key) 25 | insert(output, key .. "=" .. value) 26 | end) 27 | sort(output) 28 | expect(output).toEqual({ "a=1", "b=2", "c=3" }) 29 | end) 30 | end) 31 | -------------------------------------------------------------------------------- /tests/forEachArgs.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local forEachArgs = Dash.forEachArgs 9 | 10 | local insert = table.insert 11 | 12 | describe("forEachArgs", function() 13 | it("should iterate through elements of an array", function() 14 | local output = {} 15 | local function test(...) 16 | forEachArgs(function(value, key) 17 | insert(output, key + value) 18 | end, ...) 19 | end 20 | test(1, 2, 3, 4, 5) 21 | expect(output).toEqual({ 2, 4, 6, 8, 10 }) 22 | end) 23 | end) 24 | -------------------------------------------------------------------------------- /tests/format.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local format = Dash.format 9 | 10 | describe("format", function() 11 | it("should format arguments", function() 12 | expect(format("Hello {} please meet {}", "orc", "fate")).toEqual("Hello orc please meet fate") 13 | end) 14 | it("should format positional arguments", function() 15 | expect(format("Hello {2} please meet {1}", "orc", "fate")).toEqual("Hello fate please meet orc") 16 | end) 17 | it("should format named arguments", function() 18 | expect(format("Hello {name} please meet {fate}", { name = "orc", fate = "your maker" })).toEqual( 19 | "Hello orc please meet your maker" 20 | ) 21 | end) 22 | it("should format using a range of display strings", function() 23 | expect(format("Hello {} please meet {}", "orc", 25.7)).toEqual("Hello orc please meet 25.700000") 24 | expect(format("Hello {} please meet {:06X}", "orc", 255)).toEqual("Hello orc please meet 0000FF") 25 | expect(format("Hello {} please meet {:?}", "orc", { fate = "your maker" })).toEqual( 26 | 'Hello orc please meet {fate = "your maker"}' 27 | ) 28 | local longString1 = 29 | format("Hello {} please meet {:#?}", "orc", { fate = "your maker", achievements = { { made = "you" } } }) 30 | expect(longString1).toEqual([[Hello orc please meet { 31 | achievements = { 32 | { 33 | made = "you" 34 | } 35 | }, 36 | fate = "your maker" 37 | }]]) 38 | local longString2 = format( 39 | "Hello {fate:?} please meet {achievements:#?}", 40 | { fate = "your maker", achievements = { { made = "you" } } } 41 | ) 42 | expect(longString2).toEqual([[Hello "your maker" please meet { 43 | { 44 | made = "you" 45 | } 46 | }]]) 47 | end) 48 | end) 49 | -------------------------------------------------------------------------------- /tests/formatValue.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local formatValue = Dash.formatValue 9 | 10 | describe("formatValue", function() 11 | it("should provide a default format for a range of input types", function() 12 | expect(formatValue("Hello world")).toEqual("Hello world") 13 | expect(formatValue(255.0)).toEqual("255") 14 | expect(formatValue(255.3)).toEqual("255.300000") 15 | end) 16 | it("should format a range of input types using displayString", function() 17 | expect(formatValue(255, "s")).toEqual("255") 18 | expect(formatValue(255, "06X")).toEqual("0000FF") 19 | expect(formatValue({ a = 2, b = { 2, 3 } }, "?")).toEqual("{a = 2, b = {2, 3}}") 20 | expect(formatValue({ a = 2, b = { 2, 3 } }, "#?")).toEqual([[{ 21 | a = 2, 22 | b = { 23 | 2, 24 | 3 25 | } 26 | }]]) 27 | end) 28 | end) 29 | -------------------------------------------------------------------------------- /tests/freeze.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local freeze = Dash.freeze 9 | 10 | local customMatchers = require(script.Parent.customMatchers) 11 | expect.extend(customMatchers) 12 | 13 | describe("freeze", function() 14 | it("should make keys readonly", function() 15 | local output = freeze("MyObject", { 16 | name = "myName", 17 | red = 255, 18 | }) 19 | expect(function() 20 | output.red = 256 21 | end).toThrowWithMessage([[ReadonlyKey: Attempted to write to readonly key "red" of frozen object "MyObject"]]) 22 | expect(output.blah).toBeNil() 23 | end) 24 | it("should throw for missing keys if so desired", function() 25 | expect(function() 26 | local output = freeze("MyObject", { 27 | name = "myName", 28 | red = 255, 29 | }, true) 30 | return output.blue 31 | end).toThrowWithMessage([[MissingKey: Attempted to read missing key "blue" of frozen object "MyObject"]]) 32 | end) 33 | 34 | it("should print the key used to a reasonable depth", function() 35 | expect(function() 36 | local output = freeze("MyObject", { 37 | name = "myName", 38 | red = 255, 39 | }, true) 40 | 41 | local key = { name = "key", child = { name = "child", deep = { deeper = { element = 6 } } } } 42 | key.child.child = key 43 | return output[key] 44 | end).toThrowWithMessage( 45 | [[MissingKey: Attempted to read missing key <0>{child = {child = &0, deep = {deeper = ...}, name = "child"}, name = "key"} of frozen object "MyObject"]] 46 | ) 47 | end) 48 | end) 49 | -------------------------------------------------------------------------------- /tests/frequencies.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local frequencies = Dash.frequencies 9 | 10 | describe("frequencies", function() 11 | it("without handler should count encounters of all values", function() 12 | local input = { "Red", "Black", "Orange", "Red" } 13 | local output = frequencies(input) 14 | expect(output).toEqual({ ["Black"] = 1, ["Orange"] = 1, ["Red"] = 2 }) 15 | end) 16 | 17 | it("should count elements by keys generated by handler", function() 18 | local function toNeutral(color) 19 | return if Dash.includes({ "Black", "Gray", "White" }, color) then "Neutral" else "Non-neutral" 20 | end 21 | 22 | local output = Dash.frequencies({ "Black", "Orange", "Blue", "Gray", "White" }, function(color) 23 | return toNeutral(color) 24 | end) 25 | expect(output).toEqual({ ["Neutral"] = 3, ["Non-neutral"] = 2 }) 26 | end) 27 | end) 28 | -------------------------------------------------------------------------------- /tests/getOrSet.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local getOrSet = Dash.getOrSet 9 | 10 | describe("getOrSet", function() 11 | it("should return an existing value", function() 12 | local function throw() 13 | error("This should not be called") 14 | end 15 | expect(getOrSet({ a = 2 }, "a", throw)).toEqual(2) 16 | end) 17 | 18 | it("should set a new value", function() 19 | local function getValue(input, key) 20 | return input.a + key 21 | end 22 | local output = { a = 5 } 23 | expect(getOrSet(output, 8, getValue)).toEqual(13) 24 | expect(output).toEqual({ [8] = 13, a = 5 }) 25 | end) 26 | end) 27 | -------------------------------------------------------------------------------- /tests/groupBy.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local groupBy = Dash.groupBy 9 | 10 | describe("groupBy", function() 11 | it("should return an empty table for an empty grouping", function() 12 | local output = groupBy({}, function() 13 | return 5 14 | end) 15 | expect(output).toEqual({}) 16 | end) 17 | 18 | it("should return a table with a single group for aliased keys", function() 19 | local output = groupBy({ 1, 2, 3, 4, 5 }, function() 20 | return 5 21 | end) 22 | expect(output).toEqual({ 23 | [5] = { 24 | 1, 25 | 2, 26 | 3, 27 | 4, 28 | 5, 29 | }, 30 | }) 31 | end) 32 | 33 | it("should return a table with a separate groups for unique keys", function() 34 | local output = groupBy({ 1, 2, 3, 4, 5 }, function(_, key) 35 | return key 36 | end) 37 | expect(output).toEqual({ 38 | { 39 | 1, 40 | }, 41 | { 42 | 2, 43 | }, 44 | { 45 | 3, 46 | }, 47 | { 48 | 4, 49 | }, 50 | { 51 | 5, 52 | }, 53 | }) 54 | end) 55 | 56 | it("should allow groups of mixed sizes produced from combining handler arguments", function() 57 | local output = groupBy({ 5, 4, 3, 4, 5 }, function(child, key) 58 | return child + key 59 | end) 60 | expect(output).toEqual({ [6] = { 61 | 5, 62 | 4, 63 | 3, 64 | }, [8] = { 65 | 4, 66 | }, [10] = { 67 | 5, 68 | } }) 69 | end) 70 | 71 | it("should perform grouping for maps and a getKey string", function() 72 | local output = groupBy({ 73 | bike = { 74 | name = "bike", 75 | color = "blue", 76 | }, 77 | car = { 78 | name = "car", 79 | color = "red", 80 | }, 81 | van = { 82 | name = "van", 83 | color = "white", 84 | }, 85 | ghost = { 86 | name = "ghost", 87 | }, 88 | }, "color") 89 | expect(output).toEqual({ 90 | blue = { 91 | { 92 | color = "blue", 93 | name = "bike", 94 | }, 95 | }, 96 | red = { 97 | { 98 | color = "red", 99 | name = "car", 100 | }, 101 | }, 102 | white = { 103 | { 104 | color = "white", 105 | name = "van", 106 | }, 107 | }, 108 | }) 109 | end) 110 | end) 111 | -------------------------------------------------------------------------------- /tests/identity.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local identity = Dash.identity 9 | 10 | describe("identity", function() 11 | it("should return the input", function() 12 | local output = {} 13 | expect(identity(output)).toEqual(output) 14 | expect(identity(5)).toEqual(5) 15 | expect(identity(true)).toEqual(true) 16 | expect(identity("hula")).toEqual("hula") 17 | expect(identity(nil)).toBeNil() 18 | end) 19 | end) 20 | -------------------------------------------------------------------------------- /tests/includes.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | 9 | local includes = Dash.includes 10 | 11 | describe("includes", function() 12 | it("should return the expected value for arrays", function() 13 | expect(includes({ 1, 2, 3, 4, 5 }, 5)).toBe(true) 14 | expect(includes({ 1, 2, 3, 4, 5 }, 6)).toBe(false) 15 | end) 16 | 17 | it("should return the expected value for maps", function() 18 | expect(includes({ a = 1, b = 5, c = 3 }, 5)).toBe(true) 19 | expect(includes({ k = "10", l = 0, [0] = 9 }, "10")).toBe(true) 20 | expect(includes({ a = 1, b = 5, c = 3 }, 6)).toBe(false) 21 | end) 22 | 23 | it("should return false for nil", function() 24 | expect(includes({ a = nil }, nil)).toBe(false) 25 | end) 26 | end) 27 | -------------------------------------------------------------------------------- /tests/isCallable.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local isCallable = Dash.isCallable 9 | 10 | describe("isCallable", function() 11 | it("should return the expected value for a range of inputs", function() 12 | local callable = {} 13 | setmetatable(callable, { 14 | __call = function() end, 15 | }) 16 | expect(isCallable(26)).toBe(false) 17 | expect(isCallable(false)).toBe(false) 18 | expect(isCallable("hoop")).toBe(false) 19 | expect(isCallable({})).toBe(false) 20 | expect(isCallable(function() end)).toBe(true) 21 | expect(isCallable(callable)).toBe(true) 22 | end) 23 | end) 24 | -------------------------------------------------------------------------------- /tests/isLowercase.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local isLowercase = Dash.isLowercase 9 | 10 | local customMatchers = require(script.Parent.customMatchers) 11 | expect.extend(customMatchers) 12 | 13 | describe("isLowercase", function() 14 | it("should return the expected value for a range of inputs", function() 15 | local callable = {} 16 | setmetatable(callable, { 17 | __call = function() end, 18 | }) 19 | expect(isLowercase("hello")).toBe(true) 20 | expect(isLowercase("Hello")).toBe(false) 21 | -- Unfortunately not supported in the current version of Lua 22 | -- expect(isLowercase("êcho")).toBe(true) 23 | -- expect(isLowercase("Êcho")).toBe(false) 24 | end) 25 | 26 | it("ensures input is not an empty string", function() 27 | expect(function() 28 | isLowercase("") 29 | end).toThrowWithMessage("AssertError: Attempted to call Dash.isLowercase with an empty string") 30 | end) 31 | end) 32 | -------------------------------------------------------------------------------- /tests/isUppercase.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local isUppercase = Dash.isUppercase 9 | 10 | local customMatchers = require(script.Parent.customMatchers) 11 | expect.extend(customMatchers) 12 | 13 | describe("isUppercase", function() 14 | it("should return the expected value for a range of inputs", function() 15 | local callable = {} 16 | setmetatable(callable, { 17 | __call = function() end, 18 | }) 19 | expect(isUppercase("hello")).toBe(false) 20 | expect(isUppercase("Hello")).toBe(true) 21 | -- Unfortunately not supported in the current version of Lua 22 | -- expect(isUppercase("êcho")).toBe(false) 23 | -- expect(isUppercase("Êcho")).toBe(true) 24 | end) 25 | 26 | it("ensures input is not an empty string", function() 27 | expect(function() 28 | isUppercase("") 29 | end).toThrowWithMessage("AssertError: Attempted to call Dash.isUppercase with an empty string") 30 | end) 31 | end) 32 | -------------------------------------------------------------------------------- /tests/iterator.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local iterable = Dash.iterable 9 | local iterator = Dash.iterator 10 | local slice = Dash.slice 11 | 12 | local insert = table.insert 13 | local sort = table.sort 14 | 15 | describe("iterator", function() 16 | it("iterates through an array in order", function() 17 | local output = {} 18 | for key, value in iterator({ 10, 20, 30, 40, 50 }) do 19 | insert(output, key .. "=" .. value) 20 | end 21 | expect(output).toEqual({ 22 | "1=10", 23 | "2=20", 24 | "3=30", 25 | "4=40", 26 | "5=50", 27 | }) 28 | end) 29 | 30 | it("iterates through a map in an arbitrary order", function() 31 | local output = {} 32 | for key, value in iterator({ a = 1, b = 2, c = 3, d = 4, e = 5 }) do 33 | insert(output, key .. "=" .. value) 34 | end 35 | sort(output) 36 | expect(output).toEqual({ 37 | "a=1", 38 | "b=2", 39 | "c=3", 40 | "d=4", 41 | "e=5", 42 | }) 43 | end) 44 | 45 | it("only discovers ordered keys as ipairs assumed", function() 46 | local output = {} 47 | for key, value in iterator({ [1] = 10, [2] = 20, [3] = 30, [5] = 50, a = 1, b = 2, c = 3, d = 4, e = 5 }) do 48 | insert(output, key .. "=" .. value) 49 | end 50 | sort(output) 51 | expect(output).toEqual({ 52 | "1=10", 53 | "2=20", 54 | "3=30", 55 | }) 56 | end) 57 | 58 | it("discovers unordered keys if forced", function() 59 | local input = iterable({ [1] = 10, [2] = 20, [3] = 30, [5] = 50, a = 1, b = 2, c = 3, d = 4, e = 5 }) 60 | local output = {} 61 | for key, value in iterator(input) do 62 | insert(output, key .. "=" .. value) 63 | end 64 | expect(slice(output, 1, 3)).toEqual({ 65 | "1=10", 66 | "2=20", 67 | "3=30", 68 | }) 69 | sort(output) 70 | expect(output).toEqual({ 71 | "1=10", 72 | "2=20", 73 | "3=30", 74 | "5=50", 75 | "a=1", 76 | "b=2", 77 | "c=3", 78 | "d=4", 79 | "e=5", 80 | }) 81 | end) 82 | end) 83 | -------------------------------------------------------------------------------- /tests/join.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local join = Dash.join 9 | local None = Dash.None 10 | 11 | describe("join", function() 12 | it("should join a tables together", function() 13 | expect(join({ c = 4 }, { a = 1 }, { b = 2, c = None })).toEqual({ 14 | a = 1, 15 | b = 2, 16 | }) 17 | end) 18 | 19 | it("should skip nil arguments", function() 20 | expect(join({ a = 1, b = 2 }, nil, nil, { c = 3 })).toEqual({ 21 | a = 1, 22 | b = 2, 23 | c = 3, 24 | }) 25 | end) 26 | 27 | it("should skip None arguments", function() 28 | expect(join({ a = 1, b = 2 }, None, None, { c = 3 })).toEqual({ 29 | a = 1, 30 | b = 2, 31 | c = 3, 32 | }) 33 | end) 34 | 35 | it("should not mutate the original input", function() 36 | local input = { a = 1, b = 2 } 37 | expect(join(input, { b = 4 }, { c = 3 })).toEqual({ 38 | a = 1, 39 | b = 4, 40 | c = 3, 41 | }) 42 | expect(input).toEqual({ 43 | a = 1, 44 | b = 2, 45 | }) 46 | end) 47 | end) 48 | -------------------------------------------------------------------------------- /tests/joinDeep.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local joinDeep = Dash.joinDeep 9 | local None = Dash.None 10 | 11 | describe("joinDeep", function() 12 | it("should join two flat tables", function() 13 | expect(joinDeep({ a = 1, c = 4 }, { a = 3, b = 2 })).toEqual({ 14 | a = 3, 15 | b = 2, 16 | c = 4, 17 | }) 18 | end) 19 | 20 | it("should deeply join nested tables", function() 21 | local source = { 22 | name = "car", 23 | lights = { 24 | front = 2, 25 | back = 2, 26 | indicators = { 27 | color = "orange", 28 | }, 29 | brake = { 30 | color = "red", 31 | }, 32 | }, 33 | tyres = 4, 34 | } 35 | local delta = { 36 | name = "bike", 37 | lights = { 38 | front = 3, 39 | indicators = { 40 | rate = 20, 41 | }, 42 | }, 43 | tyres = None, 44 | } 45 | expect(joinDeep(source, delta)).toEqual({ 46 | lights = { 47 | back = 2, 48 | brake = { 49 | color = "red", 50 | }, 51 | front = 3, 52 | indicators = { 53 | color = "orange", 54 | rate = 20, 55 | }, 56 | }, 57 | name = "bike", 58 | }) 59 | end) 60 | 61 | it("should not mutate the original input", function() 62 | local input = { a = 1, b = 2 } 63 | expect(joinDeep(input, { a = 3, b = 2 })).toEqual({ 64 | a = 3, 65 | b = 2, 66 | }) 67 | expect(input).toEqual({ 68 | a = 1, 69 | b = 2, 70 | }) 71 | end) 72 | 73 | it("should not clone deep tables that don't change", function() 74 | local source = { 75 | name = "car", 76 | lights = { 77 | front = 2, 78 | back = 2, 79 | indicators = { 80 | color = "orange", 81 | }, 82 | brake = { 83 | color = "red", 84 | }, 85 | }, 86 | } 87 | local delta = { 88 | name = "bike", 89 | lights = { 90 | front = 3, 91 | indicators = { 92 | rate = 20, 93 | }, 94 | }, 95 | tyres = None, 96 | } 97 | local result = joinDeep(source, delta) 98 | expect(result.lights.brake == source.lights.brake).toBe(true) 99 | end) 100 | end) 101 | -------------------------------------------------------------------------------- /tests/keyBy.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local keyBy = Dash.keyBy 9 | 10 | describe("keyBy", function() 11 | it("should return an empty table for an empty grouping", function() 12 | local output = keyBy({}, function() 13 | return 5 14 | end) 15 | expect(output).toEqual({}) 16 | end) 17 | 18 | it("should return the most recent value for aliased keys", function() 19 | local output = keyBy({ 1, 2, 3, 4, 5 }, function(_, index) 20 | return index % 2 21 | end) 22 | expect(output).toEqual({ 23 | [1] = 5, 24 | [0] = 4, 25 | }) 26 | end) 27 | 28 | it("should return a table with a separate entries for unique keys", function() 29 | local output = keyBy({ 1, 2, 3, 4, 5 }, function(_, key) 30 | return key 31 | end) 32 | expect(output).toEqual({ 33 | 1, 34 | 2, 35 | 3, 36 | 4, 37 | 5, 38 | }) 39 | end) 40 | 41 | it("should perform assignment for maps and a getKey string", function() 42 | local output = keyBy({ 43 | bike = { 44 | name = "bike", 45 | color = "blue", 46 | }, 47 | car = { 48 | name = "car", 49 | color = "red", 50 | }, 51 | van = { 52 | name = "van", 53 | color = "white", 54 | }, 55 | ghost = { 56 | name = "ghost", 57 | }, 58 | }, "color") 59 | expect(output).toEqual({ 60 | blue = { 61 | color = "blue", 62 | name = "bike", 63 | }, 64 | red = { 65 | color = "red", 66 | name = "car", 67 | }, 68 | white = { 69 | color = "white", 70 | name = "van", 71 | }, 72 | }) 73 | end) 74 | end) 75 | -------------------------------------------------------------------------------- /tests/keys.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local keys = Dash.keys 9 | 10 | local sort = table.sort 11 | 12 | describe("keys", function() 13 | it("should return an empty table for an empty map", function() 14 | local output = keys({}) 15 | expect(output).toEqual({}) 16 | end) 17 | 18 | it("should return a list of the keys of an array", function() 19 | local output = keys({ 10, 20, 30, 40, 50 }) 20 | sort(output) 21 | expect(output).toEqual({ 22 | 1, 23 | 2, 24 | 3, 25 | 4, 26 | 5, 27 | }) 28 | end) 29 | 30 | it("should return a list of the keys of a map", function() 31 | local output = keys({ a = 10, b = 20, c = 30, d = 40, e = 50 }) 32 | sort(output) 33 | expect(output).toEqual({ 34 | "a", 35 | "b", 36 | "c", 37 | "d", 38 | "e", 39 | }) 40 | end) 41 | end) 42 | -------------------------------------------------------------------------------- /tests/last.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local last = Dash.last 9 | 10 | describe("last", function() 11 | it("should find the last element if no handler is provided", function() 12 | local input = { 10, 20, 30, 40 } 13 | local output = last(input) 14 | expect(output).toEqual(40) 15 | end) 16 | 17 | it("should find an element in a table by index", function() 18 | local input = { 10, 20, 30, 40 } 19 | local output = last(input, function(_value, index) 20 | return index < 3 21 | end) 22 | expect(output).toEqual(20) 23 | end) 24 | 25 | it("should find the last element in a table by value", function() 26 | local input = { 10, 20, 30, 40 } 27 | local output = last(input, function(value) 28 | return value < 30 29 | end) 30 | expect(output).toEqual(20) 31 | end) 32 | 33 | it("should not find an element which does not exist", function() 34 | local input = { 10, 20, 30, 40 } 35 | local output = last(input, function(_value, index) 36 | return index == 10 37 | end) 38 | expect(output).toBeNil() 39 | end) 40 | end) 41 | -------------------------------------------------------------------------------- /tests/leftPad.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local leftPad = Dash.leftPad 9 | 10 | describe("leftPad", function() 11 | it("should run correctly for a range of inputs", function() 12 | expect(leftPad("toast", 6)).toEqual(" toast") 13 | expect(leftPad("2", 2, "0")).toEqual("02") 14 | expect(leftPad("toast", 10, ":)")).toEqual(":):):toast") 15 | end) 16 | end) 17 | -------------------------------------------------------------------------------- /tests/map.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local map = Dash.map 9 | 10 | describe("map", function() 11 | it("should map elements into a new array", function() 12 | local input = { 10, 20, 30, 50 } 13 | local output = map(input, function(value, key) 14 | return key .. "=" .. value 15 | end) 16 | expect(output).toEqual({ 17 | "1=10", 18 | "2=20", 19 | "3=30", 20 | "4=50", 21 | }) 22 | end) 23 | 24 | it("should map elements into a new map", function() 25 | local input = { a = 10, b = 20, c = 30, d = 50 } 26 | local output = map(input, function(value, key) 27 | return key .. "=" .. value 28 | end) 29 | expect(output).toEqual({ 30 | a = "a=10", 31 | b = "b=20", 32 | c = "c=30", 33 | d = "d=50", 34 | }) 35 | end) 36 | end) 37 | -------------------------------------------------------------------------------- /tests/mapFirst.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local mapFirst = Dash.mapFirst 9 | 10 | describe("mapFirst", function() 11 | it("should return the first element", function() 12 | local input = { 12, 13, 14, 15, 16 } 13 | local output = mapFirst(input, function(value, _) 14 | if value > 13 then 15 | return value * 10 16 | else 17 | return nil 18 | end 19 | end) 20 | expect(output).toEqual(140) 21 | end) 22 | end) 23 | -------------------------------------------------------------------------------- /tests/mapLast.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local mapLast = Dash.mapLast 9 | 10 | describe("mapLast", function() 11 | it("should return the first element that the handler does not return nil for", function() 12 | local input = { 12, 13, 14, 15, 16 } 13 | local output = mapLast(input, function(value, _) 14 | if value < 14 then 15 | return value * 10 16 | else 17 | return nil 18 | end 19 | end) 20 | expect(output).toEqual(130) 21 | end) 22 | end) 23 | -------------------------------------------------------------------------------- /tests/mapOne.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local mapOne = Dash.mapOne 9 | 10 | describe("mapOne", function() 11 | it("should return an element that the handler does not return nil for", function() 12 | local input = { a = 5, b = 6, c = 7, d = 8 } 13 | local output = mapOne(input, function(value, key) 14 | if value == 6 then 15 | return key 16 | else 17 | return nil 18 | end 19 | end) 20 | expect(output).toEqual("b") 21 | end) 22 | 23 | it("should return any element if the handler is not defined", function() 24 | local input = { a = 6 } 25 | local output = mapOne(input) 26 | expect(output).toEqual(6) 27 | end) 28 | end) 29 | -------------------------------------------------------------------------------- /tests/max.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local max = Dash.max 9 | 10 | describe("max", function() 11 | describe("with default handler and comparator", function() 12 | it("should get maximal element", function() 13 | local input = { 20, 50, -20, 30, 10 } 14 | local output = max(input) 15 | expect(output).toEqual(50) 16 | end) 17 | end) 18 | 19 | describe("with custom comparator and default handler", function() 20 | it("should get maximal element", function() 21 | -- Yes, pun intended 22 | local function customComparator(a, b) 23 | return a < b 24 | end 25 | 26 | local input = { 20, 50, -20, 30, 10 } 27 | local output = max(input, nil, customComparator) 28 | expect(output).toEqual(-20) 29 | end) 30 | end) 31 | 32 | describe("with custom comparator and custom handler", function() 33 | it("should get maximal element", function() 34 | -- Yes, pun intended 35 | local function customComparator(a, b) 36 | return a < b 37 | end 38 | local function customHandler(a) 39 | return -a 40 | end 41 | 42 | local input = { 20, 50, -20, 30, 10 } 43 | local output = max(input, customHandler, customComparator) 44 | expect(output).toEqual(50) 45 | end) 46 | end) 47 | 48 | describe("with default comparator and custom handler", function() 49 | it("should get maximal element", function() 50 | local function customHandler(a) 51 | return -a 52 | end 53 | 54 | local input = { 20, 50, -20, 30, 10 } 55 | local output = max(input, customHandler) 56 | expect(output).toEqual(-20) 57 | end) 58 | end) 59 | 60 | it("should return nil if table is empty", function() 61 | local input = {} 62 | local output = max(input) 63 | expect(output).toEqual(nil) 64 | end) 65 | end) 66 | -------------------------------------------------------------------------------- /tests/min.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local min = Dash.min 9 | 10 | describe("min", function() 11 | describe("with default handler and comparator", function() 12 | it("should get minimal element", function() 13 | local input = { 20, 50, -20, 30, 10 } 14 | local output = min(input) 15 | expect(output).toEqual(-20) 16 | end) 17 | end) 18 | 19 | describe("with custom comparator and default handler", function() 20 | it("should get minimal element", function() 21 | -- Yes, pun intended 22 | local function customComparator(a, b) 23 | return a > b 24 | end 25 | 26 | local input = { 20, 50, -20, 30, 10 } 27 | local output = min(input, nil, customComparator) 28 | expect(output).toEqual(50) 29 | end) 30 | end) 31 | 32 | describe("with custom comparator and custom handler", function() 33 | it("should get minimal element", function() 34 | -- Yes, pun intended 35 | local function customComparator(a, b) 36 | return a > b 37 | end 38 | local function customHandler(a) 39 | return -a 40 | end 41 | 42 | local input = { 20, 50, -20, 30, 10 } 43 | local output = min(input, customHandler, customComparator) 44 | expect(output).toEqual(-20) 45 | end) 46 | end) 47 | 48 | describe("with default comparator and custom handler", function() 49 | it("should get minimal element", function() 50 | local function customHandler(a) 51 | return -a 52 | end 53 | 54 | local input = { 20, 50, -20, 30, 10 } 55 | local output = min(input, customHandler) 56 | expect(output).toEqual(50) 57 | end) 58 | end) 59 | 60 | it("should return nil if table is empty", function() 61 | local input = {} 62 | local output = min(input) 63 | expect(output).toEqual(nil) 64 | end) 65 | end) 66 | -------------------------------------------------------------------------------- /tests/noop.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local noop = Dash.noop 9 | 10 | describe("noop", function() 11 | it("should run without error and return nil", function() 12 | expect(noop()).toBeNil() 13 | end) 14 | end) 15 | -------------------------------------------------------------------------------- /tests/omit.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local omit = Dash.omit 9 | 10 | describe("omit", function() 11 | it("should omit entries from a map based on key", function() 12 | local input = { a = 10, b = 20, c = 30, d = 50 } 13 | local output = omit(input, { "a", "b" }) 14 | expect(output).toEqual({ c = 30, d = 50 }) 15 | end) 16 | end) 17 | -------------------------------------------------------------------------------- /tests/pick.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local pick = Dash.pick 9 | 10 | describe("pick", function() 11 | it("should pick elements in a map by key or value", function() 12 | local input = { a = 10, b = 20, c = 30, d = 50 } 13 | local output = pick(input, function(value, key) 14 | return key == "b" or value == 30 15 | end) 16 | expect(output).toEqual({ b = 20, c = 30 }) 17 | end) 18 | end) 19 | -------------------------------------------------------------------------------- /tests/pretty.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local pretty = Dash.pretty 9 | 10 | describe("pretty", function() 11 | it("should print primitive values", function() 12 | expect(pretty("stringy")).toEqual([["stringy"]]) 13 | expect(pretty(15)).toEqual([[15]]) 14 | expect(pretty(true)).toEqual([[true]]) 15 | local function myFunction() end 16 | expect(pretty(myFunction)).toEqual(tostring(myFunction)) 17 | end) 18 | 19 | it("should print a map with string keys", function() 20 | local input = { a = 10, b = 20, c = 30, d = 50 } 21 | local output = pretty(input) 22 | expect(output).toEqual([[{a = 10, b = 20, c = 30, d = 50}]]) 23 | end) 24 | 25 | it("should print an array", function() 26 | local input = { 10, 20, 30, 50 } 27 | local output = pretty(input) 28 | expect(output).toEqual([[{10, 20, 30, 50}]]) 29 | end) 30 | end) 31 | -------------------------------------------------------------------------------- /tests/product.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local product = Dash.product 9 | 10 | describe("product", function() 11 | it("should return product of all array elements", function() 12 | local input = { 2, 3, 7 } 13 | local output = product(input) 14 | expect(output).toEqual(42) 15 | end) 16 | 17 | it("should return 1 if array is empty", function() 18 | local input = {} 19 | local output = product(input) 20 | expect(output).toEqual(1) 21 | end) 22 | end) 23 | -------------------------------------------------------------------------------- /tests/reduce.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local reduce = Dash.reduce 9 | 10 | describe("reduce", function() 11 | it("should reduce entries from an array using the handler and value", function() 12 | local output = reduce({ 10, 20, 30, 40 }, function(a, b) 13 | return a + b 14 | end, 0) 15 | expect(output).toEqual(100) 16 | end) 17 | 18 | it("should reduce entries from an array using the handler and key", function() 19 | local output = reduce({ 10, 20, 30, 40 }, function(acc, _, key) 20 | return acc .. key .. "," 21 | end, "") 22 | expect(output).toEqual("1,2,3,4,") 23 | end) 24 | end) 25 | -------------------------------------------------------------------------------- /tests/reverse.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local reverse = Dash.reverse 9 | 10 | describe("reverse", function() 11 | it("should reverse the order of an array without modifying the input", function() 12 | local input = { 10, 20, 30, 40 } 13 | local output = reverse(input) 14 | expect(input).toEqual({ 10, 20, 30, 40 }) 15 | expect(output).toEqual({ 40, 30, 20, 10 }) 16 | end) 17 | end) 18 | -------------------------------------------------------------------------------- /tests/rightPad.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local rightPad = Dash.rightPad 9 | 10 | describe("rightPad", function() 11 | it("should run correctly for a range of inputs", function() 12 | expect(rightPad("toast", 6)).toEqual("toast ") 13 | expect(rightPad("2", 2, "!")).toEqual("2!") 14 | expect(rightPad("toast", 10, ":)")).toEqual("toast:):):") 15 | end) 16 | end) 17 | -------------------------------------------------------------------------------- /tests/shallowEqual.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local shallowEqual = Dash.shallowEqual 9 | 10 | describe("shallowEqual", function() 11 | it("should run correctly for primitive inputs", function() 12 | expect(shallowEqual("toast", "toast")).toBe(true) 13 | expect(shallowEqual("toast", "bread")).toBe(false) 14 | expect(shallowEqual(5, 5)).toBe(true) 15 | expect(shallowEqual(5, 50)).toBe(false) 16 | expect(shallowEqual("toast", 5)).toBe(false) 17 | expect(shallowEqual(5, "5")).toBe(false) 18 | expect(shallowEqual(5, nil)).toBe(false) 19 | expect(shallowEqual(nil, nil)).toBe(true) 20 | end) 21 | it("should run correctly for one table input", function() 22 | expect(shallowEqual({}, "toast")).toBe(false) 23 | expect(shallowEqual("toast", {})).toBe(false) 24 | expect(shallowEqual({}, nil)).toBe(false) 25 | expect(shallowEqual(nil, {})).toBe(false) 26 | end) 27 | it("should run correctly for arrays", function() 28 | expect(shallowEqual({}, {})).toBe(true) 29 | expect(shallowEqual({}, { 1, 2, 3 })).toBe(false) 30 | expect(shallowEqual({ 1, 2, 3 }, { 1, 2, 3 })).toBe(true) 31 | end) 32 | it("should run correctly for maps", function() 33 | expect(shallowEqual({ x = 3 }, { x = 3 })).toBe(true) 34 | expect(shallowEqual({ x = 3 }, { x = 3, y = 4 })).toBe(false) 35 | expect(shallowEqual({ x = 3, y = 4 }, { x = 3, y = 4000 })).toBe(false) 36 | expect(shallowEqual({ x = 3, y = 4000 }, { x = 3 })).toBe(false) 37 | local child = { d = 9 } 38 | expect(shallowEqual({ x = 3, y = child }, { x = 3, y = child })).toBe(true) 39 | expect(shallowEqual({ x = 3, y = child }, { x = 3, y = { d = 9 } })).toBe(false) 40 | end) 41 | end) 42 | -------------------------------------------------------------------------------- /tests/slice.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local slice = Dash.slice 9 | 10 | describe("slice", function() 11 | it("should return a copy of an array with no index arguments", function() 12 | local output = slice({ 1, 2, 3, 4, 5 }) 13 | expect(output).toEqual({ 1, 2, 3, 4, 5 }) 14 | end) 15 | it("should return a slice with positive left index", function() 16 | local output = slice({ 1, 2, 3, 4, 5 }, 2) 17 | expect(output).toEqual({ 2, 3, 4, 5 }) 18 | end) 19 | it("should return a slice with positive indices", function() 20 | local output = slice({ 1, 2, 3, 4, 5 }, 2, 4) 21 | expect(output).toEqual({ 2, 3, 4 }) 22 | end) 23 | it("should return a slice with negative left index", function() 24 | local output = slice({ 1, 2, 3, 4, 5 }, -2) 25 | expect(output).toEqual({ 3, 4, 5 }) 26 | end) 27 | it("should return a slice with negative right index", function() 28 | local output = slice({ 1, 2, 3, 4, 5 }, 2, -1) 29 | expect(output).toEqual({ 2, 3, 4 }) 30 | end) 31 | it("should return a slice with negative indices", function() 32 | local output = slice({ 1, 2, 3, 4, 5 }, -3, -1) 33 | expect(output).toEqual({ 2, 3, 4 }) 34 | end) 35 | it("should return a slice with positive left & negative right indices", function() 36 | local output = slice({ 1, 2, 3, 4, 5 }, 2, -1) 37 | expect(output).toEqual({ 2, 3, 4 }) 38 | end) 39 | it("should return a slice with negative left & positive right indices", function() 40 | local output = slice({ 1, 2, 3, 4, 5 }, -3, 4) 41 | expect(output).toEqual({ 2, 3, 4 }) 42 | end) 43 | it("should return an empty slice for invalid positive indices", function() 44 | local output = slice({ 1, 2, 3, 4, 5 }, 100, 1000) 45 | expect(output).toEqual({}) 46 | end) 47 | it("should return an empty slice for invalid negative indices", function() 48 | local output = slice({ 1, 2, 3, 4, 5 }, -100, -1000) 49 | expect(output).toEqual({}) 50 | end) 51 | it("should return an empty slice for swapped indices", function() 52 | local output = slice({ 1, 2, 3, 4, 5 }, 4, 3) 53 | expect(output).toEqual({}) 54 | end) 55 | end) 56 | -------------------------------------------------------------------------------- /tests/some.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local some = Dash.some 9 | 10 | describe("some", function() 11 | it("should return true if an element matches", function() 12 | local input = { a = 5, b = 6, c = 7, d = 8 } 13 | local output = some(input, function(value, key) 14 | return key == "c" or value == 3000 15 | end) 16 | expect(output).toBe(true) 17 | end) 18 | 19 | it("should return false if no element matches", function() 20 | local input = { a = 5, b = 6, c = 7, d = 8 } 21 | local output = some(input, function(_, key) 22 | return key == "No such key" 23 | end) 24 | expect(output).toBe(false) 25 | end) 26 | end) 27 | -------------------------------------------------------------------------------- /tests/splitOn.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local splitOn = Dash.splitOn 9 | describe("splitOn", function() 10 | it("should return a array with a single element for an empty string", function() 11 | local output = splitOn("", "[.:()]") 12 | expect(output).toEqual({ "" }) 13 | end) 14 | it("should split a string based on a pattern", function() 15 | local output = splitOn("zombie.Head.Part:Connect()", "[.:()]") 16 | expect(output).toEqual({ "zombie", "Head", "Part", "Connect", "", "" }) 17 | end) 18 | end) 19 | -------------------------------------------------------------------------------- /tests/startsWith.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local startsWith = Dash.startsWith 9 | 10 | describe("startsWith", function() 11 | it("should correctly test the string ending", function() 12 | expect(startsWith("Fun Roblox Games", "Fun")).toBe(true) 13 | expect(startsWith("Card Games", "Fun")).toBe(false) 14 | expect(startsWith("Roblox Games", "A string that is longer than the input")).toBe(false) 15 | end) 16 | end) 17 | -------------------------------------------------------------------------------- /tests/sum.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local sum = Dash.sum 9 | 10 | describe("sum", function() 11 | it("should return sum of all array elements", function() 12 | local input = { -10, 10, 0 } 13 | local output = sum(input) 14 | expect(output).toEqual(0) 15 | end) 16 | 17 | it("should return 0 if array is empty", function() 18 | local input = {} 19 | local output = sum(input) 20 | expect(output).toEqual(0) 21 | end) 22 | end) 23 | -------------------------------------------------------------------------------- /tests/trim.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local trim = Dash.trim 9 | 10 | describe("trim", function() 11 | it("can remove whitespace from before and after the end of a string", function() 12 | expect(trim(" space at the start")).toEqual("space at the start") 13 | expect(trim("spaces at the end ")).toEqual("spaces at the end") 14 | expect(trim(" spaces both sides ")).toEqual("spaces both sides") 15 | expect(trim(" mixed tabs and spaces ")).toEqual("mixed tabs and spaces") 16 | expect(trim(" !@#$%^&*()_-+=`~[]{}//\\ ")).toEqual("!@#$%^&*()_-+=`~[]{}//\\") 17 | end) 18 | end) 19 | -------------------------------------------------------------------------------- /tests/values.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local values = Dash.values 9 | 10 | local sort = table.sort 11 | 12 | describe("values", function() 13 | it("should return an empty table for an empty map", function() 14 | local output = values({}) 15 | expect(output).toEqual({}) 16 | end) 17 | 18 | it("should return a list of the values of an array", function() 19 | local output = values({ 10, 30, 20, 40, 50 }) 20 | expect(output).toEqual({ 10, 30, 20, 40, 50 }) 21 | end) 22 | 23 | it("should return a list of the values of a map", function() 24 | local output = values({ a = 10, b = 20, c = 30, d = 40, e = 50 }) 25 | sort(output) 26 | expect(output).toEqual({ 10, 20, 30, 40, 50 }) 27 | end) 28 | end) 29 | -------------------------------------------------------------------------------- /tests/zip.spec.lua: -------------------------------------------------------------------------------- 1 | local Packages = game:GetService("ReplicatedStorage").Packages 2 | local JestGlobals = require(Packages.Dev.JestGlobals) 3 | local describe = JestGlobals.describe 4 | local it = JestGlobals.it 5 | local expect = JestGlobals.expect 6 | 7 | local Dash = require(Packages.Dash) 8 | local zip = Dash.zip 9 | 10 | describe("zip", function() 11 | it("should return pairs of the items with the same index from given arrays", function() 12 | local output = {} 13 | for item1, item2 in zip({ 1, 2, 3 }, { 4, 5, 6 }) do 14 | table.insert(output, { item1, item2 }) 15 | end 16 | expect(output).toEqual({ { 1, 4 }, { 2, 5 }, { 3, 6 } }) 17 | end) 18 | 19 | it("should return elements until the shortest array ends", function() 20 | local output = {} 21 | for item1, item2 in zip({ 1, 2, 3, 10, 78 }, { 4, 5, 6 }) do 22 | table.insert(output, { item1, item2 }) 23 | end 24 | expect(output).toEqual({ { 1, 4 }, { 2, 5 }, { 3, 6 } }) 25 | 26 | local output2 = {} 27 | for item1, item2 in zip({ 1, 2, 3 }, { 4, 5, 6, 10, 78 }) do 28 | table.insert(output2, { item1, item2 }) 29 | end 30 | expect(output2).toEqual({ { 1, 4 }, { 2, 5 }, { 3, 6 } }) 31 | end) 32 | end) 33 | --------------------------------------------------------------------------------