{"id":"biotoxin495/utilitykit","name":"utilitykit","scope":"biotoxin495","platform":"roblox","description":"A focused collection of stateless Roblox and Luau utilities.","version":"1.0.0","latest":"1.0.0","versions":["1.0.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{"sleitnick/signal":{"version":"^2.0.3","alias":"Signal"}},"integrity":"e9e8170a14b330a8d7fa121bfd1462dd82a44cb8fc9279bad754bad95f3cd2de","likes":0,"downloads":0,"install":"forest install biotoxin495/utilitykit","url":"https://forest.dev/p/roblox/biotoxin495/utilitykit","files":"https://api.forest.dev/ai/package/roblox/biotoxin495/utilitykit/files","readme":"# UtilityKit — Dependency-free stateless utilities for Roblox\n\n**UtilityKit**, a focused, dependency-free collection of stateless utilities for Roblox and Luau.\n\nCommon table operations, random selection, number and time formatting, vector searches, `UDim2` conversions, and string helpers are easy to reimplement inconsistently across a project.\n\n**UtilityKit** collects those predictable operations behind a small namespaced API. It does not create Instances, connect events, read the current camera, manage UI state, or depend on project-specific configuration schemas.\n\n## Quick example\n\n```lua\nlocal UtilityKit = require(path.To.UtilityKit)\n\nlocal settings = UtilityKit.Table.DeepMerge(defaults, overrides)\nlocal reward = UtilityKit.Random.WeightedChoice(weightedRewards)\nlocal timeText = UtilityKit.Time.FormatDuration(3_661)\n```\n\nEvery module is stateless apart from UtilityKit's shared default pseudorandom generator. Functions are suitable for both server and client code, and optional `Random` instances make random behavior reproducible.\n\n## 🚀 Features\n\n- Namespaced API with no global state beyond a default pseudorandom generator.\n- Strict Luau annotations.\n- No third-party runtime dependencies.\n- Shared server and client support.\n- Cycle-safe deep copying.\n- Optional deterministic `Random` instances.\n- Explicit mutation behavior.\n- Rojo project and dependency-free Studio tests included.\n\n## Project Structure\n\n```text\nUtilityKit/\n├── src/\n│   ├── init.lua\n│   ├── Table.lua\n│   ├── Random.lua\n│   ├── Number.lua\n│   ├── Time.lua\n│   ├── Vector.lua\n│   ├── UDim.lua\n│   └── String.lua\n├── tests/\n│   └── init.server.lua\n├── examples/\n│   └── BasicUsage.client.lua\n├── default.project.json\n├── dev.project.json\n├── wally.toml\n├── stylua.toml\n├── selene.toml\n└── LICENSE\n```\n\n## 🛠️ Installation\n\n### Manual\n\nCopy the `src` directory into your project and make it a ModuleScript container named `UtilityKit`. The `init.lua` file is the package entry point and the other files become its child ModuleScripts.\n\n```lua\nlocal UtilityKit = require(game.ReplicatedStorage.UtilityKit)\n```\n\n### Rojo\n\nThe package-oriented `default.project.json` builds UtilityKit as a standalone ModuleScript. For development and tests, `dev.project.json` maps the library to `ReplicatedStorage.UtilityKit` and the runner to `ServerScriptService.UtilityKitTests`.\n\n```bash\nrojo serve dev.project.json\n```\n\nStart a Studio play session after connecting Rojo. The test runner prints a success message or reports individual failures in the output.\n\n### Wally\n\nBefore publishing, replace `your-scope` in `wally.toml` with your Wally scope. After the package is published, consumers can add it to their dependencies and require it from their generated Packages folder.\n\n## 📖 Basic Usage\n\nAll functions use dot syntax through their namespace:\n\n```lua\nUtilityKit.Table.Count(value)\nUtilityKit.Random.Choice(items)\nUtilityKit.Number.FormatCompact(value)\n```\n\nFunctions return `nil` when an empty collection has no meaningful result, and invalid arguments that indicate programmer error raise an assertion or error. Functions are non-mutating unless their name ends in `InPlace` or explicitly documents mutation.\n\n### Design Rules\n\n- All functions use dot syntax: `UtilityKit.Table.Count(value)`.\n- Functions do not require constructors or service loaders.\n- Functions return `nil` when an empty collection has no meaningful result.\n- Invalid arguments that indicate programmer error raise an assertion or error.\n- Functions are non-mutating unless their name ends in `InPlace` or explicitly documents mutation.\n- Random functions accept an optional `Random` instance for reproducible behavior.\n- Modules and the root export are shallow-frozen.\n\n### Example\n\n```lua\nlocal UtilityKit = require(game.ReplicatedStorage.UtilityKit)\n\nlocal defaults = {\n\tAudio = {\n\t\tMusic = true,\n\t\tSFX = true,\n\t},\n}\n\nlocal overrides = {\n\tAudio = {\n\t\tMusic = false,\n\t},\n}\n\nlocal settings = UtilityKit.Table.DeepMerge(defaults, overrides)\nprint(settings.Audio.Music) -- false\nprint(defaults.Audio.Music) -- true; the original was not mutated\n\nprint(UtilityKit.Number.FormatThousands(1250000)) -- 1,250,000\nprint(UtilityKit.Time.FormatClock(65)) -- 01:05\nprint(UtilityKit.String.ToSnakeCase(\"Daily Reward Count\")) -- daily_reward_count\n```\n\n# ⚙️ API Reference\n\n## `UtilityKit.Table`\n\nUtilities for dictionaries, arrays, nested data, copying, merging, and change detection.\n\n### `Table.Count`\n\n```lua\nTable.Count(source: {[any]: any}): number\n```\n\nReturns the number of key-value entries in a table. Unlike `#source`, this works for dictionaries and mixed tables.\n\n```lua\nTable.Count({A = 1, B = 2}) -- 2\n```\n\n### `Table.Keys`\n\n```lua\nTable.Keys(source: {[any]: any}): {any}\n```\n\nReturns an array containing every key in `source`. Dictionary iteration order is not guaranteed.\n\n```lua\nlocal keys = Table.Keys({Coins = 10, Gems = 2})\n```\n\n### `Table.Values`\n\n```lua\nTable.Values(source: {[any]: any}): {any}\n```\n\nReturns an array containing every value in `source`. Dictionary iteration order is not guaranteed.\n\n```lua\nlocal values = Table.Values({Coins = 10, Gems = 2})\n```\n\n### `Table.ShallowCopy`\n\n```lua\nTable.ShallowCopy(source: {[any]: any}): {[any]: any}\n```\n\nCreates a shallow copy using `table.clone`. Nested tables remain shared with the original.\n\n```lua\nlocal copy = Table.ShallowCopy(original)\n```\n\n### `Table.DeepCopy`\n\n```lua\nTable.DeepCopy(value: any, preserveMetatable: boolean?): any\n```\n\nRecursively copies tables while preserving cycles and shared references. Non-table values are returned unchanged. Metatable references are preserved by default; pass `false` to omit metatables.\n\n```lua\nlocal shared = {Value = 5}\nlocal source = {A = shared, B = shared}\nsource.Self = source\n\nlocal copy = Table.DeepCopy(source)\nassert(copy ~= source)\nassert(copy.A == copy.B)\nassert(copy.Self == copy)\n```\n\nMetatables are not recursively cloned. A copied table receives the same metatable reference when that metatable is accessible and is itself a table.\n\n### `Table.DeepEqual`\n\n```lua\nTable.DeepEqual(left: any, right: any): boolean\n```\n\nRecursively compares values and supports cyclic tables. Metatables are ignored. Table keys are matched by normal Luau key equality rather than structurally deep-compared.\n\n```lua\nTable.DeepEqual({A = {B = 2}}, {A = {B = 2}}) -- true\n```\n\n### `Table.DeepMerge`\n\n```lua\nTable.DeepMerge(base: {[any]: any}, incoming: {[any]: any}): {[any]: any}\n```\n\nReturns a deep-copied merge without mutating either input. When both values at a key are tables, their entries are recursively merged. Otherwise, the incoming value replaces the base value.\n\n```lua\nlocal result = Table.DeepMerge(\n\t{Audio = {Music = true}},\n\t{Audio = {SFX = true}}\n)\n\n-- result.Audio contains Music and SFX\n```\n\nNumeric keys are merged like any other keys. This means arrays are merged by index and a shorter incoming array does not automatically truncate the original array.\n\n### `Table.DeepMergeInPlace`\n\n```lua\nTable.DeepMergeInPlace(base: {[any]: any}, incoming: {[any]: any}): {[any]: any}\n```\n\nRecursively merges `incoming` into `base` and returns `base`. This function mutates `base`. Incoming replacement values are deep-copied before assignment.\n\n```lua\nTable.DeepMergeInPlace(settings, overrides)\n```\n\n### `Table.Unique`\n\n```lua\nTable.Unique(values: {any}): {any}\n```\n\nReturns the first occurrence of each unique array value while preserving encounter order.\n\n```lua\nTable.Unique({\"A\", \"A\", \"B\"}) -- {\"A\", \"B\"}\n```\n\nValues use normal table-key equality. This works well for strings, numbers, booleans, enums, instances, and reference-identity values.\n\n### `Table.Difference`\n\n```lua\nTable.Difference(first: {any}, second: {any}): {any}\n```\n\nReturns unique values present in `first` but absent from `second`, preserving the order from `first`.\n\n```lua\nTable.Difference({1, 2, 3}, {2, 4}) -- {1, 3}\n```\n\n### `Table.Intersection`\n\n```lua\nTable.Intersection(first: {any}, second: {any}): {any}\n```\n\nReturns unique values present in both arrays, preserving their order from `first`.\n\n```lua\nTable.Intersection({1, 2, 3}, {2, 4}) -- {2}\n```\n\n### `Table.GetPath`\n\n```lua\nTable.GetPath(root: any, path: {any}, defaultValue: any?): any\n```\n\nReads a nested value using an array of keys. Returns `defaultValue` when the path cannot be traversed or the final value is `nil`.\n\n```lua\nlocal coins = Table.GetPath(data, {\"Inventory\", \"Coins\"}, 0)\n```\n\nAn empty path returns `root`.\n\n### `Table.SetPath`\n\n```lua\nTable.SetPath(root: {[any]: any}, path: {any}, value: any): {[any]: any}\n```\n\nWrites a nested value and creates missing intermediate tables. Returns `root` for chaining. It errors when the path is empty or an existing intermediate value is not a table.\n\n```lua\nTable.SetPath(data, {\"Inventory\", \"Coins\"}, 100)\n```\n\nThis function mutates `root`.\n\n### `Table.Diff`\n\n```lua\nTable.Diff(oldValue: {[any]: any}, newValue: {[any]: any}): {DiffOperation}\n```\n\nProduces structured operations describing how to transform `oldValue` into `newValue`.\n\n```lua\nexport type DiffOperation = {\n\tKind: \"Set\" | \"Remove\",\n\tPath: {any},\n\tValue: any?,\n}\n```\n\nExample:\n\n```lua\nlocal operations = Table.Diff(\n\t{Coins = 10, OldItem = true},\n\t{Coins = 20, Gems = 2}\n)\n```\n\nPossible operations include:\n\n```lua\n{\n\t{Kind = \"Set\", Path = {\"Coins\"}, Value = 20},\n\t{Kind = \"Set\", Path = {\"Gems\"}, Value = 2},\n\t{Kind = \"Remove\", Path = {\"OldItem\"}},\n}\n```\n\nOperation order is not guaranteed for dictionary keys. Set values are deep-copied. Cyclic table pairs are guarded against, although diffs are primarily intended for ordinary serializable data tables.\n\n## `UtilityKit.Random`\n\nRandom-selection helpers built on Roblox's `Random` datatype. Every function accepts an optional `Random` object; supplying one makes tests and procedural generation reproducible.\n\n```lua\nlocal random = Random.new(12345)\nlocal item = RandomUtil.Choice(items, random)\n```\n\nWhen omitted, UtilityKit uses a shared internal generator.\n\n### `Random.Integer`\n\n```lua\nRandom.Integer(minimum: number, maximum: number, random: Random?): number\n```\n\nReturns an integer uniformly selected from the inclusive range `[minimum, maximum]`. Both bounds must be integers and `minimum` cannot exceed `maximum`.\n\n```lua\nRandomUtil.Integer(1, 6) -- inclusive dice roll\n```\n\n### `Random.Float`\n\n```lua\nRandom.Float(minimum: number?, maximum: number?, random: Random?): number\n```\n\nReturns a pseudorandom number between the provided bounds. The defaults are `0` and `1`. Bounds must be finite and ordered.\n\n```lua\nRandomUtil.Float()       -- 0 to 1\nRandomUtil.Float(-5, 5)  -- -5 to 5\n```\n\n### `Random.Choice`\n\n```lua\nRandom.Choice<T>(items: {T}, random: Random?): T?\n```\n\nReturns one uniformly selected array item, or `nil` when the array is empty.\n\n```lua\nlocal color = RandomUtil.Choice({\"Red\", \"Blue\", \"Green\"})\n```\n\n### `Random.DictionaryEntry`\n\n```lua\nRandom.DictionaryEntry(dictionary: {[any]: any}, random: Random?): (any?, any?)\n```\n\nReturns a randomly selected key and its value. Returns `nil, nil` when the dictionary is empty.\n\n```lua\nlocal key, value = RandomUtil.DictionaryEntry(rewards)\n```\n\nEach call allocates a temporary key array, so repeated selection from a large stable dictionary should use a cached key array instead.\n\n### `Random.WeightedChoice`\n\n```lua\nRandom.WeightedChoice<T>(items: {WeightedItem<T>}, random: Random?): T?\n```\n\nWeighted entries use this shape:\n\n```lua\nexport type WeightedItem<T> = {\n\tValue: T,\n\tWeight: number,\n}\n```\n\nReturns a value with probability proportional to its non-negative weight. Zero-weight entries are ignored. Returns `nil` when the list is empty or every weight is zero. Negative, infinite, and NaN weights raise an error.\n\n```lua\nlocal reward = RandomUtil.WeightedChoice({\n\t{Value = \"Coins\", Weight = 70},\n\t{Value = \"Gems\", Weight = 25},\n\t{Value = \"RareItem\", Weight = 5},\n})\n```\n\nFractional weights are supported.\n\n### `Random.Shuffle`\n\n```lua\nRandom.Shuffle<T>(items: {T}, random: Random?): {T}\n```\n\nReturns a shuffled shallow copy. The input array is not mutated. Sparse arrays are not supported because Roblox's built-in shuffle operation requires a contiguous array.\n\n```lua\nlocal shuffled = RandomUtil.Shuffle({1, 2, 3, 4})\n```\n\n### `Random.Sample`\n\n```lua\nRandom.Sample<T>(items: {T}, count: number, random: Random?): {T}\n```\n\nReturns `count` unique array entries sampled without replacement. The input is not mutated. `count` must be a non-negative integer no greater than the array length.\n\n```lua\nlocal selected = RandomUtil.Sample(players, 3)\n```\n\n## `UtilityKit.Number`\n\nNumeric calculations and predictable display formatting.\n\n### `Number.IsFinite`\n\n```lua\nNumber.IsFinite(value: number): boolean\n```\n\nReturns `false` for positive infinity, negative infinity, and NaN; otherwise returns `true`.\n\n```lua\nNumberUtil.IsFinite(10)        -- true\nNumberUtil.IsFinite(math.huge) -- false\n```\n\n### `Number.Average`\n\n```lua\nNumber.Average(values: {number}): number?\n```\n\nReturns the arithmetic mean, or `nil` for an empty array.\n\n```lua\nNumberUtil.Average({2, 4, 6}) -- 4\n```\n\n### `Number.MinMax`\n\n```lua\nNumber.MinMax(values: {number}): (number?, number?)\n```\n\nReturns the smallest and largest values in one pass. Returns `nil, nil` for an empty array.\n\n```lua\nlocal minimum, maximum = NumberUtil.MinMax({7, 2, 9})\n```\n\n### `Number.RoundTo`\n\n```lua\nNumber.RoundTo(value: number, decimalPlaces: number?): number\n```\n\nRounds to the requested number of decimal places. The default is `0`. Negative decimal places round to tens, hundreds, and larger powers of ten.\n\n```lua\nNumberUtil.RoundTo(1.235, 2) -- 1.24\nNumberUtil.RoundTo(126, -1)  -- 130\n```\n\n### `Number.RoundToStep`\n\n```lua\nNumber.RoundToStep(value: number, step: number): number\n```\n\nRounds to the nearest multiple of a positive finite step.\n\n```lua\nNumberUtil.RoundToStep(27, 5) -- 25\n```\n\n### `Number.Normalize`\n\n```lua\nNumber.Normalize(\n\tvalue: number,\n\tinputMinimum: number,\n\tinputMaximum: number,\n\tclampResult: boolean?\n): number\n```\n\nMaps an input range to normalized space, where `inputMinimum` is `0` and `inputMaximum` is `1`. When `clampResult` is true, results are clamped to `[0, 1]`.\n\n```lua\nNumberUtil.Normalize(5, 0, 10) -- 0.5\n```\n\nThe input range cannot have zero length. Reversed ranges are supported.\n\n### `Number.MapRange`\n\n```lua\nNumber.MapRange(\n\tvalue: number,\n\tinputMinimum: number,\n\tinputMaximum: number,\n\toutputMinimum: number,\n\toutputMaximum: number,\n\tclampResult: boolean?\n): number\n```\n\nMaps a value from one numeric range into another. When `clampResult` is true, the normalized input is clamped before it is mapped.\n\n```lua\nNumberUtil.MapRange(5, 0, 10, 0, 100) -- 50\n```\n\n### `Number.FormatThousands`\n\n```lua\nNumber.FormatThousands(\n\tvalue: number,\n\tdecimalPlaces: number?,\n\tthousandsSeparator: string?,\n\tdecimalSeparator: string?\n): string\n```\n\nFormats a finite number with grouped thousands.\n\n```lua\nNumberUtil.FormatThousands(1234567)       -- \"1,234,567\"\nNumberUtil.FormatThousands(1234.5, 2)     -- \"1,234.50\"\nNumberUtil.FormatThousands(1234.5, 1, \" \", \",\") -- \"1 234,5\"\n```\n\nWhen `decimalPlaces` is omitted, UtilityKit preserves the ordinary decimal representation produced by `tostring`. Scientific-notation strings are returned unchanged.\n\n### `Number.FormatCompact`\n\n```lua\nNumber.FormatCompact(value: number, decimalPlaces: number?): string\n```\n\nFormats large finite values using compact suffixes: `K`, `M`, `B`, `T`, and `Q`. The default precision is one decimal place, and trailing zeroes are removed.\n\n```lua\nNumberUtil.FormatCompact(1500)       -- \"1.5K\"\nNumberUtil.FormatCompact(2000000)    -- \"2M\"\n```\n\n### `Number.DiscountPercent`\n\n```lua\nNumber.DiscountPercent(oldPrice: number, newPrice: number, roundStep: number?): number\n```\n\nCalculates the percentage reduction from `oldPrice` to `newPrice`. `oldPrice` must be greater than zero. Supplying `roundStep` rounds the result to the nearest step.\n\n```lua\nNumberUtil.DiscountPercent(100, 72)    -- 28\nNumberUtil.DiscountPercent(100, 72, 5) -- 30\n```\n\nA negative result repre","readmeTruncated":true,"readmeFull":"https://api.forest.dev/v1/package/biotoxin495/roblox/utilitykit/1.0.0/readme"}