add gs
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"language": {
|
||||
"mode": "nonstrict"
|
||||
},
|
||||
"lint": {
|
||||
"ImplicitReturn": "fatal"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
--[[
|
||||
A helper function to define a Rodux action creator with an associated name.
|
||||
|
||||
Normally when creating a Rodux action, you can just create a function:
|
||||
|
||||
return function(value)
|
||||
return {
|
||||
type = "MyAction",
|
||||
value = value,
|
||||
}
|
||||
end
|
||||
|
||||
And then when you check for it in your reducer, you either use a constant,
|
||||
or type out the string name:
|
||||
|
||||
if action.type == "MyAction" then
|
||||
-- change some state
|
||||
end
|
||||
|
||||
Typos here are a remarkably common bug. We also have the issue that there's
|
||||
no link between reducers and the actions that they respond to!
|
||||
|
||||
`Action` (this helper) provides a utility that makes this a bit cleaner.
|
||||
|
||||
Instead, define your Rodux action like this:
|
||||
|
||||
return Action("MyAction", function(value)
|
||||
return {
|
||||
value = value,
|
||||
}
|
||||
end)
|
||||
|
||||
We no longer need to add the `type` field manually.
|
||||
|
||||
Additionally, the returned action creator now has a 'name' property that can
|
||||
be checked by your reducer:
|
||||
|
||||
local MyAction = require(Reducers.MyAction)
|
||||
|
||||
...
|
||||
|
||||
if action.type == MyAction.name then
|
||||
-- change some state!
|
||||
end
|
||||
|
||||
Now we have a clear link between our reducers and the actions they use, and
|
||||
if we ever typo a name, we'll get a warning in LuaCheck as well as an error
|
||||
at runtime!
|
||||
]]
|
||||
|
||||
return function(name, fn)
|
||||
assert(type(name) == "string", "A name must be provided to create an Action")
|
||||
assert(type(fn) == "function", "A function must be provided to create an Action")
|
||||
|
||||
return setmetatable({
|
||||
name = name,
|
||||
}, {
|
||||
__call = function(self, ...)
|
||||
local result = fn(...)
|
||||
|
||||
assert(type(result) == "table", "An action must return a table")
|
||||
|
||||
result.type = name
|
||||
|
||||
return result
|
||||
end
|
||||
})
|
||||
end
|
||||
@@ -0,0 +1,85 @@
|
||||
return function()
|
||||
local Action = require(script.Parent.Action)
|
||||
|
||||
it("should return a table", function()
|
||||
local action = Action("foo", function()
|
||||
return {}
|
||||
end)
|
||||
|
||||
expect(action).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the name of the action", function()
|
||||
local action = Action("foo", function()
|
||||
return {}
|
||||
end)
|
||||
|
||||
expect(action.name).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should be able to be called as a function", function()
|
||||
local action = Action("foo", function()
|
||||
return {}
|
||||
end)
|
||||
|
||||
expect(action).never.to.throw()
|
||||
end)
|
||||
|
||||
it("should return a table when called as a function", function()
|
||||
local action = Action("foo", function()
|
||||
return {}
|
||||
end)
|
||||
|
||||
expect(action()).to.be.a("table")
|
||||
end)
|
||||
|
||||
it("should set the type of the action", function()
|
||||
local action = Action("foo", function()
|
||||
return {}
|
||||
end)
|
||||
|
||||
expect(action().type).to.equal("foo")
|
||||
end)
|
||||
|
||||
it("should set values", function()
|
||||
local action = Action("foo", function(value)
|
||||
return {
|
||||
value = value
|
||||
}
|
||||
end)
|
||||
|
||||
expect(action(100).value).to.equal(100)
|
||||
end)
|
||||
|
||||
it("should throw when passed a function", function()
|
||||
local action = Action("foo", function()
|
||||
return function() end
|
||||
end)
|
||||
|
||||
expect(action).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw with a invalid name", function()
|
||||
expect(function()
|
||||
Action(nil, function()
|
||||
return {}
|
||||
end)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
Action(100, function()
|
||||
return {}
|
||||
end)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw when passed a invalid function", function()
|
||||
expect(function()
|
||||
Action("foo", nil)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
Action("foo", {})
|
||||
end).to.throw()
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,19 @@
|
||||
local Color = {}
|
||||
|
||||
function Color.RgbFromHex(hexColor)
|
||||
assert(hexColor >= 0 and hexColor <= 0xffffff, "RgbFromHex: Out of range")
|
||||
|
||||
local b = hexColor % 256
|
||||
hexColor = (hexColor - b) / 256
|
||||
local g = hexColor % 256
|
||||
hexColor = (hexColor - g) / 256
|
||||
local r = hexColor
|
||||
|
||||
return r, g, b
|
||||
end
|
||||
|
||||
function Color.Color3FromHex(hexColor)
|
||||
return Color3.fromRGB(Color.RgbFromHex(hexColor))
|
||||
end
|
||||
|
||||
return Color
|
||||
@@ -0,0 +1,33 @@
|
||||
return function()
|
||||
local Color = require(script.Parent.Color)
|
||||
|
||||
describe("RgbFromHex", function()
|
||||
it("should convert a hex color to rgb correctly", function()
|
||||
local r, g, b = Color.RgbFromHex(0x232527)
|
||||
expect(r).to.equal(35)
|
||||
expect(g).to.equal(37)
|
||||
expect(b).to.equal(39)
|
||||
|
||||
r, g, b = Color.RgbFromHex(0x0)
|
||||
expect(r).to.equal(0)
|
||||
expect(g).to.equal(0)
|
||||
expect(b).to.equal(0)
|
||||
|
||||
r, g, b = Color.RgbFromHex(0xffffff)
|
||||
expect(r).to.equal(255)
|
||||
expect(g).to.equal(255)
|
||||
expect(b).to.equal(255)
|
||||
end)
|
||||
|
||||
|
||||
it("should assert if given a hex color out of range", function()
|
||||
expect(function()
|
||||
Color.RgbFromHex(-1)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
Color.RgbFromHex(0x1000000)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,131 @@
|
||||
--[[
|
||||
Provides an implementation of functional programming primitives.
|
||||
]]
|
||||
|
||||
local Functional = {}
|
||||
|
||||
--[[
|
||||
Create a copy of a list with only values for which `callback` returns true
|
||||
]]
|
||||
function Functional.Filter(list, callback)
|
||||
local new = {}
|
||||
|
||||
for key = 1, #list do
|
||||
local value = list[key]
|
||||
if callback(value, key) then
|
||||
table.insert(new, value)
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a copy of a list where each value is transformed by `callback`
|
||||
]]
|
||||
function Functional.Map(list, callback)
|
||||
local new = {}
|
||||
|
||||
for key = 1, #list do
|
||||
new[key] = callback(list[key], key)
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Identical to Map, except that the result will be reversed.
|
||||
]]
|
||||
function Functional.MapReverse(list, callback)
|
||||
local new = {}
|
||||
|
||||
for key = #list, 1, -1 do
|
||||
new[key] = callback(list[key], key)
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Create a copy of a list doing a combination filter and map.
|
||||
|
||||
If callback returns nil for any item, it is considered filtered from the
|
||||
list. Any other value is considered the result of the 'map' operation.
|
||||
]]
|
||||
function Functional.FilterMap(list, callback)
|
||||
local new = {}
|
||||
|
||||
for key = 1, #list do
|
||||
local value = list[key]
|
||||
local result = callback(value, key)
|
||||
|
||||
if result ~= nil then
|
||||
table.insert(new, result)
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Performs a left-fold of the list with the given initial value and callback.
|
||||
]]
|
||||
function Functional.Fold(list, initial, callback)
|
||||
local accum = initial
|
||||
|
||||
for key = 1, #list do
|
||||
accum = callback(accum, list[key], key)
|
||||
end
|
||||
|
||||
return accum
|
||||
end
|
||||
|
||||
--[[
|
||||
Performs a fold over the entries in the given dictionary.
|
||||
]]
|
||||
function Functional.FoldDictionary(dictionary, initial, callback)
|
||||
local accum = initial
|
||||
|
||||
for key, value in pairs(dictionary) do
|
||||
accum = callback(accum, key, value)
|
||||
end
|
||||
|
||||
return accum
|
||||
end
|
||||
|
||||
--[[
|
||||
Returns a list that contains at most `count` values from the given list.
|
||||
]]
|
||||
function Functional.Take(list, count, startingIndex)
|
||||
startingIndex = startingIndex or 1
|
||||
|
||||
local maxIndex = count + (startingIndex - 1)
|
||||
if maxIndex > #list then
|
||||
maxIndex = #list
|
||||
end
|
||||
|
||||
local new = {}
|
||||
|
||||
for i = startingIndex, maxIndex do
|
||||
local value = list[i]
|
||||
local newIndex = i - (startingIndex - 1)
|
||||
new[newIndex] = value
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
If the list contains the sought-after element, return its index, or nil otherwise.
|
||||
]]
|
||||
function Functional.Find(list, value)
|
||||
for index, element in ipairs(list) do
|
||||
if element == value then
|
||||
return index
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
return Functional
|
||||
@@ -0,0 +1,218 @@
|
||||
return function()
|
||||
local Functional = require(script.Parent.Functional)
|
||||
|
||||
local function identity(...)
|
||||
return ...
|
||||
end
|
||||
|
||||
local function add(a, b)
|
||||
return a + b
|
||||
end
|
||||
|
||||
describe("Filter", function()
|
||||
it("should copy lists correctly", function()
|
||||
local listA = {1, 2, 3}
|
||||
local listB = Functional.Filter(listA, function()
|
||||
return true
|
||||
end)
|
||||
|
||||
expect(listB).never.to.equal(listA)
|
||||
|
||||
for i = 1, #listB do
|
||||
expect(listB[i]).to.equal(listA[i])
|
||||
end
|
||||
end)
|
||||
|
||||
it("should correctly use the filter predicate", function()
|
||||
local listA = {1, 2, 3, 4, 5}
|
||||
local listB = Functional.Filter(listA, function(value, key)
|
||||
expect(value).to.equal(key)
|
||||
|
||||
return value % 2 == 0
|
||||
end)
|
||||
|
||||
expect(listB[1]).to.equal(2)
|
||||
expect(listB[2]).to.equal(4)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Map", function()
|
||||
it("should copy lists correctly using the identity function", function()
|
||||
local listA = {1, 2, 3}
|
||||
local listB = Functional.Map(listA, identity)
|
||||
|
||||
expect(listB).never.to.equal(listA)
|
||||
|
||||
for i = 1, #listB do
|
||||
expect(listB[i]).to.equal(listA[i])
|
||||
end
|
||||
end)
|
||||
|
||||
it("should correctly use the map predicate", function()
|
||||
local listA = {1, 2, 3}
|
||||
local listB = Functional.Map(listA, function(value, key)
|
||||
expect(value).to.equal(key)
|
||||
|
||||
return value * 2
|
||||
end)
|
||||
|
||||
for i = 1, #listB do
|
||||
expect(listB[i]).to.equal(listA[i] * 2)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("MapReverse", function()
|
||||
it("should copy lists correctly using the identity function", function()
|
||||
local listA = {1, 2, 3}
|
||||
local listB = Functional.MapReverse(listA, identity)
|
||||
|
||||
expect(listB).never.to.equal(listA)
|
||||
|
||||
for i = 1, #listB do
|
||||
expect(listB[i]).to.equal(listA[i])
|
||||
end
|
||||
end)
|
||||
|
||||
it("should correctly use the map predicate", function()
|
||||
local listA = {1, 2, 3}
|
||||
local listB = Functional.MapReverse(listA, function(value, key)
|
||||
expect(value).to.equal(key)
|
||||
|
||||
return value * 2
|
||||
end)
|
||||
|
||||
for i = 1, #listB do
|
||||
expect(listB[i]).to.equal(listA[i] * 2)
|
||||
end
|
||||
end)
|
||||
|
||||
it("should iterate backwards", function()
|
||||
local list = {1, 2, 3}
|
||||
local nextKey = 3
|
||||
|
||||
Functional.MapReverse(list, function(value, key)
|
||||
expect(value).to.equal(nextKey)
|
||||
expect(key).to.equal(nextKey)
|
||||
|
||||
nextKey = nextKey - 1
|
||||
end)
|
||||
|
||||
expect(nextKey).to.equal(0)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("FilterMap", function()
|
||||
it("should copy truthy lists using the identity function", function()
|
||||
local listA = {1, 2, 3}
|
||||
local listB = Functional.FilterMap(listA, identity)
|
||||
|
||||
expect(listB).never.to.equal(listA)
|
||||
|
||||
for i = 1, #listB do
|
||||
expect(listB[i]).to.equal(listA[i])
|
||||
end
|
||||
end)
|
||||
|
||||
it("should correctly use the filter-map predicate", function()
|
||||
local listA = {1, 2, 3, 4, 5}
|
||||
|
||||
-- Create a list containing only the odd numbers, and double those numbers
|
||||
local listB = Functional.FilterMap(listA, function(value, key)
|
||||
expect(value).to.equal(key)
|
||||
|
||||
if value % 2 == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
return value * 2
|
||||
end)
|
||||
|
||||
expect(listB[1]).to.equal(2)
|
||||
expect(listB[2]).to.equal(6)
|
||||
expect(listB[3]).to.equal(10)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Fold", function()
|
||||
it("should left-fold lists", function()
|
||||
local list = {1, 2, 3, 4, 5}
|
||||
|
||||
local sum = Functional.Fold(list, 0, add)
|
||||
|
||||
expect(sum).to.equal(15)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Take", function()
|
||||
it("should take values from a list", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Functional.Take(a, 2)
|
||||
|
||||
expect(#b).to.equal(2)
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should not take past the end of a list", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Functional.Take(a, 4)
|
||||
|
||||
expect(#b).to.equal(3)
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(2)
|
||||
expect(b[3]).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should copy all values when taking past the end of a list", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Functional.Take(a, 4)
|
||||
|
||||
expect(#b).to.equal(#a)
|
||||
expect(a[1]).to.equal(b[1])
|
||||
expect(a[2]).to.equal(b[2])
|
||||
expect(a[3]).to.equal(b[3])
|
||||
end)
|
||||
|
||||
it("should take values from a starting index when provided", function()
|
||||
local a = {1, 2, 3, 4}
|
||||
local b = Functional.Take(a, 2, 2)
|
||||
|
||||
expect(#b).to.equal(2)
|
||||
expect(b[1]).to.equal(2)
|
||||
expect(b[2]).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should not take past the end of a list when the starting index is provided", function()
|
||||
local a = {1, 2, 3, 4}
|
||||
local b = Functional.Take(a, 3, 3)
|
||||
|
||||
expect(#b).to.equal(2)
|
||||
expect(b[1]).to.equal(3)
|
||||
expect(b[2]).to.equal(4)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Find", function()
|
||||
it("should return index of matched item", function()
|
||||
local a = {"foo", "bar", "garply"}
|
||||
local b = Functional.Find(a, "bar")
|
||||
|
||||
expect(b).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should find the first example in the case of duplicates", function()
|
||||
local a = {"foo", "bar", "garply", "bar"}
|
||||
local b = Functional.Find(a, "bar")
|
||||
|
||||
expect(b).to.equal(2)
|
||||
end)
|
||||
|
||||
it("should return nil if item is not found", function()
|
||||
local a = {"foo", "bar", "garply"}
|
||||
local b = Functional.Find(a, "fleebledegoop")
|
||||
|
||||
expect(b).to.equal(nil)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,141 @@
|
||||
--[[
|
||||
Provides functions for manipulating immutable data structures.
|
||||
]]
|
||||
|
||||
local Immutable = {}
|
||||
|
||||
--[[
|
||||
Merges dictionary-like tables together.
|
||||
]]
|
||||
function Immutable.JoinDictionaries(...)
|
||||
local result = {}
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
local dictionary = select(i, ...)
|
||||
for key, value in pairs(dictionary) do
|
||||
result[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--[[
|
||||
Joins any number of lists together into a new list
|
||||
]]
|
||||
function Immutable.JoinLists(...)
|
||||
local new = {}
|
||||
|
||||
for listKey = 1, select("#", ...) do
|
||||
local list = select(listKey, ...)
|
||||
local len = #new
|
||||
|
||||
for itemKey = 1, #list do
|
||||
new[len + itemKey] = list[itemKey]
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a new copy of the dictionary and sets a value inside it.
|
||||
]]
|
||||
function Immutable.Set(dictionary, key, value)
|
||||
local new = {}
|
||||
|
||||
for key, value in pairs(dictionary) do
|
||||
new[key] = value
|
||||
end
|
||||
|
||||
new[key] = value
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a new copy of the list with the given elements appended to it.
|
||||
]]
|
||||
function Immutable.Append(list, ...)
|
||||
local new = {}
|
||||
local len = #list
|
||||
|
||||
for key = 1, len do
|
||||
new[key] = list[key]
|
||||
end
|
||||
|
||||
for i = 1, select("#", ...) do
|
||||
new[len + i] = select(i, ...)
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Remove elements from a dictionary
|
||||
]]
|
||||
function Immutable.RemoveFromDictionary(dictionary, ...)
|
||||
local result = {}
|
||||
|
||||
for key, value in pairs(dictionary) do
|
||||
local found = false
|
||||
for listKey = 1, select("#", ...) do
|
||||
if key == select(listKey, ...) then
|
||||
found = true
|
||||
break
|
||||
end
|
||||
end
|
||||
if not found then
|
||||
result[key] = value
|
||||
end
|
||||
end
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
--[[
|
||||
Remove the given key from the list.
|
||||
]]
|
||||
function Immutable.RemoveFromList(list, removeIndex)
|
||||
local new = {}
|
||||
|
||||
for i = 1, #list do
|
||||
if i ~= removeIndex then
|
||||
table.insert(new, list[i])
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Remove the range from the list starting from the index.
|
||||
]]
|
||||
function Immutable.RemoveRangeFromList(list, index, count)
|
||||
local new = {}
|
||||
|
||||
for i = 1, #list do
|
||||
if i < index or i >= index + count then
|
||||
table.insert(new, list[i])
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
--[[
|
||||
Creates a new list that has no occurrences of the given value.
|
||||
]]
|
||||
function Immutable.RemoveValueFromList(list, removeValue)
|
||||
local new = {}
|
||||
|
||||
for i = 1, #list do
|
||||
if list[i] ~= removeValue then
|
||||
table.insert(new, list[i])
|
||||
end
|
||||
end
|
||||
|
||||
return new
|
||||
end
|
||||
|
||||
return Immutable
|
||||
@@ -0,0 +1,284 @@
|
||||
return function()
|
||||
local Immutable = require(script.Parent.Immutable)
|
||||
|
||||
describe("JoinDictionaries", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {}
|
||||
local b = {}
|
||||
|
||||
local c = Immutable.JoinDictionaries(a, b)
|
||||
|
||||
expect(c).never.to.equal(a)
|
||||
expect(c).never.to.equal(b)
|
||||
end)
|
||||
|
||||
it("should treat list-like values like dictionary values", function()
|
||||
local a = {
|
||||
[1] = 1,
|
||||
[2] = 2,
|
||||
[3] = 3
|
||||
}
|
||||
|
||||
local b = {
|
||||
[1] = 11,
|
||||
[2] = 22
|
||||
}
|
||||
|
||||
local c = Immutable.JoinDictionaries(a, b)
|
||||
|
||||
expect(c[1]).to.equal(b[1])
|
||||
expect(c[2]).to.equal(b[2])
|
||||
expect(c[3]).to.equal(a[3])
|
||||
end)
|
||||
|
||||
it("should merge dictionary values correctly", function()
|
||||
local a = {
|
||||
hello = "world",
|
||||
foo = "bar"
|
||||
}
|
||||
|
||||
local b = {
|
||||
foo = "baz",
|
||||
tux = "penguin"
|
||||
}
|
||||
|
||||
local c = Immutable.JoinDictionaries(a, b)
|
||||
|
||||
expect(c.hello).to.equal(a.hello)
|
||||
expect(c.foo).to.equal(b.foo)
|
||||
expect(c.tux).to.equal(b.tux)
|
||||
end)
|
||||
|
||||
it("should merge multiple dictionaries", function()
|
||||
local a = {
|
||||
foo = "yes"
|
||||
}
|
||||
|
||||
local b = {
|
||||
bar = "yup"
|
||||
}
|
||||
|
||||
local c = {
|
||||
baz = "sure"
|
||||
}
|
||||
|
||||
local d = Immutable.JoinDictionaries(a, b, c)
|
||||
|
||||
expect(d.foo).to.equal(a.foo)
|
||||
expect(d.bar).to.equal(b.bar)
|
||||
expect(d.baz).to.equal(c.baz)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("JoinLists", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {}
|
||||
local b = {}
|
||||
|
||||
local c = Immutable.JoinLists(a, b)
|
||||
|
||||
expect(c).never.to.equal(a)
|
||||
expect(c).never.to.equal(b)
|
||||
end)
|
||||
|
||||
it("should treat list-like values correctly", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = {4, 5, 6}
|
||||
|
||||
local c = Immutable.JoinLists(a, b)
|
||||
|
||||
expect(#c).to.equal(6)
|
||||
|
||||
for i = 1, #c do
|
||||
expect(c[i]).to.equal(i)
|
||||
end
|
||||
end)
|
||||
|
||||
it("should merge multiple lists", function()
|
||||
local a = {1, 2}
|
||||
local b = {3, 4}
|
||||
local c = {5, 6}
|
||||
|
||||
local d = Immutable.JoinLists(a, b, c)
|
||||
|
||||
expect(#d).to.equal(6)
|
||||
|
||||
for i = 1, #d do
|
||||
expect(d[i]).to.equal(i)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Set", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {}
|
||||
|
||||
local b = Immutable.Set(a, "foo", "bar")
|
||||
|
||||
expect(b).never.to.equal(a)
|
||||
end)
|
||||
|
||||
it("should treat numeric keys normally", function()
|
||||
local a = {1, 2, 3}
|
||||
|
||||
local b = Immutable.Set(a, 2, 4)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(4)
|
||||
expect(b[3]).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should overwrite dictionary-like keys", function()
|
||||
local a = {
|
||||
foo = "bar",
|
||||
baz = "qux"
|
||||
}
|
||||
|
||||
local b = Immutable.Set(a, "foo", "hello there")
|
||||
|
||||
expect(b.foo).to.equal("hello there")
|
||||
expect(b.baz).to.equal(a.baz)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Append", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {}
|
||||
|
||||
local b = Immutable.Append(a, "another happy landing")
|
||||
|
||||
expect(b).never.to.equal(a)
|
||||
end)
|
||||
|
||||
it("should append values", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Immutable.Append(a, 4, 5)
|
||||
|
||||
expect(#b).to.equal(5)
|
||||
|
||||
for i = 1, #b do
|
||||
expect(b[i]).to.equal(i)
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("RemoveFromDictionary", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = { foo = "bar" }
|
||||
|
||||
local b = Immutable.RemoveFromDictionary(a, "foo")
|
||||
|
||||
expect(b).to.never.equal(a)
|
||||
end)
|
||||
|
||||
it("should remove fields from the dictionary", function()
|
||||
local a = {
|
||||
foo = "bar",
|
||||
baz = "qux",
|
||||
boof = "garply",
|
||||
}
|
||||
|
||||
local b = Immutable.RemoveFromDictionary(a, "foo", "boof")
|
||||
|
||||
expect(b.foo).to.never.be.ok()
|
||||
expect(b.baz).to.equal("qux")
|
||||
expect(b.boof).to.never.be.ok()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("RemoveFromList", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Immutable.RemoveFromList(a, 2)
|
||||
|
||||
expect(b).never.to.equal(a)
|
||||
end)
|
||||
|
||||
it("should remove elements from the list", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Immutable.RemoveFromList(a, 2)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(3)
|
||||
expect(b[3]).never.to.be.ok()
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("RemoveRangeFromList", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Immutable.RemoveRangeFromList(a, 2, 1)
|
||||
|
||||
expect(b).never.to.equal(a)
|
||||
end)
|
||||
|
||||
it("should remove elements properly from the list 1", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Immutable.RemoveRangeFromList(a, 2, 1)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(3)
|
||||
expect(b[3]).never.to.be.ok()
|
||||
end)
|
||||
|
||||
it("should remove elements properly from the list 2", function()
|
||||
local a = {1, 2, 3, 4, 5, 6}
|
||||
local b = Immutable.RemoveRangeFromList(a, 1, 4)
|
||||
|
||||
expect(b[1]).to.equal(5)
|
||||
expect(b[2]).to.equal(6)
|
||||
expect(b[3]).never.to.be.ok()
|
||||
end)
|
||||
|
||||
it("should remove elements properly from the list 3", function()
|
||||
local a = {1, 2, 3, 4, 5, 6}
|
||||
local b = Immutable.RemoveRangeFromList(a, 2, 4)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(6)
|
||||
expect(b[3]).never.to.be.ok()
|
||||
end)
|
||||
|
||||
it("should remove elements properly from the list 4", function()
|
||||
local a = {1, 2, 3, 4, 5, 6, 7}
|
||||
local b = Immutable.RemoveRangeFromList(a, 4, 4)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(2)
|
||||
expect(b[3]).to.equal(3)
|
||||
expect(b[4]).never.to.be.ok()
|
||||
end)
|
||||
|
||||
it("should not remove any elements when count is 0 or less", function()
|
||||
local a = {1, 2, 3}
|
||||
local b = Immutable.RemoveRangeFromList(a, 2, 0)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(2)
|
||||
expect(b[3]).to.equal(3)
|
||||
|
||||
local c = Immutable.RemoveRangeFromList(a, 2, -1)
|
||||
expect(c[1]).to.equal(1)
|
||||
expect(c[2]).to.equal(2)
|
||||
expect(c[3]).to.equal(3)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("RemoveValueFromList", function()
|
||||
it("should preserve immutability", function()
|
||||
local a = {1, 1, 1}
|
||||
local b = Immutable.RemoveValueFromList(a, 1)
|
||||
|
||||
expect(b).never.to.equal(a)
|
||||
end)
|
||||
|
||||
it("should remove all elements from the list", function()
|
||||
local a = {1, 2, 2, 3}
|
||||
local b = Immutable.RemoveValueFromList(a, 2)
|
||||
|
||||
expect(b[1]).to.equal(1)
|
||||
expect(b[2]).to.equal(3)
|
||||
expect(b[3]).never.to.be.ok()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,75 @@
|
||||
--[[
|
||||
A limited, simple implementation of a Signal.
|
||||
|
||||
Handlers are fired in order, and (dis)connections are properly handled when
|
||||
executing an event.
|
||||
|
||||
Signal uses Immutable to avoid invalidating the 'Fire' loop iteration.
|
||||
]]
|
||||
|
||||
local Immutable = require(script.Parent.Immutable)
|
||||
|
||||
local Signal = {}
|
||||
|
||||
Signal.__index = Signal
|
||||
|
||||
function Signal.new()
|
||||
local self = {
|
||||
_listeners = {}
|
||||
}
|
||||
|
||||
setmetatable(self, Signal)
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Signal:connect(callback)
|
||||
local listener = {
|
||||
callback = callback,
|
||||
isConnected = true,
|
||||
}
|
||||
self._listeners = Immutable.Append(self._listeners, listener)
|
||||
|
||||
local function disconnect()
|
||||
listener.isConnected = false
|
||||
self._listeners = Immutable.RemoveValueFromList(self._listeners, listener)
|
||||
end
|
||||
|
||||
return {
|
||||
Disconnect = function()
|
||||
warn(string.format(
|
||||
"Connection:Disconnect() has been deprecated, use Connection:disconnect()\n%s]",
|
||||
debug.traceback()
|
||||
))
|
||||
disconnect()
|
||||
end,
|
||||
disconnect = disconnect,
|
||||
}
|
||||
end
|
||||
|
||||
function Signal:fire(...)
|
||||
for _, listener in ipairs(self._listeners) do
|
||||
if listener.isConnected then
|
||||
listener.callback(...)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function Signal:Connect(...)
|
||||
warn(string.format(
|
||||
"Signal:Connect() has been deprecated, use Signal:connect()\n%s]",
|
||||
debug.traceback()
|
||||
))
|
||||
return self:connect(...)
|
||||
end
|
||||
|
||||
function Signal:Fire(...)
|
||||
warn(string.format(
|
||||
"Signal:Fire() has been deprecated, use Signal:fire()\n%s]",
|
||||
debug.traceback()
|
||||
))
|
||||
self:fire(...)
|
||||
end
|
||||
|
||||
|
||||
return Signal
|
||||
@@ -0,0 +1,114 @@
|
||||
return function()
|
||||
local Signal = require(script.Parent.Signal)
|
||||
|
||||
it("should construct from nothing", function()
|
||||
local signal = Signal.new()
|
||||
|
||||
expect(signal).to.be.ok()
|
||||
end)
|
||||
|
||||
it("should fire connected callbacks", function()
|
||||
local callCount = 0
|
||||
local value1 = "Hello World"
|
||||
local value2 = 7
|
||||
|
||||
local callback = function(arg1, arg2)
|
||||
expect(arg1).to.equal(value1)
|
||||
expect(arg2).to.equal(value2)
|
||||
callCount = callCount + 1
|
||||
end
|
||||
|
||||
local signal = Signal.new()
|
||||
|
||||
local connection = signal:connect(callback)
|
||||
signal:fire(value1, value2)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
connection:disconnect()
|
||||
signal:fire(value1, value2)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should disconnect handlers", function()
|
||||
local callback = function()
|
||||
error("Callback was called after disconnect!")
|
||||
end
|
||||
|
||||
local signal = Signal.new()
|
||||
|
||||
local connection = signal:connect(callback)
|
||||
connection:disconnect()
|
||||
|
||||
signal:fire()
|
||||
end)
|
||||
|
||||
it("should fire handlers in order", function()
|
||||
local signal = Signal.new()
|
||||
local x = 0
|
||||
local y = 0
|
||||
|
||||
local callback1 = function()
|
||||
expect(x).to.equal(0)
|
||||
expect(y).to.equal(0)
|
||||
x = x + 1
|
||||
end
|
||||
|
||||
local callback2 = function()
|
||||
expect(x).to.equal(1)
|
||||
expect(y).to.equal(0)
|
||||
y = y + 1
|
||||
end
|
||||
|
||||
signal:connect(callback1)
|
||||
signal:connect(callback2)
|
||||
signal:fire()
|
||||
|
||||
expect(x).to.equal(1)
|
||||
expect(y).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should continue firing despite mid-event disconnection", function()
|
||||
local signal = Signal.new()
|
||||
local countA = 0
|
||||
local countB = 0
|
||||
|
||||
local connectionA
|
||||
connectionA = signal:connect(function()
|
||||
connectionA:disconnect()
|
||||
countA = countA + 1
|
||||
end)
|
||||
|
||||
signal:connect(function()
|
||||
countB = countB + 1
|
||||
end)
|
||||
|
||||
signal:fire()
|
||||
|
||||
expect(countA).to.equal(1)
|
||||
expect(countB).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should skip listeners that were disconnected during event evaluation", function()
|
||||
local signal = Signal.new()
|
||||
local countA = 0
|
||||
local countB = 0
|
||||
|
||||
local connectionB
|
||||
|
||||
signal:connect(function()
|
||||
countA = countA + 1
|
||||
connectionB:disconnect()
|
||||
end)
|
||||
|
||||
connectionB = signal:connect(function()
|
||||
countB = countB + 1
|
||||
end)
|
||||
|
||||
signal:fire()
|
||||
|
||||
expect(countA).to.equal(1)
|
||||
expect(countB).to.equal(0)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
local EngineFeatureTextBoundsRoundUp = game:GetEngineFeature("TextBoundsRoundUp")
|
||||
|
||||
local TextService = game:GetService("TextService")
|
||||
|
||||
local Text = {}
|
||||
|
||||
-- FYI: Any number greater than 2^30 will make TextService:GetTextSize give invalid results
|
||||
local MAX_BOUND = 10000
|
||||
|
||||
-- Remove with EngineFeatureTextBoundsRoundUp
|
||||
Text._TEMP_PATCHED_PADDING = Vector2.new(0, 0)
|
||||
|
||||
if not EngineFeatureTextBoundsRoundUp then
|
||||
Text._TEMP_PATCHED_PADDING = Vector2.new(2, 2)
|
||||
end
|
||||
|
||||
-- Wrapper function for GetTextSize
|
||||
function Text.GetTextBounds(text, font, fontSize, bounds)
|
||||
return TextService:GetTextSize(text, fontSize, font, bounds) + Text._TEMP_PATCHED_PADDING
|
||||
end
|
||||
|
||||
function Text.GetTextWidth(text, font, fontSize)
|
||||
return Text.GetTextBounds(text, font, fontSize, Vector2.new(MAX_BOUND, MAX_BOUND)).X
|
||||
end
|
||||
|
||||
function Text.GetTextHeight(text, font, fontSize, widthCap)
|
||||
return Text.GetTextBounds(text, font, fontSize, Vector2.new(widthCap, MAX_BOUND)).Y
|
||||
end
|
||||
|
||||
-- TODO(CLIPLAYEREX-391): Kill these truncate functions once we have official support for text truncation
|
||||
function Text.Truncate(text, font, fontSize, widthInPixels, overflowMarker)
|
||||
overflowMarker = overflowMarker or ""
|
||||
|
||||
if Text.GetTextWidth(text, font, fontSize) > widthInPixels then
|
||||
-- A binary search may be more efficient
|
||||
local lastText = ""
|
||||
for _, stopIndex in utf8.graphemes(text) do
|
||||
local newText = string.sub(text, 1, stopIndex) .. overflowMarker
|
||||
if Text.GetTextWidth(newText, font, fontSize) > widthInPixels then
|
||||
return lastText
|
||||
end
|
||||
lastText = newText
|
||||
end
|
||||
else -- No truncation needed
|
||||
return text
|
||||
end
|
||||
|
||||
return ""
|
||||
end
|
||||
|
||||
function Text.TruncateTextLabel(textLabel, overflowMarker)
|
||||
textLabel.Text = Text.Truncate(textLabel.Text, textLabel.Font,
|
||||
textLabel.TextSize, textLabel.AbsoluteSize.X, overflowMarker)
|
||||
end
|
||||
|
||||
-- Remove whitespace from the beginning and end of the string
|
||||
function Text.Trim(str)
|
||||
if type(str) ~= "string" then
|
||||
error(string.format("Text.Trim called on non-string type %s.", type(str)), 2)
|
||||
end
|
||||
return (str:gsub("^%s*(.-)%s*$", "%1"))
|
||||
end
|
||||
|
||||
-- Remove whitespace from the end of the string
|
||||
function Text.RightTrim(str)
|
||||
if type(str) ~= "string" then
|
||||
error(string.format("Text.RightTrim called on non-string type %s.", type(str)), 2)
|
||||
end
|
||||
return (str:gsub("%s+$", ""))
|
||||
end
|
||||
|
||||
-- Remove whitespace from the beginning of the string
|
||||
function Text.LeftTrim(str)
|
||||
if type(str) ~= "string" then
|
||||
error(string.format("Text.LeftTrim called on non-string type %s.", type(str)), 2)
|
||||
end
|
||||
return (str:gsub("^%s+", ""))
|
||||
end
|
||||
|
||||
-- Replace multiple whitespace with one; remove leading and trailing whitespace
|
||||
function Text.SpaceNormalize(str)
|
||||
if type(str) ~= "string" then
|
||||
error(string.format("Text.SpaceNormalize called on non-string type %s.", type(str)), 2)
|
||||
end
|
||||
return (str:gsub("%s+", " "):gsub("^%s+" , ""):gsub("%s+$" , ""))
|
||||
end
|
||||
|
||||
-- Splits a string by the provided pattern into a table. The pattern is interpreted as plain text.
|
||||
function Text.Split(str, pattern)
|
||||
if type(str) ~= "string" then
|
||||
error(string.format("Text.Split called on non-string type %s.", type(str)), 2)
|
||||
elseif type(pattern) ~= "string" then
|
||||
error(string.format("Text.Split called with a pattern that is non-string type %s.", type(pattern)), 2)
|
||||
elseif pattern == "" then
|
||||
error("Text.Split called with an empty pattern.", 2)
|
||||
end
|
||||
|
||||
local result = {}
|
||||
local currentPosition = 1
|
||||
|
||||
while true do
|
||||
local patternStart, patternEnd = string.find(str, pattern, currentPosition, true)
|
||||
if not patternStart or not patternEnd then break end
|
||||
table.insert(result, string.sub(str, currentPosition, patternStart - 1))
|
||||
currentPosition = patternEnd + 1
|
||||
end
|
||||
|
||||
table.insert(result, string.sub(str, currentPosition, string.len(str)))
|
||||
|
||||
return result
|
||||
end
|
||||
|
||||
return Text
|
||||
@@ -0,0 +1,410 @@
|
||||
return function()
|
||||
local Text = require(script.Parent.Text)
|
||||
|
||||
describe("GetTextBounds", function()
|
||||
it("should return a bounds of padding width and font-size height when the string is empty", function()
|
||||
local bounds = Text.GetTextBounds("", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000))
|
||||
expect(bounds.X).to.equal(Text._TEMP_PATCHED_PADDING.x)
|
||||
expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
|
||||
end)
|
||||
it("should return the height and width of a string as one line with large bounds", function()
|
||||
local bounds = Text.GetTextBounds("One Two Three", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000))
|
||||
expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
|
||||
end)
|
||||
|
||||
it("should return the height of the string as multiple lines with short bounds", function()
|
||||
local bounds = Text.GetTextBounds("One Two Three Four", Enum.Font.SourceSans, 18, Vector2.new(32, 1000))
|
||||
expect(bounds.Y > 18).to.equal(true)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("GetTextHeight", function()
|
||||
it("should return height equal to font size when string is empty", function()
|
||||
local height = Text.GetTextHeight("", Enum.Font.SourceSans, 18, 0)
|
||||
expect(height).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("GetTextWidth", function()
|
||||
it("should return width equal to 1 when string is empty", function()
|
||||
local width = Text.GetTextWidth("", Enum.Font.SourceSans, 18, 18)
|
||||
expect(width).to.equal(Text._TEMP_PATCHED_PADDING.x)
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("Truncate", function()
|
||||
it("should return empty string", function()
|
||||
local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 0, "...")
|
||||
expect(emptyQuery).to.be.a("string")
|
||||
expect(emptyQuery).to.equal("")
|
||||
end)
|
||||
|
||||
it("should return empty string for not empty box", function()
|
||||
local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 50, "...")
|
||||
expect(emptyQuery).to.be.a("string")
|
||||
expect(emptyQuery).to.equal("")
|
||||
end)
|
||||
|
||||
it("should truncate with ...", function()
|
||||
local reallyLongQuery = Text.Truncate(
|
||||
"One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100, "...")
|
||||
expect(reallyLongQuery).to.equal("One Two Thre...")
|
||||
end)
|
||||
|
||||
it("should truncate without a ...", function()
|
||||
local reallyLongQueryNoOverflowMarker = Text.Truncate(
|
||||
"One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100)
|
||||
expect(reallyLongQueryNoOverflowMarker).to.equal("One Two Three ")
|
||||
end)
|
||||
|
||||
it("should not truncate", function()
|
||||
local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, 100)
|
||||
expect(shouldFitQuery).to.equal("One Two")
|
||||
end)
|
||||
|
||||
it("should not truncate, off by one check", function()
|
||||
local oneCharQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 100)
|
||||
expect(oneCharQuery).to.equal("O")
|
||||
end)
|
||||
|
||||
it("should truncate, off by one check", function()
|
||||
local oneCharNoRoomQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 0)
|
||||
expect(oneCharNoRoomQuery).to.equal("")
|
||||
end)
|
||||
|
||||
it("should perform a negative width check", function()
|
||||
local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, -100, "...")
|
||||
expect(shouldFitQuery).to.equal("")
|
||||
end)
|
||||
|
||||
itFIXME("should truncate long graphemes properly", function()
|
||||
-- 11-byte rainbow flag grapheme
|
||||
-- Flag, zero-space-joiner, rainbow
|
||||
local rainbowFlag = utf8.char(127987) .. utf8.char(8205) .. utf8.char(127752)
|
||||
local oneFlagWithinLimit = Text.Truncate(
|
||||
rainbowFlag, Enum.Font.SourceSans, 18, 100, "...")
|
||||
expect(oneFlagWithinLimit).to.equal(rainbowFlag)
|
||||
|
||||
local twoRainbowFlags = rainbowFlag .. rainbowFlag
|
||||
local twoFlagsAreFine = Text.Truncate(
|
||||
twoRainbowFlags, Enum.Font.SourceSans, 18, 100, "...")
|
||||
expect(twoFlagsAreFine).to.equal(twoRainbowFlags)
|
||||
|
||||
local fourRainbowFlags = twoRainbowFlags .. twoRainbowFlags
|
||||
local fourFlagsIsTooLong = Text.Truncate(
|
||||
fourRainbowFlags, Enum.Font.SourceSans, 18, 100, "...")
|
||||
expect(fourFlagsIsTooLong).to.equal(twoRainbowFlags .. "...") -- With --fflags==true fails because of truncation
|
||||
end)
|
||||
end)
|
||||
|
||||
describe("TruncateTextLabel", function()
|
||||
it("should use text label attributes to truncate text", function()
|
||||
local screenGui = Instance.new("ScreenGui")
|
||||
local textLabel = Instance.new("TextLabel")
|
||||
textLabel.Size = UDim2.new(0, 100, 0, 32)
|
||||
textLabel.Text = "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve"
|
||||
textLabel.Font = Enum.Font.SourceSans
|
||||
textLabel.TextSize = 18
|
||||
textLabel.Parent = screenGui
|
||||
Text.TruncateTextLabel(textLabel)
|
||||
|
||||
expect(textLabel.Text).to.equal("One Two Three ")
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
describe("TrimString", function()
|
||||
it("Should trim the string properly 1", function()
|
||||
local trimmedInput = Text.Trim("")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 2", function()
|
||||
local trimmedInput = Text.Trim(" ")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 3", function()
|
||||
local trimmedInput = Text.Trim("ab")
|
||||
local expected = "ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 4", function()
|
||||
local trimmedInput = Text.Trim(" ab ")
|
||||
local expected = "ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 5", function()
|
||||
local trimmedInput = Text.Trim(" a b ")
|
||||
local expected = "a b"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 6", function()
|
||||
local trimmedInput = Text.Trim("\r\n\t\f a\r\n\t\f ")
|
||||
local expected = "a"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string with unicode characters properly", function()
|
||||
local trimmedInput = Text.Trim("😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 7", function()
|
||||
local trimmedInput = Text.Trim(" 😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓 ")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should trim the string properly 8", function()
|
||||
local trimmedInput = Text.Trim("\n 😤👩🏼🏫😭ぼ😀 \nで😹🤕あ👩🏻🎓 \n")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀 \nで😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
describe("RightTrimString", function()
|
||||
it("Should right trim the string properly 1", function()
|
||||
local trimmedInput = Text.RightTrim("")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 2", function()
|
||||
local trimmedInput = Text.RightTrim(" ")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 3", function()
|
||||
local trimmedInput = Text.RightTrim("ab")
|
||||
local expected = "ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 4", function()
|
||||
local trimmedInput = Text.RightTrim(" ab ")
|
||||
local expected = " ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 5", function()
|
||||
local trimmedInput = Text.RightTrim(" a b ")
|
||||
local expected = " a b"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 6", function()
|
||||
local trimmedInput = Text.RightTrim("\r\n\t\f a\r\n\t\f ")
|
||||
local expected = "\r\n\t\f a"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string with unicode characters properly", function()
|
||||
local trimmedInput = Text.RightTrim("😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 7", function()
|
||||
local trimmedInput = Text.RightTrim(" 😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓 ")
|
||||
local expected = " 😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should right trim the string properly 8", function()
|
||||
local trimmedInput = Text.RightTrim("\n 😤👩🏼🏫😭ぼ😀 \nで😹🤕あ👩🏻🎓 \n")
|
||||
local expected = "\n 😤👩🏼🏫😭ぼ😀 \nで😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
describe("LeftTrimString", function()
|
||||
it("Should left trim the string properly 1", function()
|
||||
local trimmedInput = Text.LeftTrim("")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly 2", function()
|
||||
local trimmedInput = Text.LeftTrim(" ")
|
||||
local expected = ""
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly 3", function()
|
||||
local trimmedInput = Text.LeftTrim("ab")
|
||||
local expected = "ab"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly 4", function()
|
||||
local trimmedInput = Text.LeftTrim(" ab ")
|
||||
local expected = "ab "
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly 5", function()
|
||||
local trimmedInput = Text.LeftTrim(" a b ")
|
||||
local expected = "a b "
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly 6", function()
|
||||
local trimmedInput = Text.LeftTrim("\r\n\t\f a\r\n\t\f ")
|
||||
local expected = "a\r\n\t\f "
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string with unicode characters properly", function()
|
||||
local trimmedInput = Text.LeftTrim("😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly 7", function()
|
||||
local trimmedInput = Text.LeftTrim(" 😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓 ")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀で😹🤕あ👩🏻🎓 "
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
it("Should left trim the string properly", function()
|
||||
local trimmedInput = Text.LeftTrim("\n 😤👩🏼🏫😭ぼ😀 \nで😹🤕あ👩🏻🎓 \n")
|
||||
local expected = "😤👩🏼🏫😭ぼ😀 \nで😹🤕あ👩🏻🎓 \n"
|
||||
expect(trimmedInput).to.equal(expected)
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
describe("SpaceNormalize", function()
|
||||
it("should remove multiple spaces between words", function()
|
||||
local a = "This is not a normal sentence."
|
||||
|
||||
expect(Text.SpaceNormalize(a)).to.equal("This is not a normal sentence.")
|
||||
end)
|
||||
|
||||
it("should remove leading and trailing whitespace", function()
|
||||
local a = " SpaceTabSpaceTab "
|
||||
|
||||
expect(Text.SpaceNormalize(a)).to.equal("SpaceTabSpaceTab")
|
||||
end)
|
||||
|
||||
it("should not change a string with no whitespace", function()
|
||||
local a = "There'sNo%Whit.e\\space--InThis."
|
||||
|
||||
expect(Text.SpaceNormalize(a)).to.equal(a)
|
||||
end)
|
||||
|
||||
it("should remove all whitespace in a string that is nothing but whitespace", function()
|
||||
local a = " "
|
||||
|
||||
expect(Text.SpaceNormalize(a)).to.equal("")
|
||||
end)
|
||||
|
||||
it("should handle the case where the string is empty", function()
|
||||
local a = ""
|
||||
|
||||
expect(Text.SpaceNormalize(a)).to.equal(a)
|
||||
end)
|
||||
|
||||
it("should throw an error if called an a non-string type", function()
|
||||
local a = { first = 1, second = 2 }
|
||||
|
||||
expect(function()
|
||||
Text.SpaceNormalize(a)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
|
||||
|
||||
describe("Split", function()
|
||||
local function tableEquals(tb1, tb2)
|
||||
local tables = { tb1, tb2 }
|
||||
|
||||
for _,tb in ipairs(tables) do
|
||||
for key in pairs(tb) do
|
||||
if tb1[key] ~= tb2[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
it("should return the correct table for your standard use case", function()
|
||||
local a = "this,is,comma,separated"
|
||||
local pattern = ","
|
||||
local expectedResult = {
|
||||
[1] = "this",
|
||||
[2] = "is",
|
||||
[3] = "comma",
|
||||
[4] = "separated",
|
||||
}
|
||||
|
||||
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should not remove whitespace", function()
|
||||
local a = " SpaceTab , , Space"
|
||||
local pattern = ","
|
||||
local expectedResult = {
|
||||
[1] = " SpaceTab ",
|
||||
[2] = " ",
|
||||
[3] = " Space",
|
||||
}
|
||||
|
||||
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should treat regular expressions as plain text", function()
|
||||
local a = "Notyour^%s+normalstring.Thisisasecondsentence."
|
||||
local b = "."
|
||||
local c = "^%s+"
|
||||
local d = "%A"
|
||||
|
||||
local expectedB = {
|
||||
[1] = "Notyour^%s+normalstring",
|
||||
[2] = "Thisisasecondsentence",
|
||||
[3] = "",
|
||||
}
|
||||
local expectedC = {
|
||||
[1] = "Notyour",
|
||||
[2] = "normalstring.Thisisasecondsentence."
|
||||
}
|
||||
local expectedD = {
|
||||
[1] = "Notyour^%s+normalstring.Thisisasecondsentence."
|
||||
}
|
||||
|
||||
expect(tableEquals(Text.Split(a, b), expectedB)).to.equal(true)
|
||||
expect(tableEquals(Text.Split(a, c), expectedC)).to.equal(true)
|
||||
expect(tableEquals(Text.Split(a, d), expectedD)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should work when pattern is not in string", function()
|
||||
local a = "The pattern you are looking for does not exist."
|
||||
local pattern = ","
|
||||
local expectedResult = {
|
||||
[1] = "The pattern you are looking for does not exist.",
|
||||
}
|
||||
|
||||
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should work when called on an empty string", function()
|
||||
local a = ""
|
||||
local pattern = ","
|
||||
local expectedResult = {
|
||||
[1] = "",
|
||||
}
|
||||
|
||||
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
|
||||
end)
|
||||
|
||||
it("should throw an error if called on an empty pattern", function()
|
||||
local a = "The pattern definitely doesn't exist here."
|
||||
local pattern = ""
|
||||
|
||||
expect(function()
|
||||
Text.Split(a, pattern)
|
||||
end).to.throw()
|
||||
end)
|
||||
|
||||
it("should throw an error if called an a non-string type", function()
|
||||
local a = { first = 1, second = 2 }
|
||||
local b = "an actual string"
|
||||
|
||||
expect(function()
|
||||
Text.Split(a, b)
|
||||
end).to.throw()
|
||||
|
||||
expect(function()
|
||||
Text.Split(b, a)
|
||||
end).to.throw()
|
||||
end)
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,68 @@
|
||||
--[[
|
||||
|
||||
memoize creates a function as a wrapper that caches the last outputs of a function.
|
||||
|
||||
This is useful if you know that the function should return the same output every
|
||||
|
||||
time it is run with the same inputs. The function should only return an output, and
|
||||
|
||||
not have any side effects. These side effects are not cached.
|
||||
|
||||
|
||||
|
||||
Without memoize's caching, even though the function ouputs the same values, the
|
||||
|
||||
memory locations of the values are different; tables made in the function, even if
|
||||
|
||||
they have the same values, won't be the same tables.
|
||||
|
||||
|
||||
|
||||
memoize only caches the last set of inputs and ouputs. This means that it is only
|
||||
|
||||
helpful when the function is likely to be called with the same inputs multiple
|
||||
|
||||
times in a row. This is the case with most Roact use cases.
|
||||
|
||||
|
||||
|
||||
Note that memoize only does a ** shallow check on table inputs ** . This means
|
||||
|
||||
that if the same table is input but the elements of the table are different then
|
||||
|
||||
it will be assumed that the table has not changed.
|
||||
|
||||
|
||||
|
||||
In addition to all the previous warnings, memoize strips trailing nils. This means
|
||||
|
||||
that if foo is a memoized function and we call foo(), then foo(nil) will return a
|
||||
|
||||
cached value. This is opposed to how print handles input. print() only outputs a
|
||||
|
||||
new line, but print(nil) outputs "nil". This is because varargs can detect the
|
||||
|
||||
number of arguments passed in. So, be careful when using memoize with varargs.
|
||||
|
||||
Trailing nils will be stripped.
|
||||
|
||||
|
||||
|
||||
The wrapper can take any number of inputs and give any number of outputs.
|
||||
|
||||
Leading and interspersed nils are handled gracefully. Trailing nils on the input
|
||||
|
||||
are stripped.
|
||||
|
||||
]]
|
||||
|
||||
local function captureSize(...)
|
||||
|
||||
return {...}, select("#", ...)
|
||||
|
||||
end
|
||||
|
||||
|
||||
|
||||
local function memoize(func)
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
return function()
|
||||
local memoize = require(script.Parent.memoize)
|
||||
|
||||
describe("memoize", function()
|
||||
it("should handle arity 0", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a, b)
|
||||
callCount = callCount + 1
|
||||
return a, b
|
||||
end)
|
||||
|
||||
expect(identity()).to.equal(nil)
|
||||
expect(identity(nil)).to.equal(nil)
|
||||
expect(identity(nil, nil)).to.equal(nil)
|
||||
expect(callCount).to.equal(1)
|
||||
end)
|
||||
|
||||
it("should handle arity 1", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a)
|
||||
callCount = callCount + 1
|
||||
return a
|
||||
end)
|
||||
|
||||
expect(identity(5)).to.equal(5)
|
||||
expect(identity(5)).to.equal(5)
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
expect(identity(6)).to.equal(6)
|
||||
expect(callCount).to.equal(2)
|
||||
|
||||
expect(identity(5)).to.equal(5)
|
||||
expect(callCount).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should handle arity 2", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a, b)
|
||||
callCount = callCount + 1
|
||||
return a, b
|
||||
end)
|
||||
|
||||
local a, b
|
||||
|
||||
a, b = identity(5, 6)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(6)
|
||||
|
||||
a, b = identity(5, 6)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(6)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
a, b = identity(6, 5)
|
||||
expect(a).to.equal(6)
|
||||
expect(b).to.equal(5)
|
||||
|
||||
expect(callCount).to.equal(2)
|
||||
|
||||
a, b = identity(5, 6)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(6)
|
||||
|
||||
expect(callCount).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should handle mixed arity", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a, b)
|
||||
callCount = callCount + 1
|
||||
return a, b
|
||||
end)
|
||||
|
||||
local a, b
|
||||
|
||||
a, b = identity(5, 6)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(6)
|
||||
|
||||
a, b = identity(5, 6)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(6)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
a, b = identity(5)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
a, b = identity(5)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(2)
|
||||
|
||||
a, b = identity()
|
||||
expect(a).to.equal(nil)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
a, b = identity()
|
||||
expect(a).to.equal(nil)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should handle trailing nils", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a, b)
|
||||
callCount = callCount + 1
|
||||
return a, b
|
||||
end)
|
||||
|
||||
local a, b
|
||||
|
||||
a, b = identity(5, nil)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
a, b = identity(5)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
a, b = identity(7)
|
||||
expect(a).to.equal(7)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(2)
|
||||
|
||||
a, b = identity(5)
|
||||
expect(a).to.equal(5)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should handle leading nils", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a, b)
|
||||
callCount = callCount + 1
|
||||
return a, b
|
||||
end)
|
||||
|
||||
local a, b
|
||||
|
||||
a, b = identity(nil, 7)
|
||||
expect(a).to.equal(nil)
|
||||
expect(b).to.equal(7)
|
||||
|
||||
a, b = identity(nil, 7)
|
||||
expect(a).to.equal(nil)
|
||||
expect(b).to.equal(7)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
a, b = identity(7)
|
||||
expect(a).to.equal(7)
|
||||
expect(b).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(2)
|
||||
|
||||
a, b = identity(nil, 7)
|
||||
expect(a).to.equal(nil)
|
||||
expect(b).to.equal(7)
|
||||
|
||||
expect(callCount).to.equal(3)
|
||||
end)
|
||||
|
||||
it("should handle interspersed nils", function()
|
||||
local callCount = 0
|
||||
local identity = memoize(function(a, b, c, d)
|
||||
callCount = callCount + 1
|
||||
return a, b, c, d
|
||||
end)
|
||||
|
||||
local a, b, c, d
|
||||
|
||||
a, b, c, d = identity(7, nil, 7, nil)
|
||||
expect(a).to.equal(7)
|
||||
expect(b).to.equal(nil)
|
||||
expect(c).to.equal(7)
|
||||
expect(d).to.equal(nil)
|
||||
|
||||
-- Trailing nils can affect how interspersed nils are handled
|
||||
a, b, c, d = identity(7, nil, 7)
|
||||
expect(a).to.equal(7)
|
||||
expect(b).to.equal(nil)
|
||||
expect(c).to.equal(7)
|
||||
expect(d).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(1)
|
||||
|
||||
a, b, c, d = identity(7, nil, nil, nil)
|
||||
expect(a).to.equal(7)
|
||||
expect(b).to.equal(nil)
|
||||
expect(c).to.equal(nil)
|
||||
expect(d).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(2)
|
||||
|
||||
a, b, c, d = identity(7, nil, 7, nil)
|
||||
expect(a).to.equal(7)
|
||||
expect(b).to.equal(nil)
|
||||
expect(c).to.equal(7)
|
||||
expect(d).to.equal(nil)
|
||||
|
||||
expect(callCount).to.equal(3)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
Reference in New Issue
Block a user