├── LICENSE ├── README.md ├── lib └── underscore.lua ├── spec ├── arrays_spec.lua ├── chaining_spec.lua ├── collections_spec.lua ├── functions_spec.lua ├── objects_spec.lua ├── string_spec.lua └── utility_spec.lua └── underscore-dev-1.rockspec /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 JT Archie 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 | # Deprecation Warning 2 | 3 | I am no longer actively maintaining this project. This was originally a project to help me learn Lua and provide a familiar interface for people in the Lua community. I feel there are better and well maintained libraries currently. 4 | 5 | * [Moses](https://github.com/Yonaba/Moses) 6 | * [Lua Functional](https://github.com/rtsisyk/luafun) 7 | 8 | 9 | *NOTE*: This is a 1-to-1 port of the orignal [Underscore](http://underscorejs.org/) for Javscript. Lua does not have the same concept of run-time loop, so anything functionality that relied on setTimeout has been removed. 10 | 11 | # Introduction 12 | 13 | [underscore-lua](https://github.com/jtarchie/underscore-lua) is a utility-belt library for Lua that provides a lot of the functional programming support that you would expect in or Ruby's Enumerable. 14 | 15 | The project is hosted on [GitHub](https://github.com/jtarchie/underscore-lua). You can report bugs and discuss features on the [issues page](https://github.com/jtarchie/underscore-lua/issues), or send tweets to @jtarchie. 16 | 17 | # Tests 18 | 19 | The tests are written using [busted](http://olivinelabs.com/busted/). Run `busted` from the root of the source tree to run all the tests. 20 | 21 | # Table of Contents 22 | 23 | * [Collection](#collection-functions-arrays-or-objects) 24 | * [Arrays](#arrays) 25 | * [Objects](#objects) 26 | * [String](#string) 27 | * [Functions](#functions) 28 | * [Utility](#utility) 29 | * [Chaining](#chaining) 30 | * [Changelog](#changelog) 31 | 32 | # Collection functions (Arrays or Objects) 33 | 34 | ## each 35 | 36 | `_.each(list, iterator)` 37 | 38 | Iterates over a list of elements, yielding each in turn to an iterator function. Each invocation of iterator is called with three arguments: (element, index, list). If list is a Lua object, iterator's arguments will be (value, key, list). 39 | 40 | ```lua 41 | _.each({1, 2, 3}, print) 42 | => print each number in turn... 43 | _.each({one=1, two=2, three=3}, function(num, key) print(num) end) 44 | => print each number in turn... 45 | ``` 46 | 47 | ## map 48 | 49 | `_.map(list, iterator)` Alias: collect 50 | 51 | Produces a new array of values by mapping each value in list through a transformation function (iterator). If list is a Lua object, iterator's arguments will be (value, key, list). 52 | 53 | ```lua 54 | _.map({1, 2, 3}, function(num) return num * 3 end) 55 | => {3, 6, 9} 56 | _.map({one=1, two=2, three=3}, function(num, key) return num * 3 end) 57 | => {3, 6, 9} 58 | ``` 59 | 60 | ## reduce 61 | 62 | `_.reduce(list, iterator, memo)` Aliases: inject, foldl 63 | 64 | Also known as inject and foldl, reduce boils down a list of values into a single value. Memo is the initial state of the reduction, and each successive step of it should be returned by iterator. The iterator is passed four arguments: the memo, then the value and index (or key) of the iteration, and finally a reference to the entire list. 65 | 66 | ```lua 67 | local sum = _.reduce({1, 2, 3}, function(memo, num) return memo + num end, 0) 68 | => 6 69 | ``` 70 | 71 | ## reduceRight 72 | 73 | `_.reduceRight(list, iterator, memo)` Alias: foldr 74 | 75 | The right-associative version of reduce. Foldr is not as useful in Lua as it would be in a language with lazy evaluation. 76 | 77 | ```lua 78 | local list = {{0, 1}, {2, 3}, {4, 5}} 79 | local flat = _.reduceRight(list, function(a, b) return _.concat(a, b) end, {}) 80 | => {4, 5, 2, 3, 0, 1} 81 | ``` 82 | 83 | ## find 84 | 85 | `_.find(list, iterator)` Alias: detect 86 | 87 | Looks through each value in the list, returning the first one that passes a truth test (iterator). The function returns as soon as it finds an acceptable element, and doesn't traverse the entire list. 88 | 89 | ```lua 90 | local even = _.find({1, 2, 3, 4, 5, 6}, function(num) return num % 2 == 0 end) 91 | => 2 92 | ``` 93 | 94 | ## filter 95 | 96 | `_.filter(list, iterator)` Alias: select 97 | 98 | Looks through each value in the list, returning an array of all the values that pass a truth test (iterator). Delegates to the native filter method, if it exists. 99 | 100 | ```lua 101 | local evens = _.filter({1, 2, 3, 4, 5, 6}, function(num) return num % 2 == 0 end) 102 | => {2, 4, 6} 103 | ``` 104 | 105 | ## where 106 | 107 | `_.where(list, properties)` 108 | 109 | Looks through each value in the list, returning an array of all the values that contain all of the key-value pairs listed in properties. 110 | 111 | ```lua 112 | _.where(listOfPlays, {author="Shakespeare", year=1611}) 113 | => {{title="Cymbeline", author="Shakespeare", year=1611}, 114 | {title="The Tempest", author="Shakespeare", year=1611}} 115 | ``` 116 | 117 | ## reject 118 | 119 | `_.reject(list, iterator)` 120 | 121 | Returns the values in list without the elements that the truth test (iterator) passes. The opposite of filter. 122 | 123 | ```lua 124 | local odds = _.reject({1, 2, 3, 4, 5, 6}, function(num) return num % 2 == 0 end) 125 | => {1, 3, 5} 126 | ``` 127 | 128 | ## all 129 | 130 | `_.all(list, iterator)` Alias: every 131 | 132 | Returns true if all of the values in the list pass the iterator truth test. 133 | 134 | ```lua 135 | _.all({true, 1, nil, 'yes'}, _.identity) 136 | => false 137 | ``` 138 | 139 | ## contains 140 | 141 | `_.contains(list, value)` Alias: include 142 | 143 | Returns true if the value is present in the list. Uses indexOf internally, if list is an Array. 144 | 145 | ```lua 146 | _.contains({1, 2, 3}, 3) 147 | => true 148 | ``` 149 | 150 | ## invoke 151 | 152 | `_.invoke(list, methodName, [*arguments])` 153 | 154 | Calls the method named by methodName on each value in the list. Any extra arguments passed to invoke will be forwarded on to the method invocation. 155 | 156 | ```lua 157 | local dog = {says=function() return "woof" end} 158 | local cat = {says=function() return "meow" end} 159 | local cow = {says=function() return "moo" end} 160 | _.invoke({dog, cat, cow}, 'says') 161 | => {'woof', 'meow', 'moo'} 162 | ``` 163 | 164 | ## pluck 165 | 166 | `_.pluck(list, propertyName)` 167 | 168 | A convenient version of what is perhaps the most common use-case for map: extracting a list of property values. 169 | 170 | ```lua 171 | local stooges = {{name='moe', age=40}, {name='larry', age=50}, {name='curly', age=60}} 172 | _.pluck(stooges, 'name') 173 | => {"moe", "larry", "curly"} 174 | ``` 175 | 176 | ## max 177 | 178 | `_.max(list, [iterator])` 179 | 180 | Returns the maximum value in list. If iterator is passed, it will be used on each value to generate the criterion by which the value is ranked. 181 | 182 | ```lua 183 | local stooges = {{name='moe', age=40}, {name='larry', age=50}, {name='curly', age=60}} 184 | _.max(stooges, function(stooge) return stooge.age end) 185 | => {name='curly', age=60} 186 | ``` 187 | 188 | ## min 189 | 190 | `_.min(list, [iterator])` 191 | 192 | Returns the minimum value in list. If iterator is passed, it will be used on each value to generate the criterion by which the value is ranked. 193 | 194 | ```lua 195 | local numbers = {10, 5, 100, 2, 1000} 196 | _.min(numbers) 197 | => 2 198 | ``` 199 | 200 | ## sortBy 201 | 202 | `_.sortBy(list, iterator)` 203 | 204 | Returns a sorted copy of list, ranked in ascending order by the results of running each value through iterator. Iterator may also be the string name of the property to sort by (eg. length). 205 | 206 | ```lua 207 | _.sortBy({1, 2, 3, 4, 5, 6}, function(num) return math.sin(num) end) 208 | => {5, 4, 6, 3, 1, 2} 209 | ``` 210 | 211 | ## groupBy 212 | 213 | `_.groupBy(list, iterator)` 214 | 215 | Splits a collection into sets, grouped by the result of running each value through iterator. If iterator is a string instead of a function, groups by the property named by iterator on each of the values. 216 | 217 | ```lua 218 | _.groupBy({1.3, 2.1, 2.4}, function(num) return math.floor(num) end) 219 | => {1={1.3}, 2={2.1, 2.4} 220 | ``` 221 | 222 | ## countBy 223 | 224 | `_.countBy(list, iterator)` 225 | 226 | Sorts a list into groups and returns a count for the number of objects in each group. Similar to groupBy, but instead of returning a list of values, returns a count for the number of values in that group. 227 | 228 | ```lua 229 | _.countBy({1, 2, 3, 4, 5}, function(num) 230 | if num % 2 == 0 then 231 | return 'even' 232 | else 233 | return 'odd' 234 | end 235 | end) 236 | => {odd=3, even=2} 237 | ``` 238 | 239 | ## shuffle 240 | 241 | `_.shuffle(list)` 242 | 243 | Returns a shuffled copy of the list, using a version of the Fisher-Yates shuffle. 244 | 245 | ```lua 246 | _.shuffle({1, 2, 3, 4, 5, 6}) 247 | => {4, 1, 6, 3, 5, 2} 248 | ``` 249 | 250 | 251 | ## toArray 252 | 253 | `_.toArray(list)` 254 | 255 | Converts the list (anything that can be iterated over), into a real Array. Useful for transmuting the arguments object. 256 | 257 | ```lua 258 | _.toArray(1, 2, 3, 4) 259 | => {2, 3, 4} 260 | ``` 261 | 262 | 263 | ## size 264 | 265 | `_.size(list)` 266 | 267 | Return the number of values in the list. 268 | 269 | ```lua 270 | _.size({one=1, two=2, three=3}) 271 | => 3 272 | ``` 273 | 274 | #Arrays 275 | 276 | ## first 277 | 278 | `_.first(array, [n])` Alias: head, take 279 | 280 | Returns the first element of an array. Passing n will return the first n elements of the array. 281 | 282 | ```lua 283 | _.first({5, 4, 3, 2, 1}) 284 | => 5 285 | ``` 286 | 287 | ## initial 288 | 289 | `_.initial(array, [n])` 290 | 291 | Returns everything but the last entry of the array. Especially useful on variable arguments object. Pass n to exclude the last n elements from the result. 292 | 293 | ```lua 294 | _.initial({5, 4, 3, 2, 1}) 295 | => {5, 4, 3, 2} 296 | ``` 297 | 298 | ## last 299 | 300 | `_.last(array, [n])` 301 | 302 | Returns the last element of an array. Passing n will return the last n elements of the array. 303 | 304 | ```lua 305 | _.last({5, 4, 3, 2, 1}) 306 | => 1 307 | ``` 308 | 309 | ## rest 310 | 311 | `_.rest(array, [index])` Alias: tail, drop 312 | 313 | Returns the rest of the elements in an array. Pass an index to return the values of the array from that index onward. 314 | 315 | ```lua 316 | _.rest({5, 4, 3, 2, 1}) 317 | => {4, 3, 2, 1} 318 | ``` 319 | 320 | ## compact 321 | 322 | `_.compact(array)` 323 | 324 | Returns a copy of the array with all falsy values removed. In Lua, false is the only falsy value. 325 | 326 | ```lua 327 | _.compact({0, 1, false, 2, '', 3}) 328 | => {0, 1, 2, '', 3} 329 | ``` 330 | 331 | ## flatten 332 | 333 | `_.flatten(array, [shallow])` 334 | 335 | Flattens a nested array (the nesting can be to any depth). If you pass shallow, the array will only be flattened a single level. 336 | 337 | ```lua 338 | _.flatten({1, {2}, {3, {{4}}}}) 339 | => {1, 2, 3, 4} 340 | _.flatten({1, {2}, {3, {{4}}}}, true) 341 | => {1, 2, 3, {{4}}} 342 | ``` 343 | 344 | ## without 345 | 346 | `_.without(array, [*values])` 347 | 348 | Returns a copy of the array with all instances of the values removed. 349 | 350 | ```lua 351 | _.without({1, 2, 1, 0, 3, 1, 4}, 0, 1) 352 | => {2, 3, 4} 353 | ``` 354 | 355 | ## union 356 | 357 | `_.union(*arrays)` 358 | 359 | Computes the union of the passed-in arrays: the list of unique items, in order, that are present in one or more of the arrays. 360 | 361 | ```lua 362 | _.union({1, 2, 3}, {101, 2, 1, 10}, {2, 1}) 363 | => {1, 2, 3, 101, 10} 364 | ``` 365 | 366 | ## intersection 367 | 368 | `_.intersection(*arrays)` 369 | 370 | Computes the list of values that are the intersection of all the arrays. Each value in the result is present in each of the arrays. 371 | 372 | ```lua 373 | _.intersection({1, 2, 3}, {101, 2, 1, 10}, {2, 1}) 374 | => {1, 2} 375 | ``` 376 | 377 | ## difference 378 | 379 | `_.difference(array, *others)` 380 | 381 | Similar to without, but returns the values from array that are not present in the other arrays. 382 | 383 | ```lua 384 | _.difference({1, 2, 3, 4, 5}, {5, 2, 10}) 385 | => {1, 3, 4} 386 | ``` 387 | 388 | ## uniq 389 | 390 | `_.uniq(array, [isSorted], [iterator])` Alias: unique 391 | 392 | Produces a duplicate-free version of the array, using === to test object equality. If you know in advance that the array is sorted, passing true for isSorted will run a much faster algorithm. If you want to compute unique items based on a transformation, pass an iterator function. 393 | 394 | ```lua 395 | _.uniq({1, 2, 1, 3, 1, 4}) 396 | => {1, 2, 3, 4} 397 | ``` 398 | 399 | ## zip 400 | 401 | `_.zip(*arrays)` 402 | 403 | Merges together the values of each of the arrays with the values at the corresponding position. Useful when you have separate data sources that are coordinated through matching array indexes. If you're working with a matrix of nested arrays, zip.apply can transpose the matrix in a similar fashion. 404 | 405 | ```lua 406 | _.zip({'moe', 'larry', 'curly'}, {30, 40, 50}, {true, false, false}) 407 | => {{"moe", 30, true}, {"larry", 40, false}, {"curly", 50, false}} 408 | ``` 409 | 410 | ## object 411 | 412 | `_.object(list, [values])` 413 | 414 | Converts arrays into objects. Pass either a single list of (key, value) pairs, or a list of keys, and a list of values. 415 | 416 | ```lua 417 | _.object({'moe', 'larry', 'curly'}, {30, 40, 50}) 418 | => {moe=30, larry=40, curly=50} 419 | _.object({{'moe', 30}, {'larry', 40}, {'curly', 50}}) 420 | => {moe=30, larry=40, curly=50} 421 | ``` 422 | 423 | ## indexOf 424 | 425 | `_.indexOf(array, value, [isSorted])` 426 | 427 | Returns the index at which value can be found in the array, or 0 if value is not present in the array. Uses the native indexOf function unless it's missing. If you're working with a large array, and you know that the array is already sorted, pass true for isSorted to use a faster binary search ... or, pass a number as the third argument in order to look for the first matching value in the array after the given index. 428 | 429 | ```lua 430 | _.indexOf({1, 2, 3}, 2) 431 | => 1 432 | ``` 433 | 434 | ## lastIndexOf 435 | 436 | `_.lastIndexOf(array, value, [fromIndex])` 437 | 438 | Returns the index of the last occurrence of value in the array, or 0 if value is not present. Uses the native lastIndexOf function if possible. Pass fromIndex to start your search at a given index. 439 | 440 | ```lua 441 | _.lastIndexOf({1, 2, 3, 1, 2, 3}, 2) 442 | => 4 443 | ``` 444 | 445 | ## range 446 | 447 | `_.range([start], stop, [step])` 448 | 449 | A function to create flexibly-numbered lists of integers, handy for each and map loops. start, if omitted, defaults to 0; step defaults to 1. Returns a list of integers from start to stop, incremented (or decremented) by step, exclusive. 450 | 451 | ```lua 452 | _.range(10) 453 | => {0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 454 | _.range(1, 11) 455 | => {1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 456 | _.range(0, 30, 5) 457 | => {0, 5, 10, 15, 20, 25} 458 | _.range(0, -10, -1) 459 | => {0, -1, -2, -3, -4, -5, -6, -7, -8, -9} 460 | _.range(0) 461 | => {} 462 | ``` 463 | 464 | ## pop 465 | 466 | `_.pop(array)` 467 | 468 | Removes the last element from an array and returns that value. 469 | 470 | ```lua 471 | _.pop({1,2,3,4}) 472 | => 4 473 | ``` 474 | 475 | ## push 476 | 477 | `_.push(array, [elements])` 478 | 479 | Adds the list of elements on to the end of an array. 480 | 481 | ```lua 482 | local array = {1,2,3} 483 | _.push(array, 4,5,6) 484 | => {1,2,3,4,5,6} 485 | => array == {1,2,3,4,5,6} 486 | ``` 487 | 488 | ## shift 489 | 490 | `_.shift(array)` 491 | 492 | Removes the last element from an array and returns that value. 493 | 494 | ```lua 495 | _.shift({1,2,3,4}) 496 | => 1 497 | ``` 498 | 499 | ## unshift 500 | 501 | `_.unshift(array, [elements])` 502 | 503 | Adds the list of elements on to the end of an array. 504 | 505 | 506 | ```lua 507 | local array = {1,2,3} 508 | _.unshift(array, 4,5,6) 509 | => {4,5,6,1,2,3} 510 | => array == {4,5,6,1,2,3} 511 | ``` 512 | 513 | ## slice 514 | 515 | `_.slice(array, begin, [end])` 516 | 517 | Returns a portion of the array that starts from begin and returning all the elemtns to the end of the array. If end is provided then it provideds the elements to that position. 518 | 519 | ```lua 520 | _.slice({1,2,3,4,5,6,7,8}, 4) 521 | => {4,5,6,7,8} 522 | _.slice({1,2,3,4,5,6,7,8}, 4, 6) 523 | => {4,5,6} 524 | ``` 525 | 526 | ## sort 527 | 528 | `_.sort(array, [compareFunction])` 529 | 530 | Sorts the elements in the array based on the `tostring` value of the element. For numerical values, this puts "80" before "9" in their lexical form. 531 | 532 | With the compareFunction, the elements are sorted based on the returned value. This relies on Lua's underlying `table.sort` so the comparison relies the value being compared as less than or grater than. 533 | 534 | ```lua 535 | _.sort({"how","are","you","today"}) 536 | => {"are","how","today","you"} 537 | _.sort({1,5,10,90}) 538 | => {1,10,5,90} 539 | ``` 540 | 541 | ## concat 542 | 543 | `_.concat(value1, value2, ..., arrayn)` 544 | 545 | Creates a new array by concatenating the values passed in. It does not alter the original versions of the values passed in. 546 | 547 | ```lua 548 | _.concat(1,2,3,4,5,6) 549 | => {1,2,3,4,5,6} 550 | _.concat({1,2,3},{4,5,6}) 551 | => {1,2,3,4,5,6} 552 | ``` 553 | 554 | ## join 555 | 556 | `_.join(array, [separator])` 557 | 558 | Joins the elements of an array into a string. By default the separator is a blank string. If a separator is passed then it will be used as the string that separates the elements in the string. 559 | 560 | ```lua 561 | _.join({1,2,3,4,5}) 562 | => "12345" 563 | _.join({"Hello", "world"}, ", ") 564 | => "Hello, world" 565 | ``` 566 | 567 | ## splice 568 | 569 | `_.splice(array, index, [howMany, element1, element2, .., elementN])` 570 | 571 | Changes the content of an array, adding new elements while removing old elements. Will start changing elements starting at index and remove howMany elements from that position. Elements can be provided to replace the elements that are being removed. 572 | 573 | If hasMany is not specified it remove all elements to the end of the array. 574 | 575 | ```lua 576 | local kids = {'bart', 'marsha', 'maggie'} 577 | _.splice(kids, 2, 1, 'lisa') 578 | => {'marsha'} 579 | array == {'bart', 'lisa', 'maggie'} 580 | ``` 581 | 582 | #Functions 583 | 584 | ## memoize 585 | 586 | `_.memoize(function, [hashFunction])` 587 | 588 | Memoizes a given function by caching the computed result. Useful for speeding up slow-running computations. If passed an optional hashFunction, it will be used to compute the hash key for storing the result, based on the arguments to the original function. The default hashFunction just uses the first argument to the memoized function as the key. 589 | 590 | ```lua 591 | local fibonacci = _.memoize(function(n) 592 | if n < 2 then 593 | return n 594 | else 595 | return fibonacci(n - 1) + fibonacci(n - 2) 596 | end 597 | end) 598 | ``` 599 | 600 | ## once 601 | 602 | `_.once(function)` 603 | 604 | Creates a version of the function that can only be called one time. Repeated calls to the modified function will have no effect, returning the value from the original call. Useful for initialization functions, instead of having to set a boolean flag and then check it later. 605 | 606 | ```lua 607 | local initialize = _.once(createApplication) 608 | initialize() 609 | initialize() 610 | ``` 611 | 612 | ## after 613 | 614 | `_.after(count, function)` 615 | 616 | Creates a version of the function that will only be run after first being called count times. Useful for grouping asynchronous responses, where you want to be sure that all the async calls have finished, before proceeding. 617 | 618 | ```lua 619 | local renderNotes = _.after(_.size(notes), render) 620 | _.each(notes, function(note) 621 | note.asyncSave({success: renderNotes}) 622 | end) 623 | ``` 624 | 625 | ## bind 626 | 627 | `_.bind(function, table, [arguments])` 628 | 629 | Bind a function to a table, meaning that whenever the function is called, the value of self will be the table. Optionally, bind arguments to the function to pre-fill them, also known as partial application. 630 | 631 | ```lua 632 | local greet = function(self, greeting) return greeting .. ': ' .. self.name end 633 | 634 | func = _.bind(greet, {name = 'moe'}) 635 | func('hi') 636 | => 'hi: moe' 637 | 638 | func = _.bind(greet, {name = 'moe'}, 'hey') 639 | func() 640 | => 'hey: moe' 641 | ``` 642 | 643 | 644 | ## wrap 645 | 646 | `_.wrap(function, wrapper)` 647 | 648 | Wraps the first function inside of the wrapper function, passing it as the first argument. This allows the wrapper to execute code before and after the function runs, adjust the arguments, and execute it conditionally. 649 | 650 | ```lua 651 | local hello = function(name) return "hello: " + name end 652 | hello = _.wrap(hello, function(func) 653 | return "before, " + func("moe") + ", after" 654 | end) 655 | hello() 656 | => 'before, hello: moe, after' 657 | ``` 658 | 659 | ## compose 660 | 661 | `_.compose(*functions)` 662 | 663 | Returns the composition of a list of functions, where each function consumes the return value of the function that follows. In math terms, composing the functions f(), g(), and h() produces f(g(h())). 664 | 665 | ```lua 666 | local greet = function(name) return "hi: " + name end 667 | local exclaim = function(statement) return statement + "!" end 668 | local welcome = _.compose(exclaim, greet) 669 | welcome('moe') 670 | => 'hi: moe!' 671 | ``` 672 | 673 | #Objects 674 | 675 | ## keys 676 | 677 | `_.keys(object)` 678 | 679 | Retrieve all the names of the object's properties. The order of the keys is not guaranteed to be consistent. 680 | 681 | ```lua 682 | _.keys({one=1, two=2, three=3}) 683 | => {"one", "two", "three"} 684 | ``` 685 | ## values 686 | 687 | `_.values(object)` 688 | 689 | Return all of the values of the object's properties. The order of the values is not guaranteed to be consistent. 690 | 691 | ```lua 692 | _.values({one=1, two=2, three=3}) 693 | => {1, 2, 3} 694 | ``` 695 | 696 | ## pairs 697 | 698 | `_.pairs(object)` 699 | 700 | Convert an object into a list of [key, value] pairs. 701 | 702 | ```lua 703 | _.pairs({one=1, two=2, three=3}) 704 | => [["one", 1], ["two", 2], ["three", 3]] 705 | ``` 706 | 707 | ## invert 708 | 709 | `_.invert(object)` 710 | 711 | Returns a copy of the object where the keys have become the values and the values the keys. For this to work, all of your object's values should be unique and string serializable. 712 | 713 | ```lua 714 | _.invert({Moe="Moses", Larry="Louis", Curly="Jerome"}) 715 | => {Moses="Moe", Louis="Larry", Jerome="Curly"} 716 | ``` 717 | 718 | ## functions 719 | 720 | `_.functions(object)` Alias: methods 721 | 722 | Returns a sorted list of the names of every method in an object — that is to say, the name of every function property of the object. 723 | 724 | ```lua 725 | _.functions(_) 726 | => {"all", "any", "bind", "bindAll", "clone", "compact", "compose" ... } 727 | ``` 728 | 729 | ## extend 730 | 731 | `_.extend(destination, *sources)` 732 | 733 | Copy all of the properties in the source objects over to the destination object, and return the destination object. It's in-order, so the last source will override properties of the same name in previous arguments. 734 | 735 | ```lua 736 | _.extend({name='moe'}, {age=50}) 737 | => {name='moe', age=50} 738 | ``` 739 | 740 | ## pick 741 | 742 | `_.pick(object, *keys)` 743 | 744 | Return a copy of the object, filtered to only have values for the whitelisted keys (or array of valid keys). 745 | 746 | ```lua 747 | _.pick({name='moe', age=50, userid='moe1'}, 'name', 'age') 748 | => {name='moe', age=50} 749 | ``` 750 | 751 | ## omit 752 | 753 | `_.omit(object, *keys)` 754 | 755 | Return a copy of the object, filtered to omit the blacklisted keys (or array of keys). 756 | 757 | ```lua 758 | _.omit({name='moe', age=50, userid='moe1'}, 'userid') 759 | => {name='moe', age=50} 760 | ``` 761 | 762 | ## defaults 763 | 764 | `_.defaults(object, *defaults)` 765 | 766 | Fill in null and undefined properties in object with values from the defaults objects, and return the object. As soon as the property is filled, further defaults will have no effect. 767 | 768 | ```lua 769 | local iceCream = {flavor="chocolate"} 770 | _.defaults(iceCream, {flavor="vanilla", sprinkles="lots"}) 771 | => {flavor="chocolate", sprinkles="lots"} 772 | ``` 773 | 774 | ## clone 775 | 776 | `_.clone(object)` 777 | 778 | Create a shallow-copied clone of the object. Any nested objects or arrays will be copied by reference, not duplicated. 779 | 780 | ```lua 781 | _.clone({name='moe'}) 782 | => {name='moe'} 783 | ``` 784 | 785 | ## tap 786 | 787 | `_.tap(object, interceptor)` 788 | 789 | Invokes interceptor with the object, and then returns object. The primary purpose of this method is to "tap into" a method chain, in order to perform operations on intermediate results within the chain. 790 | 791 | ```lua 792 | _.chain([1,2,3,200]) 793 | :filter(function(num) return num % 2 == 0 end) 794 | :tap(print) 795 | -- {2, 200} 796 | :map(function(num) return num * num end) 797 | :value() 798 | => {4, 40000} 799 | ``` 800 | 801 | ## has 802 | 803 | `_.has(object, key)` 804 | 805 | Does the object contain the given key? Identical to object.hasOwnProperty(key), but uses a safe reference to the hasOwnProperty function, in case it's been overridden accidentally. 806 | 807 | ```lua 808 | _.has({a=1, b=2, c=3}, "b") 809 | => true 810 | ``` 811 | 812 | ## isEqual 813 | 814 | Not yet implemented 815 | 816 | ## isEmpty 817 | 818 | `_.isEmpty(object)` 819 | 820 | Returns true if object contains no values. 821 | 822 | ```lua 823 | _.isEmpty({1, 2, 3}) 824 | => false 825 | _.isEmpty({}) 826 | => true 827 | ``` 828 | 829 | ## isArray 830 | 831 | `_.isArray(object)` 832 | 833 | Returns true if object is an Array. 834 | 835 | ```lua 836 | _.isArray({1,2,3}) 837 | => true 838 | ``` 839 | 840 | ## isObject 841 | 842 | `_.isObject(value)` 843 | 844 | Returns true if value is an Object. Note that JavaScript arrays and functions are objects, while (normal) strings and numbers are not. 845 | 846 | ```lua 847 | _.isObject({}) 848 | => true 849 | _.isObject(1) 850 | => false 851 | ``` 852 | 853 | ## isFunction 854 | 855 | `_.isFunction(object)` 856 | 857 | Returns true if object is a Function. 858 | 859 | ```lua 860 | _.isFunction(print) 861 | => true 862 | ``` 863 | 864 | ## isString 865 | 866 | `_.isString(object)` 867 | 868 | Returns true if object is a String. 869 | 870 | ```lua 871 | _.isString("moe") 872 | => true 873 | ``` 874 | 875 | ## isNumber 876 | 877 | `_.isNumber(object)` 878 | 879 | Returns true if object is a Number (including NaN). 880 | 881 | ```lua 882 | _.isNumber(8.4 * 5) 883 | => true 884 | ``` 885 | 886 | ## isFinite 887 | 888 | `_.isFinite(object)` 889 | 890 | Returns true if object is a finite Number. 891 | 892 | ```lua 893 | _.isFinite(-101) 894 | => true 895 | _.isFinite(math.huge) 896 | => false 897 | ``` 898 | 899 | ## isBoolean 900 | 901 | `_.isBoolean(object)` 902 | 903 | Returns true if object is either true or false. 904 | 905 | ```lua 906 | _.isBoolean(nil) 907 | => false 908 | ``` 909 | 910 | ## isNaN 911 | 912 | `_.isNaN(object)` 913 | 914 | Returns true if object is NaN. 915 | 916 | ```lua 917 | _.isNaN(0/0) 918 | => true 919 | _.isNaN(1) 920 | => false 921 | ``` 922 | 923 | ## isNil 924 | 925 | `_.isNil(object)` 926 | 927 | Returns true if the value of object is nil. 928 | 929 | ```lua 930 | _.isNil(nil) 931 | => true 932 | _.isNil(1) 933 | => false 934 | ``` 935 | 936 | #Utility 937 | 938 | ## identity 939 | 940 | `_.identity(value)` 941 | 942 | Returns the same value that is used as the argument. In math: f(x) = x 943 | 944 | This function looks useless, but is used throughout Underscore as a default iterator. 945 | 946 | ```lua 947 | local moe = {name='moe'} 948 | moe == _.identity(moe); 949 | => true 950 | ``` 951 | 952 | ## times 953 | 954 | `_.times(n, iterator)` 955 | 956 | Invokes the given iterator function n times. Each invocation of iterator is called with an index argument. 957 | 958 | ```lua 959 | _(3).times(function(n) genie.grantWishNumber(n) end) 960 | ``` 961 | 962 | ## random 963 | 964 | `_.random(min, max)` 965 | 966 | Returns a random integer between min and max, inclusive. If you only pass one argument, it will return a number between 0 and that number. 967 | 968 | ```lua 969 | _.random(0, 100) 970 | => 42 971 | ``` 972 | 973 | ## mixin 974 | 975 | `_.mixin(object)` 976 | 977 | Allows you to extend Underscore with your own utility functions. Pass a hash of {name: function} definitions to have your functions added to the Underscore object, as well as the OOP wrapper. 978 | 979 | ```lua 980 | _.mixin({ 981 | capitalize=function(s) 982 | return s:substr(1,1):upper() .. s:substr(2):lower() 983 | end 984 | }) 985 | _("fabio").capitalize(); 986 | => "Fabio" 987 | ``` 988 | 989 | ## uniqueId 990 | 991 | `_.uniqueId([prefix])` 992 | 993 | Generate a globally-unique id for client-side models or DOM elements that need one. If prefix is passed, the id will be appended to it. Without prefix, returns an integer. 994 | 995 | ```lua 996 | _.uniqueId('contact_') 997 | => 'contact_104' 998 | ``` 999 | 1000 | ## escape 1001 | 1002 | `_.escape(string)` 1003 | 1004 | Escapes a string for insertion into HTML, replacing &, <, >, ", ', and / characters. 1005 | 1006 | ```lua 1007 | _.escape('Curly, Larry & Moe') 1008 | => "Curly, Larry & Moe" 1009 | ``` 1010 | 1011 | ## unescape 1012 | 1013 | `_.unescape(string)` 1014 | 1015 | Un-escapes a string from HTML to the proper characters &, <, >, ", ', and /. 1016 | 1017 | ```lua 1018 | _.unescape('Curly, Larry & Moe') 1019 | => "Curly, Larry & Moe" 1020 | ``` 1021 | 1022 | ## result 1023 | 1024 | `_.result(object, property)` 1025 | 1026 | If the value of the named property is a function then invoke it; otherwise, return it. 1027 | 1028 | ```lua 1029 | var object = {cheese='crumpets', stuff=function() return 'nonsense' end} 1030 | _.result(object, 'cheese') 1031 | => "crumpets" 1032 | _.result(object, 'stuff') 1033 | => "nonsense" 1034 | ``` 1035 | 1036 | # String 1037 | 1038 | ## split 1039 | 1040 | `_.split(value, [separator])` 1041 | 1042 | Splits a string into an array of strings by separating the string into substrings. If there is no separator is passed, the substring are individual characters. 1043 | 1044 | With a separator, the substring is string up to the separator position. The separator can be a [string pattern](http://www.lua.org/manual/5.2/manual.html#6.4.1). 1045 | 1046 | ```lua 1047 | _.split("John Smith") 1048 | => {"J","o","h","n"," ","S","m","i","t","h"} 1049 | _.split("John Smith", "%s+") 1050 | => {"John", "Smith"} 1051 | ``` 1052 | 1053 | #Chaining 1054 | 1055 | You can use Underscore in either an object-oriented or a functional style, depending on your preference. The following two lines of code are identical ways to double a list of numbers. 1056 | 1057 | ```lua 1058 | _.map([1, 2, 3], function(n) return n * 2 end) 1059 | _([1, 2, 3]).map(function(n) return n * 2 end) 1060 | ``` 1061 | 1062 | Calling chain will cause all future method calls to return wrapped objects. When you've finished the computation, use value to retrieve the final value. Here's an example of chaining together a map/flatten/reduce, in order to get the word count of every word in a song. 1063 | 1064 | ```lua 1065 | local lyrics = { 1066 | {line=1, words="I'm a lumberjack and I'm okay"}, 1067 | {line=2, words="I sleep all night and I work all day"}, 1068 | {line=3, words="He's a lumberjack and he's okay"}, 1069 | {line=4, words="He sleeps all night and he works all day"} 1070 | } 1071 | 1072 | _.chain(lyrics) 1073 | :map(function(line) return _.split(line.words, ' ') end) 1074 | :flatten() 1075 | :reduce(function(counts, word) 1076 | counts[word] = counts[word] or 0 1077 | counts[word] = counts[word] + 1 1078 | return counts 1079 | end, {}) 1080 | :value() 1081 | 1082 | => {lumberjack=2, all=4, night=2 ... } 1083 | ``` 1084 | 1085 | ## chain 1086 | 1087 | `_.chain(obj)` 1088 | 1089 | Returns a wrapped object. Calling methods on this object will continue to return wrapped objects until value is used. 1090 | 1091 | ```lua 1092 | local stooges = {{name='curly', age=25}, {name='moe', age=21}, {name='larry', age=23}} 1093 | local youngest = _.chain(stooges) 1094 | :sortBy(function(stooge) return stooge.age end) 1095 | :map(function(stooge) return stooge.name .. ' is ' .. stooge.age end) 1096 | :first() 1097 | :value() 1098 | => "moe is 21" 1099 | ``` 1100 | 1101 | ## value 1102 | 1103 | `_(obj).value()` 1104 | 1105 | Extracts the value of a wrapped object. 1106 | 1107 | ```lua 1108 | _({1, 2, 3}).value(); 1109 | => {1, 2, 3} 1110 | ``` 1111 | 1112 | 1113 | #Changelog 1114 | -------------------------------------------------------------------------------- /lib/underscore.lua: -------------------------------------------------------------------------------- 1 | local table, ipairs, pairs, math, string = table, ipairs, pairs, math, string 2 | 3 | local _ = {} 4 | local chainable_mt = {} 5 | 6 | _.identity = function(value) return value end 7 | 8 | function _.reverse(list) 9 | if _.isString(list) then 10 | return _(list).chain():split():reverse():join():value() 11 | else 12 | local length = _.size(list) 13 | for i = 1, length / 2, 1 do 14 | list[i], list[length-i+1] = list[length-i+1], list[i] 15 | end 16 | return list 17 | end 18 | end 19 | 20 | function _.splice(list, index, howMany, ...) 21 | if not _.isArray(list) then return nil end 22 | 23 | local elements = {...} 24 | local removed = {} 25 | 26 | howMany = howMany or #list - index + 1 27 | 28 | for i = 1, #elements, 1 do 29 | table.insert(list, i + index + howMany - 1, elements[i]) 30 | end 31 | 32 | for i = index, index + howMany - 1, 1 do 33 | table.insert(removed, table.remove(list, index)) 34 | end 35 | 36 | return removed 37 | end 38 | 39 | function _.pop(list) 40 | return table.remove(list, #list) 41 | end 42 | 43 | function _.push(list, ...) 44 | local values = {...} 45 | _.each(values, function(v) 46 | table.insert(list, v) 47 | end) 48 | return list 49 | end 50 | 51 | function _.shift(list) 52 | return table.remove(list, 1) 53 | end 54 | 55 | function _.unshift(list, ...) 56 | local values = {...} 57 | _.each(_.reverse(values), function(v) 58 | table.insert(list, 1, v) 59 | end) 60 | 61 | return list 62 | end 63 | 64 | function _.sort(list, func) 65 | func = func or function(a,b) 66 | return tostring(a) < tostring(b) 67 | end 68 | 69 | table.sort(list, func) 70 | return list 71 | end 72 | 73 | function _.join(list, separator) 74 | separator = separator or "" 75 | return table.concat(list,separator) 76 | end 77 | 78 | function _.slice(list, start, stop) 79 | local array = {} 80 | stop = stop or #list 81 | 82 | for index = start, stop, 1 do 83 | table.insert(array, list[index]) 84 | end 85 | 86 | return array 87 | end 88 | 89 | function _.concat(...) 90 | local values = _.flatten({...}, true) 91 | local cloned = {} 92 | 93 | _.each(values, function(v) 94 | table.insert(cloned, v) 95 | end) 96 | 97 | return cloned 98 | end 99 | 100 | function _.each(list, func) 101 | local pairing = pairs 102 | if _.isArray(list) then pairing = ipairs end 103 | 104 | for index, value in pairing(list) do 105 | func(value, index, list) 106 | end 107 | end 108 | 109 | function _.map(list, func) 110 | local new_list = {} 111 | _.each(list, function(value, key, original_list) 112 | table.insert(new_list, func(value, key, original_list)) 113 | end) 114 | 115 | return new_list 116 | end 117 | 118 | function _.reduce(list, func, memo) 119 | local init = memo == nil 120 | _.each(list, function(value, key, object) 121 | if init then 122 | memo = value 123 | init = false 124 | else 125 | memo = func(memo, value, key, object) 126 | end 127 | end) 128 | 129 | if init then 130 | error("Reduce of empty array with no initial value") 131 | end 132 | 133 | return memo 134 | end 135 | 136 | function _.reduceRight(list, func, memo) 137 | local init = memo == nil 138 | _.each(_.reverse(list), function(value, key, object) 139 | if init then 140 | memo = value 141 | init = false 142 | else 143 | memo = func(memo, value, key, object) 144 | end 145 | end) 146 | 147 | if init then 148 | error("Reduce of empty array with no initial value") 149 | end 150 | 151 | return memo 152 | end 153 | 154 | function _.find(list, func) 155 | if func == nil then return nil end 156 | 157 | local result = nil 158 | _.any(list, function(value, key, object) 159 | if func(value, key, object) then 160 | result = value 161 | return true 162 | end 163 | end) 164 | 165 | return result 166 | end 167 | 168 | function _.select(list, func) 169 | local found = {} 170 | _.each(list, function(value, key, object) 171 | if func(value, key, object) then 172 | table.insert(found, value) 173 | end 174 | end) 175 | 176 | return found 177 | end 178 | 179 | function _.reject(list, func) 180 | local found = {} 181 | _.each(list, function(value, key, object) 182 | if not func(value, key, object) then 183 | table.insert(found, value) 184 | end 185 | end) 186 | 187 | return found 188 | end 189 | 190 | function _.all(list, func) 191 | if _.isEmpty(list) then return false end 192 | 193 | func = func or _.identity 194 | 195 | local found = true 196 | _.each(list, function(value, index, object) 197 | if found and not func(value, index, object) then 198 | found = false 199 | end 200 | end) 201 | 202 | return found 203 | end 204 | 205 | function _.any(list, func) 206 | if _.isEmpty(list) then return false end 207 | 208 | func = func or _.identity 209 | 210 | local found = false 211 | _.each(list, function(value, index, object) 212 | if not found and func(value, index, object) then 213 | found = true 214 | end 215 | end) 216 | 217 | return found 218 | end 219 | 220 | function _.include(list, v) 221 | return _.any(list, function(value) 222 | return v == value 223 | end) 224 | end 225 | 226 | function _.pluck(list, key) 227 | local found = {} 228 | _.each(list, function(value) 229 | table.insert(found, value[key]) 230 | end) 231 | 232 | return found 233 | end 234 | 235 | function _.where(list, properties) 236 | local found = {} 237 | return _.select(list, function(value) 238 | return _.all(properties, function(v, k) 239 | return value[k] == v 240 | end) 241 | end) 242 | end 243 | 244 | function _.findWhere(list, properties) 245 | return _.first( 246 | _.where(list, properties) 247 | ) 248 | end 249 | 250 | function _.max(list, func) 251 | if _.isEmpty(list) then 252 | return -math.huge 253 | elseif _.isFunction(func) then 254 | local max = {computed=-math.huge} 255 | _.each(list, function(value, key, object) 256 | local computed = func(value, key, object) 257 | if computed >= max.computed then 258 | max = {computed=computed, value=value} 259 | end 260 | end) 261 | return max.value 262 | else 263 | return math.max(unpack(list)) 264 | end 265 | end 266 | 267 | function _.min(list, func) 268 | if _.isEmpty(list) then 269 | return math.huge 270 | elseif _.isFunction(func) then 271 | local min = {computed=math.huge} 272 | _.each(list, function(value, key, object) 273 | local computed = func(value, key, object) 274 | if computed < min.computed then 275 | min = {computed=computed, value=value} 276 | end 277 | end) 278 | return min.value 279 | else 280 | return math.min(unpack(list)) 281 | end 282 | end 283 | 284 | function _.invoke(list, func, ...) 285 | local invoke_func, args = func, {...} 286 | 287 | if _.isString(func) then 288 | invoke_func = function(value) 289 | return value[func](value, unpack(args)) 290 | end 291 | end 292 | 293 | return _.collect(list, function(value) 294 | return invoke_func(value, unpack(args)) 295 | end) 296 | end 297 | 298 | function _.sortBy(list, func) 299 | func = func or _.identity 300 | local sorted_func = function(a,b) 301 | if a == nil then return false end 302 | if b == nil then return true end 303 | return func(a) < func(b) 304 | end 305 | 306 | if _.isString(func) then 307 | sorted_func = function(a,b) 308 | return a[func](a) < b[func](b) 309 | end 310 | end 311 | 312 | table.sort(list, sorted_func) 313 | return list 314 | end 315 | 316 | function _.groupBy(list, func) 317 | local group_func, result = func, {} 318 | 319 | if _.isString(func) then 320 | group_func = function(v) 321 | return v[func](v) 322 | end 323 | end 324 | 325 | _.each(list, function(value, key, object) 326 | local key = group_func(value, key, object) 327 | result[key] = result[key] or {} 328 | table.insert(result[key], value) 329 | end) 330 | 331 | return result 332 | end 333 | 334 | function _.countBy(list, func) 335 | local count_func, result = func, {} 336 | 337 | if _.isString(func) then 338 | count_func = function(v) 339 | return v[func](v) 340 | end 341 | end 342 | 343 | _.each(list, function(value, key, object) 344 | local key = count_func(value, key, object) 345 | result[key] = result[key] or 0 346 | result[key] = result[key] + 1 347 | end) 348 | 349 | return result 350 | end 351 | 352 | function _.shuffle(list) 353 | local rand, index, shuffled = 0, 1, {} 354 | _.each(list, function(value) 355 | rand = math.random(1, index) 356 | index = index + 1 357 | shuffled[index - 1] = shuffled[rand] 358 | shuffled[rand] = value 359 | end) 360 | 361 | return shuffled 362 | end 363 | 364 | function _.toArray(list, ...) 365 | if not list then return {} end 366 | if not _.isObject(list) then list = {list, ...} end 367 | 368 | local cloned = {} 369 | _.each(list, function(value) 370 | table.insert(cloned, value) 371 | end) 372 | 373 | return cloned 374 | end 375 | 376 | function _.size(list, ...) 377 | local args = {...} 378 | 379 | if _.isArray(list) then 380 | return #list 381 | elseif _.isObject(list) then 382 | local length = 0 383 | _.each(list, function() length = length + 1 end) 384 | return length 385 | elseif _.isString(list) then 386 | return string.len(list) 387 | elseif not _.isEmpty(args) then 388 | return _.size(args) + 1 389 | end 390 | 391 | return 0 392 | end 393 | 394 | function _.memoize(func) 395 | local list = {} 396 | 397 | return function(...) 398 | if not list[...] then 399 | list[...] = func(...) 400 | end 401 | 402 | return list[...] 403 | end 404 | end 405 | 406 | function _.once(func) 407 | local called = false 408 | return function(...) 409 | if not called then 410 | called = true 411 | return func(...) 412 | end 413 | end 414 | end 415 | 416 | function _.after(times, func) 417 | if times <= 0 then return func() end 418 | 419 | return function(...) 420 | times = times - 1 421 | if times < 1 then 422 | return func(...) 423 | end 424 | end 425 | end 426 | 427 | function _.bind(func, context, ...) 428 | local arguments = {...} 429 | return function(...) 430 | return func(context, unpack(_.concat(arguments, {...}))) 431 | end 432 | end 433 | 434 | function _.partial(func, ...) 435 | local args = {...} 436 | return function(self, ...) 437 | return func(self, unpack(_.concat(args, {...}))) 438 | end 439 | end 440 | 441 | function _.wrap(func, wrapper) 442 | return function(...) 443 | return wrapper(func, ...) 444 | end 445 | end 446 | 447 | function _.compose(...) 448 | local funcs = {...} 449 | 450 | return function(...) 451 | local args = {...} 452 | 453 | _.each(_.reverse(funcs), function(func) 454 | args = {func(unpack(args))} 455 | end) 456 | 457 | return args[1] 458 | end 459 | end 460 | 461 | function _.range(...) 462 | local args = {...} 463 | local start, stop, step = unpack(args) 464 | 465 | if #args <= 1 then 466 | stop = start or 0 467 | start = 0 468 | end 469 | step = args[3] or 1 470 | 471 | local length, index, array = 472 | math.max(math.ceil((stop - start) / step), 0), 473 | 0, {} 474 | 475 | while index < length do 476 | table.insert(array, start) 477 | index = index + 1 478 | start = start + step 479 | end 480 | 481 | return array 482 | end 483 | 484 | function _.first(list, count) 485 | if not list then return nil end 486 | 487 | if not count then 488 | return list[1] 489 | else 490 | return _.slice(list, 1, count) 491 | end 492 | end 493 | 494 | function _.rest(list, start) 495 | start = start or 2 496 | 497 | return _.slice(list, start, #list) 498 | end 499 | 500 | function _.initial(list, stop) 501 | stop = stop or (#list - 1) 502 | 503 | return _.slice(list, 1, stop) 504 | end 505 | 506 | function _.last(list, count) 507 | if not list then return nil end 508 | 509 | if not count then 510 | return list[#list] 511 | else 512 | local start, stop, array = #list - count + 1, #list, {} 513 | return _.slice(list, start, stop) 514 | end 515 | end 516 | 517 | function _.compact(list) 518 | return _.filter(list, function(v) 519 | return not not v 520 | end) 521 | end 522 | 523 | function _.flatten(list, shallow, output) 524 | output = output or {} 525 | 526 | _.each(list, function(value) 527 | if _.isArray(value) then 528 | if shallow then 529 | _.each(value, function(v) table.insert(output, v) end) 530 | else 531 | _.flatten(value, false, output) 532 | end 533 | else 534 | table.insert(output, value) 535 | end 536 | end) 537 | 538 | return output 539 | end 540 | 541 | function _.without(list, ...) 542 | local args = {...} 543 | 544 | return _.difference(list, args) 545 | end 546 | 547 | function _.uniq(list, sorted, iterator) 548 | local initial, results, seen = list, {}, {} 549 | if iterator then 550 | initial = _.map(list, iterator) 551 | end 552 | 553 | _.each(initial, function(value, index) 554 | if (sorted and (index==1 or seen[#seen]~=value)) or (not _.contains(seen, value)) then 555 | table.insert(seen, value) 556 | table.insert(results, list[index]) 557 | end 558 | end) 559 | 560 | return results 561 | end 562 | 563 | function _.indexOf(list, value, start) 564 | if not list then return 0 end 565 | start = start or 1 566 | 567 | for index = start, #list, 1 do 568 | if value == list[index] then 569 | return index 570 | end 571 | end 572 | 573 | return 0 574 | end 575 | 576 | function _.intersection(a, ...) 577 | local b = {...} 578 | return _.filter(_.uniq(a), function(item) 579 | return _.every(b, function(other) 580 | return _.indexOf(other, item) >= 1 581 | end) 582 | end) 583 | end 584 | 585 | function _.union(...) 586 | return _.uniq(_.flatten({...}, true)) 587 | end 588 | 589 | function _.difference(a, ...) 590 | local b = _.flatten({...}, true) 591 | return _.filter(a, function(value) 592 | return not _.contains(b, value) 593 | end) 594 | end 595 | 596 | function _.zip(...) 597 | local args = {...} 598 | local length = _.max(_.map(args, function(a) return #a end)) 599 | local results = {} 600 | 601 | for i=1, length, 1 do 602 | table.insert(results, _.pluck(args, i)) 603 | end 604 | 605 | return results 606 | end 607 | 608 | function _.object(list, values) 609 | if not list then return {} end 610 | 611 | local result = {} 612 | _.each(list, function(value, index) 613 | if values then 614 | result[value] = values[index] 615 | else 616 | result[value[1]] = value[2] 617 | end 618 | end) 619 | 620 | return result 621 | end 622 | 623 | function _.lastIndexOf(list, value, start) 624 | if not list then return 0 end 625 | start = start or #list 626 | 627 | for index = start, 1, -1 do 628 | if value == list[index] then 629 | return index 630 | end 631 | end 632 | 633 | return 0 634 | end 635 | 636 | function _.keys(list) 637 | if not _.isObject(list) then error("Not an object") end 638 | return _.map(list, function(_, key) 639 | return key 640 | end) 641 | end 642 | 643 | function _.values(list) 644 | if _.isArray(list) then return list end 645 | return _.map(list, function(value) 646 | return value 647 | end) 648 | end 649 | 650 | function _.pairs(list) 651 | return _.map(list, function(value, key) 652 | return {key, value} 653 | end) 654 | end 655 | 656 | function _.invert(list) 657 | local array = {} 658 | 659 | _.each(list, function(value, key) 660 | array[value] = key 661 | end) 662 | 663 | return array 664 | end 665 | 666 | function _.functions(list) 667 | local method_names = {} 668 | 669 | _.each(list, function(value, key) 670 | if _.isFunction(value) then 671 | table.insert(method_names, key) 672 | end 673 | end) 674 | 675 | return method_names 676 | end 677 | 678 | function _.extend(list, ...) 679 | local lists = {...} 680 | _.each(lists, function(source) 681 | _.each(source, function(value, key) 682 | list[key] = source[key] 683 | end) 684 | end) 685 | 686 | return list 687 | end 688 | 689 | function _.pick(list, ...) 690 | local keys = _.flatten({...}) 691 | 692 | local array = {} 693 | _.each(keys, function(key) 694 | if list[key] then 695 | array[key] = list[key] 696 | end 697 | end) 698 | 699 | return array 700 | end 701 | 702 | function _.omit(list, ...) 703 | local keys = _.flatten({...}) 704 | 705 | local array = {} 706 | _.each(list, function(value,key) 707 | if not _.contains(keys, key) then 708 | array[key] = list[key] 709 | end 710 | end) 711 | 712 | return array 713 | end 714 | 715 | function _.defaults(list, ...) 716 | local keys = {...} 717 | 718 | _.each(keys, function(source) 719 | _.each(source, function(value, key) 720 | if not list[key] then 721 | list[key] = value 722 | end 723 | end) 724 | end) 725 | 726 | return list 727 | end 728 | 729 | function _.clone(list) 730 | if not _.isObject(list) then return list end 731 | 732 | if _.isArray(list) then 733 | return _.slice(list, 1, #list) 734 | else 735 | return _.extend({}, list) 736 | end 737 | end 738 | 739 | function _.isNaN(value) 740 | return _.isNumber(value) and value ~= value 741 | end 742 | 743 | function _.isEmpty(value) 744 | if not value then 745 | return true 746 | elseif _.isArray(value) or _.isObject(value) then 747 | return next(value) == nil 748 | elseif _.isString(value) then 749 | return string.len(value) == 0 750 | else 751 | return false 752 | end 753 | end 754 | 755 | function _.isObject(value) 756 | return type(value) == "table" 757 | end 758 | 759 | function _.isArray(value) 760 | return type(value) == "table" and (value[1] or next(value) == nil) 761 | end 762 | 763 | function _.isString(value) 764 | return type(value) == "string" 765 | end 766 | 767 | function _.isNumber(value) 768 | return type(value) == "number" 769 | end 770 | 771 | function _.isFunction(value) 772 | return type(value) == "function" 773 | end 774 | 775 | function _.isFinite(value) 776 | return _.isNumber(value) and -math.huge < value and value < math.huge 777 | end 778 | 779 | function _.isBoolean(value) 780 | return type(value) == "boolean" 781 | end 782 | 783 | function _.isNil(value) 784 | return value == nil 785 | end 786 | 787 | function _.tap(value, func) 788 | func(value) 789 | return value 790 | end 791 | 792 | local function splitIterator(value, pattern, start) 793 | if pattern then 794 | return string.find(value, pattern, start) 795 | else 796 | if start > string.len(value) then 797 | return nil 798 | else 799 | return start+1, start 800 | end 801 | end 802 | end 803 | 804 | 805 | function _.split(value, pattern) 806 | if not _.isString(value) then return {} end 807 | local values = {} 808 | local start = 1 809 | local start_pattern, end_pattern = splitIterator(value, pattern, start) 810 | 811 | while start_pattern do 812 | table.insert( 813 | values, 814 | string.sub(value, start, start_pattern - 1) 815 | ) 816 | start = end_pattern + 1 817 | start_pattern, end_pattern = splitIterator(value, pattern, start) 818 | end 819 | 820 | if start <= string.len(value) then 821 | table.insert(values, string.sub(value, start)) 822 | end 823 | 824 | return values 825 | end 826 | 827 | function _.capitalize(str) 828 | str = tostring(str or "") 829 | return str:gsub("^%l", string.upper) 830 | end 831 | 832 | local function round(num, idp) 833 | local mult = 10^(idp or 0) 834 | return math.floor(num * mult + 0.5) / mult 835 | end 836 | 837 | function _.numberFormat(number, dec, dsep, tsep) 838 | if not _.isNumber(number) then return "" end 839 | dec = dec or 0 840 | dsep = dsep or '.' 841 | 842 | number = tostring(round(number, dec)) 843 | tsep = tsep or ',' 844 | 845 | local parts = _.split(number, '%.') 846 | local fnums = parts[1] 847 | 848 | local decimals = '' 849 | if dec and dec > 0 then 850 | decimals = dsep .. (parts[2] or string.rep('0', dec)) 851 | end 852 | local digits = fnums:reverse():gsub("(%d%d%d)", '%1' .. tsep):reverse() .. decimals 853 | if digits:sub(1,1) == tsep then 854 | return digits:sub(2) 855 | else 856 | return digits 857 | end 858 | end 859 | 860 | function _.chain(value) 861 | return _(value).chain() 862 | end 863 | 864 | local id_counter = -1 865 | function _.uniqueId(prefix) 866 | id_counter = id_counter + 1 867 | if prefix then 868 | return prefix .. id_counter 869 | else 870 | return id_counter 871 | end 872 | end 873 | 874 | function _.times(n, func) 875 | for i=0, (n-1), 1 do 876 | func(i) 877 | end 878 | end 879 | 880 | local result = function(self, obj) 881 | if _.isObject(self) and self._chain then 882 | return _(obj).chain() 883 | else 884 | return obj 885 | end 886 | end 887 | 888 | 889 | function _.mixin(obj) 890 | _.each(_.functions(obj), function(name) 891 | local func = obj[name] 892 | _[name] = func 893 | 894 | chainable_mt[name] = function(target, ...) 895 | local r = func(target._wrapped, ...) 896 | if _.include({'pop','shift'}, name) then 897 | return result(target, target._wrapped) 898 | else 899 | return result(target, r) 900 | end 901 | end 902 | end) 903 | end 904 | 905 | local entityMap = { 906 | escape={ 907 | ['&']='&', 908 | ['<']='<', 909 | ['>']='>', 910 | ['"']='"', 911 | ["'"]=''', 912 | ['/']='/' 913 | } 914 | } 915 | entityMap.unescape = _.invert(entityMap.escape) 916 | 917 | function _.escape(value) 918 | value = value or '' 919 | return value:gsub("[" .. _(entityMap.escape).chain():keys():join():value() .. "]", function(s) 920 | return entityMap['escape'][s] 921 | end) 922 | end 923 | 924 | function _.unescape(value) 925 | value = value or '' 926 | _.each(entityMap.unescape, function(escaped, key) 927 | value = value:gsub(key, function(s) 928 | return escaped 929 | end) 930 | end) 931 | 932 | return value 933 | end 934 | 935 | function _.result(obj, prop) 936 | if not obj then return nil end 937 | local value = obj[prop] 938 | 939 | if _.isFunction(value) then 940 | return value(obj) 941 | else 942 | return value 943 | end 944 | end 945 | 946 | function _.print_r (t, name, indent) 947 | local tableList = {} 948 | function table_r (t, name, indent, full) 949 | local serial=string.len(full) == 0 and name 950 | or type(name)~="number" and '["'..tostring(name)..'"]' or '['..name..']' 951 | io.write(indent,serial,' = ') 952 | if type(t) == "table" then 953 | if tableList[t] ~= nil then io.write('{}; -- ',tableList[t],' (self reference)\n') 954 | else 955 | tableList[t]=full..serial 956 | if next(t) then -- Table not empty 957 | io.write('{\n') 958 | for key,value in pairs(t) do table_r(value,key,indent..'\t',full..serial) end 959 | io.write(indent,'};\n') 960 | else io.write('{};\n') end 961 | end 962 | else io.write(type(t)~="number" and type(t)~="boolean" and '"'..tostring(t)..'"' 963 | or tostring(t),';\n') end 964 | end 965 | table_r(t,name or '__unnamed__',indent or '','') 966 | end 967 | 968 | _.collect = _.map 969 | _.inject = _.reduce 970 | _.foldl = _.reduce 971 | _.foldr = _.reduceRight 972 | _.detect = _.find 973 | _.filter = _.select 974 | _.every = _.all 975 | _.same = _.any 976 | _.contains = _.include 977 | _.head = _.first 978 | _.take = _.first 979 | _.drop = _.rest 980 | _.tail = _.rest 981 | _.methods = _.functions 982 | 983 | _.mixin(_) 984 | 985 | setmetatable(_,{ 986 | __call = function(target, ...) 987 | local wrapped = ... 988 | if _.isObject(wrapped) and wrapped._wrapped then return wrapped end 989 | 990 | local instance = setmetatable({}, {__index=chainable_mt}) 991 | instance.chain = function() 992 | instance._chain = true 993 | return instance 994 | end 995 | instance.value = function() return instance._wrapped end 996 | 997 | instance._wrapped = wrapped 998 | return instance 999 | end 1000 | }) 1001 | 1002 | return _ 1003 | -------------------------------------------------------------------------------- /spec/arrays_spec.lua: -------------------------------------------------------------------------------- 1 | local _ = require("lib/underscore") 2 | 3 | describe("#first", function() 4 | local numbers = {1,2,3} 5 | 6 | it("can pass an index to first", function() 7 | assert.same(_.first(numbers, 0), {}) 8 | assert.same(_.first(numbers, 2), {1,2}) 9 | assert.same(_.first(numbers, 5), {1,2,3}) 10 | end) 11 | 12 | it("is aliased as #head and #take", function() 13 | assert.same(_.head(numbers, 2), {1,2}) 14 | assert.same(_.take(numbers, 2), {1,2}) 15 | end) 16 | 17 | it("handles nil", function() 18 | assert.same(_.first(nil), nil) 19 | end) 20 | 21 | it("returns the first element", function() 22 | assert.same(_.first({{a=1,b=2},{c=2,d=3}}), {a=1,b=2}) 23 | end) 24 | end) 25 | 26 | describe("#rest", function() 27 | local numbers = {1,2,3,4} 28 | 29 | it("can take an index to rest", function() 30 | assert.same(_.rest(numbers), {2,3,4}) 31 | assert.same(_.rest(numbers,1), {1,2,3,4}) 32 | assert.same(_.rest(numbers,3), {3,4}) 33 | end) 34 | 35 | it("is aliased as #drop and #tail", function() 36 | assert.same(_.drop(numbers), {2,3,4}) 37 | assert.same(_.tail(numbers), {2,3,4}) 38 | end) 39 | end) 40 | 41 | describe("#initial", function() 42 | it("can take an index to #initial", function() 43 | local numbers = {1,2,3,4,5} 44 | assert.same(_.initial(numbers), {1,2,3,4}) 45 | assert.same(_.initial(numbers, 2), {1,2}) 46 | end) 47 | end) 48 | 49 | describe("#last", function() 50 | local numbers = {1,2,3} 51 | 52 | it("can take an index to #last", function() 53 | assert.same(_.last(numbers), 3) 54 | assert.same(_.last(numbers, 0), {}) 55 | assert.same(_.last(numbers, 2), {2,3}) 56 | assert.same(_.last(numbers, 5), {1,2,3}) 57 | end) 58 | 59 | it("handles nil", function() 60 | assert.same(_.last(nil), nil) 61 | end) 62 | end) 63 | 64 | describe("#compact", function() 65 | it("can trim falsy values", function() 66 | assert.same(_.compact({0,1,false,2,false,3}), {0,1,2,3}) 67 | end) 68 | end) 69 | 70 | describe("#flatten", function() 71 | local list = {1, {2}, {3, {{4}}}} 72 | 73 | it("flattens nested arrays", function() 74 | assert.same(_.flatten(list), {1,2,3,4}) 75 | end) 76 | 77 | it("flatten shallowly", function() 78 | assert.same(_.flatten(list, true), {1,2,3,{{4}}}) 79 | end) 80 | 81 | it("flatten empty", function() 82 | list[2] = {} 83 | assert.same(_.flatten(list), {1,3,4}) 84 | end) 85 | end) 86 | 87 | describe("#without", function() 88 | it("can remove all instances of an object", function() 89 | local list = {1,2,1,0,3,1,4} 90 | assert.same(_.without(list, 0, 1), {2,3,4}) 91 | end) 92 | 93 | it("uses object identity for comparison", function() 94 | local list = {{one=1},{two=2}} 95 | assert.same(_.without(list, {one=1}), {{one=1},{two=2}}) 96 | assert.same(_.without(list, list[1]), {{two=2}}) 97 | end) 98 | end) 99 | 100 | describe("#uniq", function() 101 | it("can find the uniq values of an unsorted array", function() 102 | local list = {1, 2, 1, 3, 1, 4} 103 | assert.same(_.uniq(list), {1,2,3,4}) 104 | end) 105 | 106 | it("can find the uniq values of an empty array", function() 107 | assert.same(_.uniq({}), {}) 108 | end) 109 | 110 | it("can find the uniq values of a sorted array", function() 111 | local list = {1, 1, 1, 2, 2, 3, 4} 112 | assert.same(_.uniq(list, true), {1,2,3,4}) 113 | end) 114 | 115 | it("can find the unique values using a custom iterator", function() 116 | local list = {{name="moe"},{name="curly"},{name="larry"},{name="curly"}} 117 | local iterator = function(v) return v.name end 118 | assert.same(_.map(_.uniq(list, false, iterator), iterator), {"moe", "curly", "larry"}) 119 | end) 120 | 121 | it("can use an iterator with a sorted array", function() 122 | local list = {1, 2, 2, 3, 4, 4} 123 | local iterator = function(v) return v + 1 end 124 | assert.same(_.uniq(list, true, iterator), {1,2,3,4}) 125 | end) 126 | end) 127 | 128 | describe("#intersection", function() 129 | it("can take the set intersection of two arrays", function() 130 | local a = {"moe","curly","larry"} 131 | local b = {"moe","groucho"} 132 | assert.same(_.intersection(a,b), {"moe"}) 133 | end) 134 | it("intersection with empty is empty", function() 135 | local a = {"moe","curly","larry"} 136 | local b = {"moe","groucho"} 137 | assert.same(_.intersection(a,{}), {}) 138 | assert.same(_.intersection(b,{}), {}) 139 | end) 140 | it("intersection with inclusion is smaller array", function() 141 | local a = {"moe","curly","larry"} 142 | local b = {"moe","curly"} 143 | assert.same(_.intersection(a,b), b) 144 | end) 145 | end) 146 | 147 | describe("#union", function() 148 | it("takes the union of a list of arrays", function() 149 | local result = _.union({1, 2, 3}, {2, 30, 1}, {1, 40}) 150 | assert.same(result, {1,2,3,30,40}) 151 | end) 152 | 153 | it("takes the union of a list of nested arrays", function() 154 | local result = _.union({1, 2, 3}, {2, 30, 1}, {1, 40, {1}}) 155 | assert.same(result, {1,2,3,30,40,{1}}) 156 | end) 157 | end) 158 | 159 | describe("#difference", function() 160 | it("takes the difference of two arrays", function() 161 | assert.same(_.difference({1,2,3},{2,30,40}), {1,3}) 162 | end) 163 | 164 | it("takes the difference of three arrays", function() 165 | assert.same(_.difference({1, 2, 3, 4}, {2, 30, 40}, {1, 11, 111}), {3, 4}) 166 | end) 167 | end) 168 | 169 | describe("#zip", function() 170 | it("zipped together arrays of different lengths", function() 171 | local names = {"moe","larry","curly"} 172 | local ages = {30,40,50} 173 | local leaders = {true} 174 | 175 | assert.same(_.zip(names, ages, leaders), { 176 | {"moe",30,true}, 177 | {"larry",40}, 178 | {"curly",50} 179 | }) 180 | end) 181 | end) 182 | 183 | describe("#object", function() 184 | it("zips two arrays together into an object", function() 185 | local result = _.object({'moe', 'larry', 'curly'}, {30, 40, 50}) 186 | assert.same(result, {["moe"]=30,["larry"]=40,["curly"]=50}) 187 | end) 188 | 189 | it("returns an empty list", function() 190 | assert.same(_.object(), {}) 191 | assert.same(_.object(nil), {}) 192 | end) 193 | 194 | it("zips an array of pairs together into an object", function() 195 | local result = _.object({{'one', 1}, {'two', 2}, {'three', 3}}) 196 | assert.same(result, {["one"]=1,["two"]=2,["three"]=3}) 197 | end) 198 | end) 199 | 200 | describe("#indexOf", function() 201 | it("can compute index of a value", function() 202 | local list = {1,2,3} 203 | assert.same(_.indexOf(list, 2), 2) 204 | assert.same(_.indexOf(list, 5), 0) 205 | end) 206 | 207 | it("support starting from an index", function() 208 | local list = {1, 2, 3, 1, 2, 3, 1, 2, 3} 209 | assert.same(_.indexOf(list, 2, 6), 8) 210 | end) 211 | 212 | it("handles nil properly", function() 213 | assert.equal(_.indexOf(nil, 0), 0) 214 | end) 215 | end) 216 | 217 | describe("#lastIndexOf", function() 218 | local numbers = {1, 0, 1, 0, 0, 1, 0, 0, 0} 219 | 220 | it("returns the last index from the right the element appears", function() 221 | assert.equal(_.lastIndexOf(numbers, 1), 6) 222 | assert.equal(_.lastIndexOf(numbers, 0), 9) 223 | end) 224 | 225 | it("supports starting from an index", function() 226 | assert.equal(_.lastIndexOf(numbers, 10), 0) 227 | end) 228 | 229 | it("handles nil properly", function() 230 | assert.equal(_.lastIndexOf(nil, 0), 0) 231 | end) 232 | end) 233 | 234 | describe("#range", function() 235 | it("generates an empty array", function() 236 | assert.same(_.range(), {}) 237 | assert.same(_.range(0), {}) 238 | end) 239 | 240 | it("generates a range for a positive integer", function() 241 | assert.same(_.range(4), {0,1,2,3}) 242 | end) 243 | 244 | it("generates a range between a & b where a < b", function() 245 | assert.same(_.range(5,8), {5,6,7}) 246 | end) 247 | 248 | it("generates an empty array when a & b where b > a", function() 249 | assert.same(_.range(8,5), {}) 250 | end) 251 | 252 | it("generates a range between a < b stepping with c", function() 253 | assert.same(_.range(3,10,3), {3,6,9}) 254 | end) 255 | 256 | it("generates an empty array when a < b with c > b", function() 257 | assert.same(_.range(3,10,15), {3}) 258 | end) 259 | 260 | it("generates a range where a > b stepping with c", function() 261 | assert.same(_.range(12,7,-2), {12,10,8}) 262 | end) 263 | 264 | it("does the final exmaple in the Python docs", function() 265 | assert.same(_.range(0,-10, -1), {0,-1,-2,-3,-4,-5,-6,-7,-8,-9}) 266 | end) 267 | end) 268 | 269 | describe("#pop", function() 270 | it("return nil when there array is empty", function() 271 | assert.same(_.pop({}), nil) 272 | end) 273 | 274 | it("removes and return the last element off the array", function() 275 | assert.same(_.pop({1,2,3,4,5}), 5) 276 | assert.same(_.pop({1,2,"a"}), "a") 277 | end) 278 | end) 279 | 280 | describe("#push", function() 281 | it("adds multiple values to an empty array", function() 282 | local obj = {} 283 | local result = _.push(obj,1,2,3,4) 284 | assert.equal(tostring(obj), tostring(result)) 285 | assert.same(result,{1,2,3,4}) 286 | end) 287 | 288 | it("adds values to the end of non-empty array", function() 289 | local obj = {1,2} 290 | local result = _.push(obj,"a", {"b"}) 291 | assert.equal(tostring(obj), tostring(result)) 292 | assert.same(result,{1,2,"a",{"b"}}) 293 | end) 294 | end) 295 | 296 | describe("#shift", function() 297 | it("returns nil from an empty array", function() 298 | assert.equals(_.shift({}), nil) 299 | end) 300 | 301 | it("removes and returns the first value of an array", function() 302 | assert.equals(_.shift({1,2,3}), 1) 303 | assert.equals(_.shift({"a","bc",3}), "a") 304 | end) 305 | end) 306 | 307 | describe("#sort", function() 308 | it("sorts a list by lexicalgraphically order", function() 309 | assert.same(_.sort({3,2,1}), {1,2,3}) 310 | assert.same(_.sort({"z","a","j"}), {"a","j","z"}) 311 | assert.same(_.sort({1,2,80,10}),{1,10,2,80}) 312 | end) 313 | 314 | it("can take a comparison function", function() 315 | local reverse = function(a,b) 316 | return tostring(a) > tostring(b) 317 | end 318 | assert.same(_.sort({1,2,80,10}, reverse),{80,2,10,1}) 319 | end) 320 | end) 321 | 322 | describe("#splice", function() 323 | it("can remove on element", function() 324 | local list = {1,2,3,4} 325 | local removed = _.splice(list,2,1) 326 | assert.same(list, {1,3,4}) 327 | assert.same(removed, {2}) 328 | end) 329 | it("can replace sub array", function() 330 | local list = {1,2,3,4} 331 | local removed = _.splice(list,2,1,8,9) 332 | assert.same(list, {1,8,9,3,4}) 333 | assert.same(removed, {2}) 334 | end) 335 | end) 336 | 337 | describe("#unshift", function() 338 | it("adds multiple values to an empty array", function() 339 | local obj = {} 340 | local result = _.unshift(obj,1,2,3,4) 341 | assert.equal(tostring(obj), tostring(result)) 342 | assert.same(result,{1,2,3,4}) 343 | end) 344 | 345 | it("adds values to the end of non-empty array", function() 346 | local obj = {1,2} 347 | local result = _.unshift(obj,"a", {"b"}) 348 | assert.equal(tostring(obj), tostring(result)) 349 | assert.same(result,{"a",{"b"},1,2}) 350 | end) 351 | end) 352 | 353 | describe("#concat", function() 354 | local alpha, num1, num2, num3 = {"a","b","c"}, {1,2,3}, {4,5,6}, {7,8,9} 355 | 356 | it("concats two arrays", function() 357 | assert.same(_.concat(alpha, num1), {"a","b","c",1,2,3}) 358 | end) 359 | 360 | it("concats three arrays", function() 361 | assert.same(_.concat(num1, num2, num3), {1,2,3,4,5,6,7,8,9}) 362 | end) 363 | 364 | it("does not change the original arrays", function() 365 | assert.same(num1, {1,2,3}) 366 | assert.same(num3, {7,8,9}) 367 | end) 368 | 369 | it("can handle non-array values", function() 370 | assert.same(_.concat(alpha, 1, {2,{3}}), {"a","b","c",1,2,{3}}) 371 | assert.same(_.concat(4, 5, 6 , 1, {2,{3}}), {4,5,6 ,1,2,{3}}) 372 | end) 373 | end) 374 | 375 | describe("#join", function() 376 | it("defaults to joining all values with no separator", function() 377 | assert.same(_.join({"moe", 30, "curly", 20}), "moe30curly20") 378 | assert.same(_.join({"a", "b", "c"}), "abc") 379 | end) 380 | 381 | it("allows the user to specify separator character", function() 382 | assert.same(_.join({"moe", 30, "curly", 20}, "\t"), "moe\t30\tcurly\t20") 383 | assert.same(_.join({"a", "b", "c"}, "..."), "a...b...c") 384 | end) 385 | 386 | it("joining single element shouldn't change", function() 387 | assert.same(_.join({"moe"}, "dontprintit"), "moe") 388 | end) 389 | end) 390 | 391 | describe("#slice", function() 392 | local array = {1,2,3,4,5,6} 393 | it("returns a subset of an array", function() 394 | assert.same(_.slice(array, 1,6), {1,2,3,4,5,6}) 395 | assert.same(_.slice(array, 2,5), {2,3,4,5}) 396 | assert.same(_.slice(array, 5,5), {5}) 397 | end) 398 | 399 | it("returns a from the start pos to the end of the array", function() 400 | assert.same(_.slice(array, 1), {1,2,3,4,5,6}) 401 | assert.same(_.slice(array, 2), {2,3,4,5,6}) 402 | assert.same(_.slice(array, 6), {6}) 403 | end) 404 | end) 405 | 406 | describe("#splice", function() 407 | it("removes 0 elements from index 2 and inserts 'drum'", function() 408 | local array = {"angel", "clown", "mandarin", "surgeon"} 409 | local removed = _.splice(array, 2, 0, "drum") 410 | assert.same(array, {"angel","drum", "clown", "mandarin", "surgeon"}) 411 | assert.same(removed, {}) 412 | end) 413 | 414 | it("removes 2 elements from index 1 and inserts 'parrot', 'blue', and 'grape'", function() 415 | local array = {"angel", "clown", "mandarin", "surgeon"} 416 | local removed = _.splice(array, 1, 2, "parrot", "blue", "grape") 417 | assert.same(array, {"parrot", "blue", "grape", "mandarin", "surgeon"}) 418 | assert.same(removed, {'angel', 'clown'}) 419 | end) 420 | 421 | it("removes all elements when no hasMany is ", function() 422 | local array = {"angel", "clown", "mandarin", "surgeon"} 423 | local removed = _.splice(array, 3) 424 | assert.same(removed, {"mandarin", "surgeon"}) 425 | assert.same(array, {'angel', 'clown'}) 426 | end) 427 | end) 428 | -------------------------------------------------------------------------------- /spec/chaining_spec.lua: -------------------------------------------------------------------------------- 1 | local _ = require("lib/underscore") 2 | 3 | describe("chaining of methods", function() 4 | describe("with #map, #flatten, and #reduce", function() 5 | local lyrics = { 6 | "I'm a lumberjack and I'm okay", 7 | "I sleep all night and I work all day", 8 | "He's a lumberjack and he's okay", 9 | "He sleeps all night and he works all day" 10 | } 11 | 12 | it("counted all the letters in a song", function() 13 | local counts = _(lyrics).chain() 14 | :map(function(line) return _.split(line) end) 15 | :flatten() 16 | :reduce(function(hash, l) 17 | hash[l] = hash[l] or 0 18 | hash[l] = hash[l] + 1 19 | return hash 20 | end, {}):value() 21 | 22 | assert.equals(counts['a'], 16) 23 | assert.equals(counts['e'], 10) 24 | end) 25 | end) 26 | 27 | describe("with #select, #reject, #sortBy", function() 28 | local numbers = {1,2,3,4,5,6,7,8,9,10} 29 | 30 | it("filtered and reversed the numbers", function() 31 | numbers = _(numbers).chain():select(function(n) 32 | return n % 2 ==0 33 | end):reject(function(n) 34 | return n % 4 == 0 35 | end):sortBy(function(n) 36 | return -n 37 | end):value() 38 | 39 | assert.same(numbers, {10,6,2}) 40 | end) 41 | end) 42 | 43 | describe("with #select, #reject, #sortBy in functional style", function() 44 | local numbers = {1,2,3,4,5,6,7,8,9,10} 45 | 46 | it("filtered and reversed the numbers", function() 47 | numbers = _.chain(numbers):select(function(n) 48 | return n % 2 ==0 49 | end):reject(function(n) 50 | return n % 4 == 0 51 | end):sortBy(function(n) 52 | return -n 53 | end):value() 54 | 55 | assert.same(numbers, {10,6,2}) 56 | end) 57 | end) 58 | 59 | describe("with #reverse, #concat, #unshift, #pop, #map", function() 60 | local numbers = {1,2,3,4,5} 61 | 62 | it("can chain together array functions", function() 63 | numbers = _(numbers).chain() 64 | :reverse() 65 | :concat({5,5,5}) 66 | :unshift(17) 67 | :pop() 68 | :map(function(n) return n * 2 end) 69 | :value() 70 | 71 | assert.same(numbers, {34,10,8,6,4,2,10,10}) 72 | end) 73 | end) 74 | end) 75 | -------------------------------------------------------------------------------- /spec/collections_spec.lua: -------------------------------------------------------------------------------- 1 | local _ = require("lib/underscore") 2 | 3 | describe("#each", function() 4 | it("provides the value and iteration count", function() 5 | local array = {} 6 | _.each({1,2,3}, function(value, index) 7 | assert.equals(value, index) 8 | table.insert(array, value) 9 | end) 10 | assert.same({1,2,3}, array) 11 | end) 12 | 13 | it("can reference the orignal table", function() 14 | _.each({1,2,3}, function(value, index, array) 15 | assert.equals(array[index], value) 16 | end) 17 | end) 18 | 19 | it("can iterate over objects", function() 20 | local array = {} 21 | _.each({one=1,two=2,three=3}, function(value) 22 | table.insert(array, value) 23 | end) 24 | assert.same({3,1,2}, array) 25 | end) 26 | end) 27 | 28 | describe("#map", function() 29 | it("doubled the numbers", function() 30 | local values = _.map({1,2,3}, function(i) return i * 2 end) 31 | assert.same(values, {2,4,6}) 32 | end) 33 | 34 | it("aliased to #collect", function() 35 | local values = _.collect({1,2,3}, function(i) return i * 2 end) 36 | assert.same(values, {2,4,6}) 37 | end) 38 | end) 39 | 40 | describe("#reduce", function() 41 | it("sums up values", function() 42 | local sum = _.reduce({1,2,3}, function(sum, value) return sum + value end, 0) 43 | assert.equals(sum, 6) 44 | end) 45 | 46 | it("has a default initial value for the first value", function() 47 | local sum = _.reduce({2,3,4}, function(sum, value) return sum + value end) 48 | assert.equals(sum, 9) 49 | end) 50 | 51 | it("has a default value with an object", function() 52 | local sum = _.reduce({foo=1,bar=2,baz=3}, function(memo, str) return memo + str end) 53 | assert.equals(sum, 6) 54 | end) 55 | 56 | it("aliased to #inject", function() 57 | local sum = _.inject({1,2,3}, function(sum, value) return sum + value end, 0) 58 | assert.equals(sum, 6) 59 | end) 60 | 61 | it("aliased to #foldl", function() 62 | local sum = _.foldl({1,2,3}, function(sum, value) return sum + value end, 0) 63 | assert.equals(sum, 6) 64 | end) 65 | 66 | it("raises an error with empty array and no initial value", function() 67 | assert.error(function() 68 | _.reduce({}) 69 | end) 70 | end) 71 | end) 72 | 73 | describe("#reduceRight", function() 74 | it("can reduce from the right", function() 75 | local list = _.reduceRight({"foo","bar","baz"}, function(memo, str) return memo .. str end, "") 76 | assert.equals(list, "bazbarfoo") 77 | end) 78 | 79 | it("has a default inital value for the first value", function() 80 | local list = _.reduceRight({"foo","bar","baz"}, function(memo, str) return memo .. str end) 81 | assert.equals(list, "bazbarfoo") 82 | end) 83 | 84 | it("aliased as #foldr", function() 85 | local list = _.foldr({"foo","bar","baz"}, function(memo, str) return memo .. str end) 86 | assert.equals(list, "bazbarfoo") 87 | end) 88 | 89 | it("has a default value with an object", function() 90 | local sum = _.reduceRight({foo=1,bar=2,baz=3}, function(memo, str) return memo + str end) 91 | assert.equals(sum, 6) 92 | end) 93 | 94 | it("raises an error with empty array and no initial value", function() 95 | assert.error(function() 96 | _.reduceRight({}) 97 | end) 98 | end) 99 | end) 100 | 101 | describe("#find", function() 102 | local array = {1,2,3,4} 103 | 104 | it("returns the first found value", function() 105 | local value = _.find(array, function(n) return n > 2 end) 106 | assert.equals(value, 3) 107 | end) 108 | 109 | it("returns nil if value is not found", function() 110 | local value = _.find(array, function(n) return false end) 111 | assert.equals(value, nil) 112 | end) 113 | 114 | it("aliased as #detect", function() 115 | local value = _.detect(array, function(n) return n > 2 end) 116 | assert.equals(value, 3) 117 | end) 118 | end) 119 | 120 | describe("#select", function() 121 | it("returns all the even numbers", function() 122 | local result = _.select({1,2,3,4,5,6}, function(value) return value % 2 == 0 end) 123 | assert.same({2,4,6}, result) 124 | end) 125 | 126 | it("aliased as #filter", function() 127 | local result = _.filter({1,2,3,4,5,6}, function(value) return value % 2 == 0 end) 128 | assert.same({2,4,6}, result) 129 | end) 130 | end) 131 | 132 | describe("#reject", function() 133 | it("returns a list of non even numbers", function() 134 | local result = _.reject({1,2,3,4,5,6}, function(value) return value % 2 == 0 end) 135 | assert.same({1,3,5}, result) 136 | end) 137 | end) 138 | 139 | describe("#all", function() 140 | it("handles true/false/nil values", function() 141 | assert.is.truthy(_.all({true, true, true})) 142 | assert.is_not.truthy(_.all({true, false, true})) 143 | assert.is_not.truthy(_.all({nil, nil, nil})) 144 | end) 145 | 146 | it("returns true for all even numbers", function() 147 | assert.truthy(_.all({2,4,6,8}, function(v) return v % 2 == 0 end)) 148 | assert.is_not.truthy(_.all({2,4,6,9}, function(v) return v % 2 == 0 end)) 149 | end) 150 | 151 | it("aliased as #every", function() 152 | assert.truthy(_.every({2,4,6,8}, function(v) return v % 2 == 0 end)) 153 | assert.is_not.truthy(_.every({2,4,6,9}, function(v) return v % 2 == 0 end)) 154 | end) 155 | end) 156 | 157 | describe("#any", function() 158 | it("returns true with any true value", function() 159 | assert.truthy(_.any({true, true, true})) 160 | assert.truthy(_.any({true, false, true})) 161 | assert.truthy(_.any({nil, true, nil})) 162 | end) 163 | 164 | it("return false without any true value", function() 165 | assert.is_not.truthy(_.any({false, false, false})) 166 | assert.is_not.truthy(_.any({nil, false, nil})) 167 | end) 168 | 169 | it("returns true for any even numbers", function() 170 | assert.truthy(_.any({0,4,7,13}, function(v) return v % 2 == 0 end)) 171 | assert.is_not.truthy(_.any({1,3,9}, function(v) return v % 2 == 0 end)) 172 | end) 173 | 174 | it("aliased to #same", function() 175 | assert.truthy(_.same({0,4,7,13}, function(v) return v % 2 == 0 end)) 176 | assert.is_not.truthy(_.same({1,3,9}, function(v) return v % 2 == 0 end)) 177 | end) 178 | end) 179 | 180 | describe("#include", function() 181 | it("returns true if the value is in the array", function() 182 | assert.truthy(_.include({1,2,3}, 2)) 183 | assert.is_not.truthy(_.include({1,4,3}, 2)) 184 | end) 185 | 186 | it("returns true when the value is in the object", function() 187 | assert.truthy(_.include({one=1,two=2,three=3}, 2)) 188 | assert.is_not.truthy(_.include({one=1,four=4,three=3}, 2)) 189 | end) 190 | 191 | it("return true when values are string", function() 192 | local value = {} 193 | assert.truthy(_.include({"function","table"}, type(value))) 194 | end) 195 | 196 | it("aliased to #contains", function() 197 | assert.truthy(_.contains({1,2,3}, 2)) 198 | end) 199 | end) 200 | 201 | describe("#pluck", function() 202 | it("pulls names out of the object", function() 203 | local people = {{name="moe", age=30}, {name="curly", age=50}} 204 | assert.same(_.pluck(people, 'name'), {"moe","curly"}) 205 | end) 206 | end) 207 | 208 | describe("#where", function() 209 | local list = {{a=1, b=2}, {a=2, b=2}, {a=1, b=3}, {a=1, b=4}} 210 | 211 | it("returns a list of elements that matches the properties", function() 212 | local results = _.where(list, {a=1}) 213 | assert.equals(#results, 3) 214 | assert.equals(results[#results].b, 4) 215 | 216 | results = _.where(list, {b=2}) 217 | assert.equals(#results, 2) 218 | assert.equals(results[1].a, 1) 219 | end) 220 | end) 221 | 222 | describe("#max", function() 223 | it("returns a max number from a list", function() 224 | assert.equals(_.max({1,2,3}), 3) 225 | assert.equals(_.max({2,3,6,1}), 6) 226 | assert.equals(_.max({-1,8,-8}), 8) 227 | end) 228 | 229 | it("returns infinity for an empty array", function() 230 | assert.equals(_.max({}), -math.huge) 231 | end) 232 | 233 | it("performs a computation based max", function() 234 | assert.equals(_.max({1,2,3}, function(v) return -v end), 1) 235 | end) 236 | end) 237 | 238 | describe("#min", function() 239 | it("returns a min number from a list", function() 240 | assert.equals(_.min({1,2,3}), 1) 241 | assert.equals(_.min({2,3,6,1}), 1) 242 | assert.equals(_.min({-1,8,-8}), -8) 243 | end) 244 | 245 | it("returns infinity for an empty array", function() 246 | assert.equals(_.min({}), math.huge) 247 | end) 248 | 249 | it("performs a computation based max", function() 250 | assert.equals(_.min({1,2,3}, function(v) return -v end), 3) 251 | end) 252 | end) 253 | 254 | describe("#invoke", function() 255 | local values = {{2,1,3}, {6,5,4}} 256 | 257 | it("sorts the arrays together", function() 258 | local first = function(array) return array[1] end 259 | values[1].first = first 260 | values[2].first = first 261 | assert.same(_.invoke(values, 'first'), {2,6}) 262 | end) 263 | 264 | it("invokes the passed function", function() 265 | local second = function(array) return array[2] end 266 | assert.same(_.invoke(values, second), {1,5}) 267 | end) 268 | 269 | it("passes the arguments to the invoked function", function() 270 | local index = function(array, index) return array[index] end 271 | assert.same(_.invoke(values, index, 3), {3,4}) 272 | end) 273 | 274 | describe("when passing self into the invoke function", function() 275 | local object = {count=0} 276 | function object:incr() 277 | self.count = self.count + 1 278 | return self.count 279 | end 280 | 281 | it("passes the object the method is being invoked on", function() 282 | local array = {object, object, object} 283 | assert.same(_.invoke(array, 'incr'), {1,2,3}) 284 | end) 285 | end) 286 | end) 287 | 288 | describe("#sortBy", function() 289 | it("sorts by object properties", function() 290 | local people = {{name='curly', age=50}, {name='moe', age=30}} 291 | people = _.sortBy(people, function(person) return person.age end) 292 | assert.same(_.pluck(people, 'name'), {'moe', 'curly'}) 293 | end) 294 | 295 | it("sorts by value by default", function() 296 | local list = {4, 1, 3, 2} 297 | assert.same(_.sortBy(list), {1,2,3,4}); 298 | end) 299 | 300 | it("sorts by function name reference", function() 301 | local list = {"one", "two", "three", "four", "five"} 302 | local sorted = _.sortBy(list, 'len') 303 | assert.same(sorted, {'one','two','four','five','three'}) 304 | end) 305 | end) 306 | 307 | describe("#groupBy", function() 308 | it("groups even and odd number together", function() 309 | local list = {1,2,3,4,5,6,7,8} 310 | local grouped = _.groupBy(list, function(v) return v % 2 end) 311 | 312 | assert.same(grouped[0], {2,4,6,8}) 313 | assert.same(grouped[1], {1,3,5,7}) 314 | assert.equals(#grouped, 1) 315 | end) 316 | 317 | it("groups by length of the string", function() 318 | local list = {"one", "two", "three", "four", "five"} 319 | local grouped = _.groupBy(list, 'len') 320 | 321 | assert.same(grouped[3], {'one', 'two'}) 322 | assert.same(grouped[4], {'four', 'five'}) 323 | assert.same(grouped[5], {'three'}) 324 | end) 325 | end) 326 | 327 | describe("#countBy", function() 328 | it("counts even and odd number together", function() 329 | local list = {1,2,3,4,5,6,7,8, 9} 330 | local grouped = _.countBy(list, function(v) return v % 2 end) 331 | 332 | assert.same(grouped[0], 4) 333 | assert.same(grouped[1], 5) 334 | assert.equals(#grouped, 1) 335 | end) 336 | 337 | it("counts by length of the string", function() 338 | local list = {"one", "two", "three", "four", "five"} 339 | local grouped = _.countBy(list, 'len') 340 | 341 | assert.same(grouped[3], 2) 342 | assert.same(grouped[4], 2) 343 | assert.same(grouped[5], 1) 344 | end) 345 | end) 346 | 347 | describe("#reverse", function() 348 | it("reverses a string", function() 349 | assert.equal(_.reverse("racecar"), "racecar") 350 | assert.equal(_.reverse("walrus"), "surlaw") 351 | assert.equal(_.reverse("12345"), "54321") 352 | end) 353 | 354 | it("reverses the elements in an array", function() 355 | assert.same(_.reverse({1,2,3,4,5}),{5,4,3,2,1}) 356 | end) 357 | 358 | it("modifies the original array", function() 359 | local array = {1,2,3,4,5} 360 | assert.equals(tostring(_.reverse(array)), tostring(array)) 361 | end) 362 | end) 363 | 364 | describe("#shuffle", function() 365 | it("returns original list in random order", function() 366 | local list = {1,2,3,4,5,6,7,8,9,10} 367 | local shuffled = _.shuffle(list) 368 | 369 | assert.is_not.same(list, shuffled) 370 | table.sort(shuffled) 371 | assert.same(list, shuffled) 372 | end) 373 | end) 374 | 375 | describe("#toArray", function() 376 | it("clones the array", function() 377 | local list = {1,2,3,4,5} 378 | local cloned = _.toArray(list) 379 | 380 | assert.same(list, cloned) 381 | assert.is_not.equal(list, cloned) 382 | end) 383 | 384 | it("flattens an object to an array", function() 385 | local list = {one=1,two=2,three=3} 386 | local array = _.toArray(list) 387 | 388 | assert.is_not.equal(list, array) 389 | table.sort(array) 390 | assert.same(array, {1,2,3}) 391 | end) 392 | 393 | it("handles a list of arguments", function() 394 | local array = _.toArray(1,2,3,4) 395 | assert.same(array,{1,2,3,4}) 396 | end) 397 | end) 398 | 399 | describe("#size", function() 400 | it("returns the size of an object", function() 401 | assert.equals(_.size({one=1,two=2,three=3}), 3) 402 | end) 403 | 404 | it("returns the size of an array", function() 405 | assert.equals(_.size({1,2,3,4}), 4) 406 | end) 407 | 408 | it("returns the size of variable arguments", function() 409 | assert.equals(_.size(1,2,3,4,5), 5) 410 | end) 411 | 412 | it("returns the size of string", function() 413 | assert.equals(_.size("hello world"), 11) 414 | end) 415 | 416 | it("returns the size of nil", function() 417 | assert.equals(_.size(nil), 0) 418 | end) 419 | end) 420 | 421 | describe("#findWhere", function() 422 | local list = {{a=1, b=2}, {a=2, b=2}, {a=1, b=3}, {a=1, b=4}, {a=2, b=4}} 423 | it("finds the first value to match the key value pair", function() 424 | local result = _.findWhere(list, {a=1}) 425 | assert.same(result, {a=1, b=2}) 426 | result = _.findWhere(list, {b=4}) 427 | assert.same(result, {a=1, b=4}) 428 | end) 429 | end) 430 | 431 | -------------------------------------------------------------------------------- /spec/functions_spec.lua: -------------------------------------------------------------------------------- 1 | local _ = require("lib/underscore") 2 | 3 | describe("#memoize", function() 4 | function fib(n) 5 | if n < 2 then 6 | return n 7 | else 8 | return fib(n - 1) + fib(n - 2) 9 | end 10 | end 11 | 12 | it("returns a value of fib", function() 13 | local fast_fib = _.memoize(fib) 14 | assert.equal(fib(10), 55) 15 | assert.equal(fast_fib(10), 55) 16 | end) 17 | end) 18 | 19 | describe("#once", function() 20 | it("only invokes the function once", function() 21 | local num = 0 22 | local incr = _.once(function() num = num + 1 end) 23 | 24 | incr() 25 | incr() 26 | 27 | assert.equal(num, 1) 28 | end) 29 | end) 30 | 31 | describe("#after", function() 32 | local test_after = function(after_amount, times_called) 33 | local after_called = 0 34 | local after = _.after(after_amount, function() 35 | after_called = after_called + 1 36 | end) 37 | 38 | while times_called > 0 do 39 | after() 40 | times_called = times_called - 1 41 | end 42 | 43 | return after_called 44 | end 45 | 46 | it("should be called N times", function() 47 | assert.equal(test_after(5,5), 1) 48 | end) 49 | 50 | it("should not be called until N times", function() 51 | assert.equal(test_after(5,4), 0) 52 | end) 53 | 54 | it("should fire immediately", function() 55 | assert.equal(test_after(0,0), 1) 56 | end) 57 | end) 58 | 59 | describe("#bind", function() 60 | local table = {name = "moe"} 61 | local greet = function(self, greeting) return greeting .. ": " .. self.name end 62 | it("returns a closure that binds a function to a table scope ", function() 63 | local binded = _.bind(greet, table) 64 | assert.equals(binded("hi"), "hi: moe") 65 | end) 66 | 67 | it("creates a partial application by pre-filling arguments", function() 68 | local binded = _.bind(greet, table, "hi") 69 | assert.equals(binded(), "hi: moe") 70 | end) 71 | 72 | it("can pass multiple arguments with arrays", function() 73 | local obj = {} 74 | function obj:test(p1, p2) 75 | assert.same(p1, {1,2}) 76 | assert.same(p2, {3,4}) 77 | end 78 | 79 | local binded = _.bind(obj.test, obj) 80 | binded({1,2},{3,4}) 81 | end) 82 | end) 83 | 84 | describe("#wrap", function() 85 | it("passes arguments ", function() 86 | local greet = function(name) return "hi: " .. name end 87 | local backwards = _.wrap(greet, function(func, name) 88 | return func(name) .. " " .. string.reverse(name) 89 | end) 90 | 91 | assert.equals(backwards("moe"), "hi: moe eom") 92 | end) 93 | end) 94 | 95 | describe("#compose", function() 96 | local greet = function(name) return "hi: " .. name end 97 | local exclaim = function(sentence) return sentence .. "!" end 98 | 99 | it("can compose a function that takes another", function() 100 | local composed = _.compose(greet, exclaim) 101 | assert.equal(composed("moe"), "hi: moe!") 102 | end) 103 | 104 | it("compose functions that are commutative", function() 105 | local composed = _.compose(exclaim, greet) 106 | assert.equal(composed("moe"), "hi: moe!") 107 | end) 108 | 109 | it("composes multiple functions", function() 110 | local quoted = function(sentence) return "'" .. sentence .. "'" end 111 | local composed = _.compose(quoted, exclaim, greet) 112 | assert.equal(composed("moe"), "'hi: moe!'") 113 | end) 114 | end) 115 | 116 | describe("#partial", function() 117 | it("can partially apply arguments to a function", function() 118 | local obj = {name='moe'} 119 | local func = function(self, a , b, c, d) 120 | return self.name .. ' ' .. _({a,b,c,d}):join(' ') 121 | end 122 | 123 | obj.func = _.partial(func, 'a', 'b') 124 | assert.equals(obj:func('c', 'd'), 'moe a b c d') 125 | end) 126 | end) 127 | -------------------------------------------------------------------------------- /spec/objects_spec.lua: -------------------------------------------------------------------------------- 1 | local _ = require("lib/underscore") 2 | 3 | describe("#keys", function() 4 | it("can extract keys from an object", function() 5 | assert.truthy(_.keys({one=1,two=2}),{'one','two'}) 6 | assert.truthy(_.keys({moe=10}),{'moe'}) 7 | end) 8 | 9 | it("throws a type error", function() 10 | assert.has.error(function() _.keys(nil) end) 11 | assert.has.error(function() _.keys(1) end) 12 | assert.has.error(function() _.keys('a') end) 13 | assert.has.error(function() _.keys(true) end) 14 | end) 15 | end) 16 | 17 | describe("#values", function() 18 | it("can extract values from an object", function() 19 | assert.truthy(_.values({one=10,two=20}), {10,20}) 20 | assert.truthy(_.values({moe=1,curly=20,joe="joe"}), {"joe",20,1}) 21 | end) 22 | 23 | it("returns the original values of an array", function() 24 | local list = {1,2,3} 25 | assert.equal(_.values(list), list) 26 | end) 27 | end) 28 | 29 | describe("#pairs", function() 30 | it("can convert an object into pairs", function() 31 | assert.same(_.pairs({one=1,two=2}),{{'one',1},{'two',2}}) 32 | assert.same(_.pairs({moe=10,joe=20}),{{'joe',20},{'moe',10}}) 33 | end) 34 | end) 35 | 36 | describe("#invert", function() 37 | it("changes keys<->values position", function() 38 | local list = {one=1,two=2,three=3} 39 | 40 | assert.same(_.invert(list),{ 41 | [1]="one", 42 | [2]="two", 43 | [3]="three" 44 | }) 45 | 46 | assert.same(_.invert(_.invert(list)), list) 47 | end) 48 | end) 49 | 50 | describe("#functions", function() 51 | it("can grab the function names of any passed-in object", function() 52 | local list = { 53 | a='dash', 54 | b=_.map, 55 | c=123, 56 | d=_.reduce 57 | } 58 | 59 | assert.same(_.functions(list), {'b','d'}) 60 | assert.same(_.methods(list), {'b','d'}) 61 | end) 62 | end) 63 | 64 | describe("#extend", function() 65 | it("can extend an object with attributes of another", function() 66 | assert.same(_.extend({},{b=1}), {b=1}) 67 | end) 68 | 69 | it("properties in source override destination", function() 70 | assert.same(_.extend({a=1},{a=2}),{a=2}) 71 | end) 72 | 73 | it("properties not in source dont get overriden", function() 74 | assert.same(_.extend({a=1},{b=2}),{a=1,b=2}) 75 | end) 76 | 77 | it("can extend from multiple source objects", function() 78 | assert.same(_.extend({a=1},{b=2},{c=3}),{a=1,b=2,c=3}) 79 | end) 80 | 81 | it("extending from multiple source objects last property trumps", function() 82 | assert.same(_.extend({a=1},{b=2},{a=2}),{a=2,b=2}) 83 | end) 84 | end) 85 | 86 | describe("#pick", function() 87 | it("can restrict properties to those named", function() 88 | assert.same(_.pick({a=1,b=2,c=3},'a','c'),{a=1,c=3}) 89 | end) 90 | 91 | it("can restrict properties to those named in an array", function() 92 | assert.same(_.pick({a=1,b=2,c=3},{'b','c'}),{b=2,c=3}) 93 | end) 94 | 95 | it("can restrict properties to those named in mixed args", function() 96 | assert.same(_.pick({a=1,b=2,c=3},{'b'},'c'),{b=2,c=3}) 97 | end) 98 | end) 99 | 100 | describe("#omit", function() 101 | it("can omit a singled named property", function() 102 | assert.same(_.omit({a=1,b=2,c=3},'b'),{a=1,c=3}) 103 | end) 104 | 105 | it("can moit several named properties", function() 106 | assert.same(_.omit({a=1,b=2,c=3},'a','c'),{b=2}) 107 | end) 108 | 109 | it("can moit properties named in an array", function() 110 | assert.same(_.omit({a=1,b=2,c=3},{'b','c'}), {a=1}) 111 | end) 112 | end) 113 | 114 | describe("#defaults", function() 115 | local list = {zero=0,one=1,empty="",nan=0/0,string="string"} 116 | 117 | it("does not override already set values", function() 118 | local result = _.defaults(list,{zero=1,one=10,twenty=20}) 119 | assert.equals(result.zero, 0) 120 | assert.equals(result.one, 1) 121 | assert.equals(result.twenty, 20) 122 | end) 123 | 124 | it("handles multiple arguments by having first value win", function() 125 | local result = _.defaults(list, {word="word"},{word="dog"}) 126 | assert.equals(result.word, "word") 127 | end) 128 | 129 | it("does not overide empty values", function() 130 | local result = _.defaults(list, {empty="full"},{nan="nan"}) 131 | assert.equals(result.empty, "") 132 | assert.is.truthy(_.isNaN(result.nan)) 133 | end) 134 | end) 135 | 136 | describe("#clone", function() 137 | local list = {name="moe",lucky={13,27,34}} 138 | 139 | it("has the attributes of the original", function() 140 | local result = _.clone(list) 141 | assert.is_not.equals(list, result) 142 | assert.same(list, result) 143 | end) 144 | 145 | it("can change shallow attrbiutes of the clone without effecting the original", function() 146 | local result = _.clone(list) 147 | result.name = "curly" 148 | assert.is_not.equals(result.name, list.name) 149 | end) 150 | 151 | it("can change deep attributes of the clone that effect the original", function() 152 | local result = _.clone(list) 153 | table.insert(result.lucky, 101) 154 | assert.equals(_.last(list.lucky), 101) 155 | end) 156 | 157 | it("does not change non objects", function() 158 | assert.equals(_.clone(nil),nil) 159 | assert.equals(_.clone(1),1) 160 | assert.equals(_.clone("string"),"string") 161 | end) 162 | end) 163 | 164 | 165 | describe("#isEqual", function() 166 | 167 | end) 168 | 169 | describe("#isEmpty", function() 170 | it("returns false for non-empty values", function() 171 | assert.is_not.truthy(_.isEmpty({1})) 172 | assert.is_not.truthy(_.isEmpty({one=1})) 173 | assert.is_not.truthy(_.isEmpty("string")) 174 | assert.is_not.truthy(_.isEmpty(1)) 175 | end) 176 | 177 | it("returns true for empty values", function() 178 | assert.truthy(_.isEmpty({})) 179 | assert.truthy(_.isEmpty(nil)) 180 | assert.truthy(_.isEmpty('')) 181 | assert.truthy(_.isEmpty()) 182 | end) 183 | end) 184 | 185 | describe("#isObject", function() 186 | it("returns true if table is object or array", function() 187 | assert.truthy(_.isObject({})) 188 | assert.truthy(_.isObject({1,2,3})) 189 | assert.truthy(_.isObject({one=1})) 190 | assert.truthy(_.isObject({one=1,two=2,three=3})) 191 | end) 192 | 193 | it("returns false if not an object or array", function() 194 | assert.is_not.truthy(_.isObject(function() end)) 195 | assert.is_not.truthy(_.isObject(nil)) 196 | assert.is_not.truthy(_.isObject("string")) 197 | assert.is_not.truthy(_.isObject(12)) 198 | assert.is_not.truthy(_.isObject(true)) 199 | end) 200 | end) 201 | 202 | describe("#isArray", function() 203 | it("returns true for array, not object", function() 204 | assert.truthy(_.isArray({})) 205 | assert.truthy(_.isArray({1,2,3})) 206 | assert.is_not.truthy(_.isArray({one=1,two=2,three=3})) 207 | end) 208 | 209 | it("returns false if not an object or array", function() 210 | assert.is_not.truthy(_.isArray(nil)) 211 | assert.is_not.truthy(_.isArray("string")) 212 | assert.is_not.truthy(_.isArray(12)) 213 | assert.is_not.truthy(_.isArray(true)) 214 | end) 215 | end) 216 | 217 | describe("#isString", function() 218 | it("returns true when a string", function() 219 | assert.truthy(_.isString("")) 220 | assert.truthy(_.isString("name")) 221 | end) 222 | 223 | it("returns false for non-string values", function() 224 | assert.is_not.truthy(_.isString({})) 225 | assert.is_not.truthy(_.isString(1)) 226 | assert.is_not.truthy(_.isString(nil)) 227 | assert.is_not.truthy(_.isString(true)) 228 | end) 229 | end) 230 | 231 | describe("#isNumber", function() 232 | it("returns true for any number", function() 233 | assert.truthy(_.isNumber(1)) 234 | assert.truthy(_.isNumber(1.1)) 235 | assert.truthy(_.isNumber(math.huge)) 236 | assert.truthy(_.isNumber(-math.huge)) 237 | assert.truthy(_.isNumber(-1)) 238 | assert.truthy(_.isNumber(-1.1)) 239 | end) 240 | 241 | it("returns false for non-number values", function() 242 | assert.is_not.truthy(_.isNumber({})) 243 | assert.is_not.truthy(_.isNumber("")) 244 | assert.is_not.truthy(_.isNumber(nil)) 245 | assert.is_not.truthy(_.isNumber(true)) 246 | end) 247 | end) 248 | 249 | describe("#isNaN", function() 250 | it("returns false for non NaN values", function() 251 | assert.is_not.truthy(_.isNaN(nil)) 252 | assert.is_not.truthy(_.isNaN(0)) 253 | assert.is_not.truthy(_.isNaN("")) 254 | end) 255 | 256 | it("returns true for the NaN values", function() 257 | assert.truthy(_.isNaN(0/0)) 258 | 259 | local nan_value = 0/0 260 | assert.is_not.truthy(0/0==nan_value) 261 | assert.truthy(_.isNaN(nan_value)) 262 | end) 263 | end) 264 | 265 | describe("#isBoolean", function() 266 | it("returns true for boolean value", function() 267 | assert.truthy(_.isBoolean(true)) 268 | assert.truthy(_.isBoolean(false)) 269 | end) 270 | 271 | it("returns false for non-boolean values", function() 272 | assert.is_not.truthy(_.isBoolean({})) 273 | assert.is_not.truthy(_.isBoolean("")) 274 | assert.is_not.truthy(_.isBoolean(nil)) 275 | assert.is_not.truthy(_.isBoolean(function() end)) 276 | end) 277 | end) 278 | 279 | describe("#isFunction", function() 280 | it("returns true for boolean value", function() 281 | assert.truthy(_.isFunction(function() end)) 282 | assert.truthy(_.isFunction(_.map)) 283 | assert.truthy(_.isFunction(function(v) return v end)) 284 | end) 285 | 286 | it("returns false for non-boolean values", function() 287 | assert.is_not.truthy(_.isFunction({})) 288 | assert.is_not.truthy(_.isFunction(1)) 289 | assert.is_not.truthy(_.isFunction("")) 290 | assert.is_not.truthy(_.isFunction(nil)) 291 | assert.is_not.truthy(_.isFunction(true)) 292 | end) 293 | end) 294 | 295 | describe("#isFinite", function() 296 | it("returns true for numbers that are finite", function() 297 | assert.truthy(_.isFinite(1)) 298 | assert.truthy(_.isFinite(100000)) 299 | assert.truthy(_.isFinite(0)) 300 | assert.truthy(_.isFinite(-100000)) 301 | end) 302 | 303 | it("returns false for numbers that are not finite", function() 304 | assert.is_not.truthy(_.isFinite(math.huge)) 305 | assert.is_not.truthy(_.isFinite(-math.huge)) 306 | end) 307 | 308 | it("returns false for any non-number value", function() 309 | assert.is_not.truthy(_.isFinite({})) 310 | assert.is_not.truthy(_.isFinite("")) 311 | assert.is_not.truthy(_.isFinite(function() end)) 312 | assert.is_not.truthy(_.isFinite(nil)) 313 | assert.is_not.truthy(_.isFinite(true)) 314 | end) 315 | end) 316 | 317 | describe("#isNil", function() 318 | it("returns true if nil", function() 319 | assert.truthy(_.isNil(nil)) 320 | assert.truthy(_.isNil()) 321 | end) 322 | 323 | it("return false if non-nil value", function() 324 | assert.is_not.truthy(_.isNil({})) 325 | assert.is_not.truthy(_.isNil("")) 326 | assert.is_not.truthy(_.isNil(function() end)) 327 | assert.is_not.truthy(_.isNil(123)) 328 | assert.is_not.truthy(_.isNil(true)) 329 | end) 330 | end) 331 | 332 | describe("#tap", function() 333 | local intercepted = nil 334 | local interceptor = function(obj) intercepted = obj end 335 | local returned = _.tap(1, interceptor) 336 | 337 | it("passes the tapped object to interceptor", function() 338 | assert.equals(intercepted, 1) 339 | assert.equals(returned, 1) 340 | end) 341 | end) 342 | -------------------------------------------------------------------------------- /spec/string_spec.lua: -------------------------------------------------------------------------------- 1 | local _ = require("lib/underscore") 2 | 3 | describe("string functions", function() 4 | describe("#split", function() 5 | it("splits a string on each character", function() 6 | assert.same(_.split("asdf"),{"a","s","d","f"}) 7 | assert.same(_.split(""),{}) 8 | end) 9 | 10 | it("returns an empty array with non-string values", function() 11 | assert.same(_.split({}),{}) 12 | assert.same(_.split(nil),{}) 13 | assert.same(_.split(123),{}) 14 | assert.same(_.split(function() end),{}) 15 | end) 16 | 17 | describe("with a specific split string", function() 18 | it("can split a specific characters", function() 19 | assert.same(_.split("a|b|c|d", "|"), {"a","b","c","d"}) 20 | assert.same(_.split("a 1 c 2", " "), {"a","1","c","2"}) 21 | assert.same(_.split("a\tb\tc\td", "\t"), {"a","b","c","d"}) 22 | end) 23 | 24 | it("can split using Lua pattern matchers", function() 25 | assert.same(_.split("a b c d", "%s+"), {"a","b","c","d"}) 26 | assert.same(_.split("a1b2c3d4e", "%d"), {"a","b","c","d","e"}) 27 | end) 28 | 29 | it("can split on whole words", function() 30 | assert.same(_.split("arabbitbrabbitc", "rabbit"), {"a","b","c"}) 31 | end) 32 | end) 33 | 34 | describe("#capitalize", function() 35 | it("capitalizes only the first letter", function() 36 | assert.same(_.capitalize('fabio'), 'Fabio'); 37 | assert.same(_.capitalize('FOO'), 'FOO'); 38 | assert.same(_(123):capitalize(), '123'); 39 | assert.same(_.capitalize(''), ''); 40 | assert.same(_.capitalize(nil), ''); 41 | end) 42 | end) 43 | 44 | describe("#numberFormat", function() 45 | it("formats a number to the decimal place", function() 46 | assert.same(_.numberFormat(9000), '9,000'); 47 | assert.same(_.numberFormat(9000, 0), '9,000'); 48 | assert.same(_.numberFormat(9000, 0, '', ''), '9000'); 49 | assert.same(_.numberFormat(90000, 2), '90,000.00'); 50 | assert.same(_.numberFormat(1000.754), '1,001'); 51 | assert.same(_.numberFormat(1000.754, 2), '1,000.75'); 52 | assert.same(_.numberFormat(1000.754, 0, ',', '.'), '1.001'); 53 | assert.same(_.numberFormat(1000.754, 2, ',', '.'), '1.000,75'); 54 | assert.same(_.numberFormat(1000000.754, 2, ',', '.'), '1.000.000,75'); 55 | assert.same(_.numberFormat(1000000000), '1,000,000,000'); 56 | assert.same(_.numberFormat(100000000), '100,000,000'); 57 | assert.same(_.numberFormat('not number'), ''); 58 | assert.same(_.numberFormat(), ''); 59 | assert.same(_.numberFormat(null, '.', ','), ''); 60 | end) 61 | end) 62 | end) 63 | end) 64 | -------------------------------------------------------------------------------- /spec/utility_spec.lua: -------------------------------------------------------------------------------- 1 | local _ = require("lib/underscore") 2 | 3 | describe("_ as a function", function() 4 | it("return the original instance", function() 5 | local instance = _({1,2,3}) 6 | assert.equal(_(instance), instance) 7 | end) 8 | end) 9 | 10 | describe("#identity", function() 11 | it("return the value passed in", function() 12 | local moe = {name="moe"} 13 | assert.same(_.identity(moe), moe) 14 | end) 15 | end) 16 | 17 | describe("#uniqueId", function() 18 | it("can generate a globally-unique stream of ids", function() 19 | local ids = {} 20 | for i=1,100,1 do table.insert(ids, _.uniqueId()) end 21 | assert.same(_.uniq(ids),ids) 22 | end) 23 | end) 24 | 25 | describe("#times", function() 26 | it("is 0 indexed", function() 27 | local vals = {}; 28 | _.times(3, function (i) table.insert(vals, i) end) 29 | assert.same(vals, {0,1,2}); 30 | end) 31 | it("works as a wrapper", function() 32 | local vals = {} 33 | _(3):times(function (i) table.insert(vals, i) end); 34 | assert.same(vals, {0,1,2}) 35 | end) 36 | end) 37 | 38 | describe("#mixin", function() 39 | _.mixin({ 40 | myReverse=function(string) 41 | return table.concat(_(string).chain():split():reverse():value()) 42 | end 43 | }) 44 | 45 | it("mixed in a function to _", function() 46 | assert.equal(_.myReverse('panacea'),'aecanap') 47 | assert.equal(_('champ').chain():myReverse():value(), 'pmahc') 48 | end) 49 | end) 50 | 51 | describe("#escape", function() 52 | it("handles escaping of HTML special characters", function() 53 | assert.equals(_.escape("Curly & Moe"), "Curly & Moe") 54 | assert.equals(_.escape("& < > \" ' /"),"& < > " ' /") 55 | assert.equals(_.escape("Curly & Moe"), "Curly &amp; Moe") 56 | assert.equals(_.escape(nil), '') 57 | assert.equals(_.escape(_.unescape("Curly & Moe")), "Curly & Moe") 58 | end) 59 | end) 60 | 61 | describe("#unescape", function() 62 | it("handles unescaping of HTML special characters", function() 63 | assert.equals(_.unescape("Curly & Moe"), "Curly & Moe") 64 | assert.equals(_.unescape("Curly &amp; Moe"), "Curly & Moe") 65 | assert.equals(_.unescape(nil), "") 66 | assert.equals(_.unescape("& < > " ' /"), "& < > \" ' /") 67 | end) 68 | end) 69 | 70 | describe("#result", function() 71 | local obj = {w="",x="x",y=function(obj) return obj.x end} 72 | 73 | it("calls the function and returns result", function() 74 | assert.same(_.result(obj, 'y'), 'x') 75 | end) 76 | 77 | it("returns the primitive result", function() 78 | assert.same(_.result(obj, 'w'), '') 79 | assert.same(_.result(obj, 'x'), 'x') 80 | end) 81 | 82 | it("defaults to nil with unknown values", function() 83 | assert.same(_.result(obj, 'z'), nil) 84 | end) 85 | end) 86 | -------------------------------------------------------------------------------- /underscore-dev-1.rockspec: -------------------------------------------------------------------------------- 1 | package = "underscore" 2 | version = "dev-1" 3 | source = { 4 | url = "git://github.com/jtarchie/underscore-lua.git" 5 | } 6 | description = { 7 | summary = "Underscore is a utility-belt library for Lua", 8 | detailed = [[ 9 | underscore-lua is a utility-belt library for Lua that provides a lot of the functional programming support that you would expect in or Ruby's Enumerable. 10 | ]], 11 | homepage = "http://jtarchie.github.com/underscore-lua/", 12 | maintainer = "JT Archie ", 13 | license = "MIT" 14 | } 15 | dependencies = { 16 | "lua >= 5.1" 17 | } 18 | build = { 19 | type = "builtin", 20 | modules = { 21 | underscore = "lib/underscore.lua" 22 | } 23 | } 24 | --------------------------------------------------------------------------------