pamda.pamda
1from functools import reduce 2from pamda.pamda_utils import pamda_utils 3from pamda.pamda_fast import ( 4 __getForceDict__, 5 __assocPath__, 6 __groupByHashable__, 7 __mergeDeep__, 8 __pathOr__, 9 __getKeyValues__, 10) 11from pamda.pamda_curry import curry_obj 12from pamda import pamda_wrappers 13from typing import Any 14 15 16@pamda_wrappers.typed_curry_wrap 17@pamda_wrappers.classmethod_wrap 18class pamda(pamda_utils): 19 def accumulate(self, fn, initial_accumulator, data: list): 20 """ 21 Function: 22 23 - Returns an accumulated list of items by iterating a function starting with an accumulator over a list 24 25 Requires: 26 27 - `fn`: 28 - Type: function | method 29 - What: The function or method to reduce 30 - Note: This function should have an arity of 2 (take two inputs) 31 - Note: The first input should take the accumulator value 32 - Note: The second input should take the data value 33 -`initial_accumulator`: 34 - Type: any 35 - What: The initial item to pass into the function when starting the accumulation process 36 - `data`: 37 - Type: list 38 - What: The list of items to iterate over 39 40 Example: 41 42 ``` 43 data=[1,2,3,4] 44 pamda.accumulate( 45 fn=pamda.add, 46 initial_accumulator=0, 47 data=data 48 ) 49 #=> [1,3,6,10] 50 51 ``` 52 """ 53 fn = self.curry(fn) 54 if fn.__arity__ != 2: 55 raise Exception("`fn` must have an arity of 2 (take two inputs)") 56 if not len(data) > 0: 57 raise Exception( 58 "`data` has a length of 0, however it must have a length of at least 1" 59 ) 60 acc = initial_accumulator 61 out = [] 62 for i in data: 63 acc = fn(acc, i) 64 out.append(acc) 65 return out 66 67 def add(self, a: int | float, b: int | float): 68 """ 69 Function: 70 71 - Adds two numbers 72 73 Requires: 74 75 - `a`: 76 - Type: int | float 77 - What: The first number to add 78 - `b`: 79 - Type: int | float 80 - What: The second number to add 81 82 Example: 83 84 ``` 85 pamda.add(1, 2) #=> 3 86 ``` 87 """ 88 return a + b 89 90 def adjust(self, index: int, fn, data: list): 91 """ 92 Function: 93 94 - Adjusts an item in a list by applying a function to it 95 96 Requires: 97 98 - `index`: 99 - Type: int 100 - What: The 0 based index of the item in the list to adjust 101 - Note: Indicies are accepted 102 - Note: If the index is out of range, picks the (-)first / (+)last item 103 - `fn`: 104 - Type: function | method 105 - What: The function to apply the index item to 106 - Note: This is automatically curried 107 - `data`: 108 - Type: list 109 - What: The list to adjust 110 111 Example: 112 113 ``` 114 data=[1,5,9] 115 pamda.adjust( 116 index=1, 117 fn=pamda.inc, 118 data=data 119 ) #=> [1,6,9] 120 ``` 121 """ 122 fn = self.curry(fn) 123 index = self.clamp(-len(data), len(data) - 1, index) 124 data[index] = fn(data[index]) 125 return data 126 127 def assocPath(self, path: list | str | int | tuple, value, data: dict): 128 """ 129 Function: 130 131 - Ensures a path exists within a nested dictionary 132 - Note: This updates the object in place, but also returns the object 133 134 Requires: 135 136 - `path`: 137 - Type: list[str | int | tuple] | str | int | tuple 138 - What: The path to check 139 - Note: If a string is passed, assumes a single item path list with that string 140 - `value`: 141 - Type: any 142 - What: The value to appropriate to the end of the path 143 - `data`: 144 - Type: dict 145 - What: A dictionary in which to associate the given value to the given path 146 147 Example: 148 149 ``` 150 data={'a':{'b':1}} 151 pamda.assocPath(path=['a','c'], value=3, data=data) #=> {'a':{'b':1, 'c':3}} 152 ``` 153 """ 154 if not isinstance(path, list): 155 path = [path] 156 reduce(__getForceDict__, path[:-1], data).__setitem__(path[-1], value) 157 return data 158 159 def assocPathComplex( 160 self, default, default_fn, path: list | int | float | tuple, data: dict 161 ): 162 """ 163 Function: 164 165 - Ensures a path exists within a nested dictionary 166 - Note: This updates the object in place, but also returns the object 167 168 Requires: 169 170 - `default`: 171 - Type: any 172 - What: The default item to add to a path that does not yet exist 173 - `default_fn`: 174 - Type: function | method 175 - What: A unary (single input) function that takes in the current path item (or default) and adjusts it 176 - Example: `lambda x: x` # Returns the value in the dict or the default value if none was present 177 - `path`: 178 - Type: list[str | int | tuple] | str | int | tuple 179 - What: The path to check 180 - `data`: 181 - Type: dict 182 - What: A dictionary to check if the path exists 183 184 Example: 185 186 ``` 187 data={'a':{'b':1}} 188 pamda.assocPathComplex(default=[2], default_fn=lambda x:x+[1], path=['a','c'], data=data) #=> {'a':{'b':1,'c':[2,1]}} 189 ``` 190 """ 191 if self.getArity(default_fn) != 1: 192 raise Exception( 193 "`assocPathComplex` `default_fn` must be an unary (single input) function." 194 ) 195 if not isinstance(path, list): 196 path = [path] 197 path_object = reduce(__getForceDict__, path[:-1], data) 198 path_object.__setitem__( 199 path[-1], default_fn(path_object.get(path[-1], default)) 200 ) 201 return data 202 203 def asyncKill(self, fn: curry_obj): 204 """ 205 Function: 206 207 - Kills an asynchronous function that is currently running 208 - Returns: 209 - `None` if the function has not yet finished running 210 - The result of the function if it has finished running 211 212 Requires: 213 214 - `fn`: 215 - Type: thunkified function | thunkified method 216 - What: The function or method to run asychronously 217 - Note: The supplied `fn` must already be asynchronously running 218 219 Notes: 220 221 - See also `asyncRun` and `asyncWait` 222 - A thunkified function currently running asynchronously can call `asyncKill` on itself 223 - If a function has already finished running, calling `asyncKill` on it will have no effect 224 - `asyncKill` does not kill threads that are sleeping (EG: `time.sleep`), but will kill the thread once the sleep is finished 225 226 Example: 227 228 ``` 229 import time 230 from pamda import pamda 231 232 @pamda.thunkify 233 def test(name, wait): 234 waited = 0 235 while waited < wait: 236 time.sleep(1) 237 waited += 1 238 print(f'{name} has waited {waited} seconds') 239 240 async_test = pamda.asyncRun(test('a',3)) 241 time.sleep(1) 242 pamda.asyncKill(async_test) 243 # Alternatively: 244 # async_test.asyncKill() 245 ``` 246 """ 247 return fn.asyncKill() 248 249 def asyncRun(self, fn: curry_obj): 250 """ 251 Function: 252 253 - Runs the supplied function asychronously 254 255 Requires: 256 257 - `fn`: 258 - Type: thunkified function | thunkified method 259 - What: The function or method to run asychronously 260 - Note: The supplied `fn` must have an arity of 0 261 262 Notes: 263 264 - To pass inputs to a function in asyncRun, first thunkify the function and pass all arguments before calling `asyncRun` on it 265 - To get the results of an `asyncRun` call `asyncWait` 266 - To kill an `asyncRun` mid process call `asyncKill` 267 - A thunkified function with arity of 0 can call `asyncRun` on itself 268 269 Examples: 270 271 Input: 272 ``` 273 import time 274 275 @pamda.thunkify 276 def test(name, wait): 277 print(f'{name} start') 278 time.sleep(wait) 279 print(f'{name} end') 280 281 async_test = pamda.asyncRun(test('a',2)) 282 sync_test = test('b',1)() 283 ``` 284 Output: 285 ``` 286 a start 287 b start 288 b end 289 a end 290 ``` 291 292 293 Input: 294 ``` 295 import time 296 297 @pamda.thunkify 298 def test(name, wait): 299 time.sleep(wait) 300 return f"{name}: {wait}" 301 302 async_test = pamda.asyncRun(test('a',2)) 303 print(async_test.asyncWait()) #=> a: 2 304 ``` 305 306 307 Input: 308 ``` 309 import time 310 311 @pamda.thunkify 312 def test(name, wait): 313 time.sleep(wait) 314 return f"{name}: {wait}" 315 316 async_test = test('a',2).asyncRun() 317 print(async_test.asyncWait()) #=> a: 2 318 ``` 319 """ 320 return fn.asyncRun() 321 322 def asyncWait(self, fn: curry_obj): 323 """ 324 Function: 325 326 - Waits for a supplied function (if needed) and returns the results 327 328 Requires: 329 330 - `fn`: 331 - Type: function | method 332 - What: The function or method for which to wait 333 - Note: The supplied `fn` must have previously called `asyncRun` 334 335 Notes: 336 337 - A thunkified function that has called `asyncRun` can call `asyncWait` on itself 338 339 Examples: 340 341 ``` 342 import time 343 344 @pamda.thunkify 345 def test(name, wait): 346 time.sleep(wait) 347 return f"{name}: {wait}" 348 349 async_test = pamda.asyncRun(test('a',2)) 350 print(pamda.asyncWait(async_test)) #=> a: 2 351 ``` 352 353 354 ``` 355 import time 356 357 @pamda.thunkify 358 def test(name, wait): 359 time.sleep(wait) 360 return f"{name}: {wait}" 361 362 async_test = pamda.asyncRun(test('a',2)) 363 print(async_test.asyncWait()) #=> a: 2 364 ``` 365 """ 366 return fn.asyncWait() 367 368 def clamp(self, minimum: int | float, maximum: int | float, a: int | float): 369 """ 370 Function: 371 372 - Forces data to be within minimum and maximum 373 374 Requires: 375 376 - `minimum`: 377 - Type: int | float 378 - What: The minimum number 379 - `maximum`: 380 - Type: int | float 381 - What: The maximum number 382 - `a`: 383 - Type: int | float 384 - What: The number to clamp 385 386 Example: 387 388 ``` 389 pamda.clamp(1, 3, 2) #=> 2 390 pamda.clamp(1, 3, 5) #=> 3 391 ``` 392 """ 393 return min(max(a, minimum), maximum) 394 395 def curry(self, fn): 396 """ 397 Function: 398 399 - Curries a function such that inputs can be added interatively 400 401 Requires: 402 403 - `fn`: 404 - Type: function | method 405 - What: The function or method to curry 406 - Note: Class methods auto apply self during curry 407 408 Notes: 409 410 - Once curried, the function | method becomes a curry_obj object 411 - The initial function is only called once all inputs are passed 412 413 414 Examples: 415 416 ``` 417 curriedZip=pamda.curry(pamda.zip) 418 curriedZip(['a','b'])([1,2]) #=> [['a',1],['b',2]] 419 420 # Curried functions can be thunkified at any time 421 # See also thunkify 422 zipThunk=curriedZip.thunkify()(['a','b'])([1,2]) 423 zipThunk() #=> [['a',1],['b',2]] 424 ``` 425 426 ``` 427 def myFunction(a,b,c): 428 return [a,b,c] 429 430 curriedMyFn=pamda.curry(myFunction) 431 432 curriedMyFn(1,2,3) #=> [1,2,3] 433 curriedMyFn(1)(2,3) #=> [1,2,3] 434 435 x=curriedMyFn(1)(2) 436 x(3) #=> [1,2,3] 437 x(4) #=> [1,2,4] 438 439 440 ``` 441 """ 442 if fn.__dict__.get("__isCurried__"): 443 return fn() 444 return curry_obj(fn) 445 446 def curryTyped(self, fn): 447 """ 448 Function: 449 450 - Curries a function such that inputs can be added interatively and function annotations are type checked at runtime 451 452 Requires: 453 454 - `fn`: 455 - Type: function | method 456 - What: The function or method to curry 457 - Note: Class methods auto apply self during curry 458 459 Notes: 460 461 - Once curried, the function | method becomes a curry_obj object 462 - The initial function is only called once all inputs are passed 463 464 465 Examples: 466 467 ``` 468 @pamda.curryTyped 469 def add(a:int,b:int): 470 return a+b 471 472 add(1)(1) #=> 2 473 add(1)(1.5) #=> Raises type exception 474 ``` 475 """ 476 if fn.__dict__.get("__isCurried__"): 477 return fn().typeEnforce() 478 return curry_obj(fn).typeEnforce() 479 480 def dec(self, a: int | float): 481 """ 482 Function: 483 484 - Decrements a number by one 485 486 Requires: 487 488 - `a`: 489 - Type: int | float 490 - What: The number to decrement 491 492 Example: 493 494 ``` 495 pamda.dec(42) #=> 41 496 ``` 497 """ 498 if not isinstance(a, (int, float)): 499 raise Exception("`a` must be an `int` or a `float`") 500 return a - 1 501 502 def difference(self, a: list, b: list): 503 """ 504 Function: 505 506 - Combines two lists into a list of no duplicate items present in the first list but not the second 507 508 Requires: 509 510 - `a`: 511 - Type: list 512 - What: List of items in which to look for a difference 513 - `b`: 514 - Type: list 515 - What: List of items in which to compare when looking for the difference 516 517 Example: 518 519 ``` 520 a=['a','b'] 521 b=['b','c'] 522 pamda.difference(a=a, b=b) #=> ['a'] 523 pamda.difference(a=b, b=a) #=> ['c'] 524 ``` 525 """ 526 return list(set(a).difference(set(b))) 527 528 def dissocPath(self, path: list | str | int | tuple, data: dict): 529 """ 530 Function: 531 532 - Removes the value at the end of a path within a nested dictionary 533 - Note: This updates the object in place, but also returns the object 534 535 Requires: 536 537 - `path`: 538 - Type: list of strs | str 539 - What: The path to remove from the dictionary 540 - Note: If a string is passed, assumes a single item path list with that string 541 - `data`: 542 - Type: dict 543 - What: A dictionary with a path to be removed 544 545 Example: 546 547 ``` 548 data={'a':{'b':{'c':0,'d':1}}} 549 pamda.dissocPath(path=['a','b','c'], data=data) #=> {'a':{'b':{'d':1}}} 550 ``` 551 """ 552 if not isinstance(path, list): 553 path = [path] 554 if not self.hasPath(path=path, data=data): 555 raise Exception("Path does not exist") 556 else: 557 reduce(__getForceDict__, path[:-1], data).pop(path[-1]) 558 return data 559 560 def flatten(self, data: list): 561 """ 562 Function: 563 564 - Flattens a list of lists of lists ... into a single list depth first 565 566 Requires: 567 568 - `data`: 569 - Type: list of lists 570 - What: The list of lists to reduce to a single list 571 Example: 572 573 ``` 574 data=[['a','b'],[1,[2]]] 575 pamda.flatten(data=data) #=> ['a','b',1,2] 576 ``` 577 """ 578 579 def iter_flatten(data): 580 out = [] 581 for i in data: 582 if isinstance(i, list): 583 out.extend(iter_flatten(i)) 584 else: 585 out.append(i) 586 return out 587 588 return iter_flatten(data) 589 590 def flip(self, fn): 591 """ 592 Function: 593 594 - Returns a new function equivalent to the supplied function except that the first two inputs are flipped 595 596 Requires: 597 598 - `fn`: 599 - Type: function | method 600 - What: The function or method to flip 601 - Note: This function must have an arity of at least 2 (take two inputs) 602 - Note: Only args are flipped, kwargs are passed as normal 603 604 Notes: 605 606 - Input functions are not flipped in place 607 - The returned function is a flipped version of the input function 608 - A curried function can be flipped in place by calling fn.flip() 609 - A function can be flipped multiple times: 610 - At each flip, the first and second inputs for the function as it is currently curried are switched 611 - Flipping a function two times before adding an input will return the initial value 612 613 Examples: 614 615 ``` 616 def concat(a,b,c,d): 617 return str(a)+str(b)+str(c)+str(d) 618 619 flip_concat=pamda.flip(concat) 620 621 concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum' 622 flip_concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum' 623 ``` 624 625 ``` 626 @pamda.curry 627 def concat(a,b,c,d): 628 return str(a)+str(b)+str(c)+str(d) 629 630 concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum' 631 632 concat.flip() 633 634 concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum' 635 ``` 636 637 ``` 638 @pamda.curry 639 def concat(a,b,c,d): 640 return str(a)+str(b)+str(c)+str(d) 641 642 a=pamda.flip(concat)('fi-') 643 b=pamda.flip(a)('fo-') 644 c=pamda.flip(b)('fum') 645 c('fe-') #=> 'fe-fi-fo-fum' 646 ``` 647 648 ``` 649 def concat(a,b,c,d): 650 return str(a)+str(b)+str(c)+str(d) 651 652 a=pamda.flip(concat)('fi-').flip()('fo-').flip()('fum') 653 a('fe-') #=> 'fe-fi-fo-fum' 654 ``` 655 """ 656 fn = self.curry(fn) 657 return fn.flip() 658 659 def getArity(self, fn): 660 """ 661 Function: 662 663 - Gets the arity (number of inputs left to be specified) of a function or method (curried or uncurried) 664 665 Requires: 666 667 - `fn`: 668 - Type: function | method 669 - What: The function or method to get the arity of 670 - Note: Class methods remove one arity to account for self 671 672 Examples: 673 674 ``` 675 pamda.getArity(pamda.zip) #=> 2 676 curriedZip=pamda.curry(pamda.zip) 677 ABCuriedZip=curriedZip(['a','b']) 678 pamda.getArity(ABCuriedZip) #=> 1 679 ``` 680 """ 681 fn = self.curry(fn) 682 return fn.__arity__ 683 684 def groupBy(self, fn, data: list): 685 """ 686 Function: 687 688 - Splits a list into a dictionary of sublists keyed by the return string of a provided function 689 690 Requires: 691 692 - `fn`: 693 - Type: function | method 694 - What: The function or method to group by 695 - Note: Must return a string (or other hashable object) 696 - Note: This function must be unary (take one input) 697 - Note: This function is applied to each item in the list recursively 698 - `data`: 699 - Type: list 700 - What: List of items to apply the function to and then group by the results 701 702 Examples: 703 704 ``` 705 def getGrade(item): 706 score=item['score'] 707 if score>90: 708 return 'A' 709 elif score>80: 710 return 'B' 711 elif score>70: 712 return 'C' 713 elif score>60: 714 return 'D' 715 else: 716 return 'F' 717 718 data=[ 719 {'name':'Connor', 'score':75}, 720 {'name':'Fred', 'score':79}, 721 {'name':'Joe', 'score':84}, 722 ] 723 pamda.groupBy(getGrade,data) 724 #=>{ 725 #=> 'B':[{'name':'Joe', 'score':84}] 726 #=> 'C':[{'name':'Connor', 'score':75},{'name':'Fred', 'score':79}] 727 #=>} 728 ``` 729 """ 730 curried_fn = self.curry(fn) 731 if curried_fn.__arity__ != 1: 732 raise Exception( 733 "groupBy `fn` must only take one parameter as its input" 734 ) 735 return __groupByHashable__(fn=fn, data=data) 736 737 def groupKeys(self, keys: list, data: list): 738 """ 739 Function: 740 741 - Splits a list of dicts into a list of sublists of dicts separated by values with equal keys 742 743 Requires: 744 745 - `keys`: 746 - Type: list of strs 747 - What: The keys to group by 748 - `data`: 749 - Type: list of dicts 750 - What: List of dictionaries with which to match keys 751 752 Examples: 753 754 ``` 755 data=[ 756 {'color':'red', 'size':9, 'shape':'ball'}, 757 {'color':'red', 'size':10, 'shape':'ball'}, 758 {'color':'green', 'size':11, 'shape':'ball'}, 759 {'color':'green', 'size':12, 'shape':'square'} 760 ] 761 pamda.groupKeys(['color','shape'],data) 762 #=> [ 763 #=> [{'color': 'red', 'size': 9, 'shape': 'ball'}, {'color': 'red', 'size': 10, 'shape': 'ball'}], 764 #=> [{'color': 'green', 'size': 11, 'shape': 'ball'}], 765 #=> [{'color': 'green', 'size': 12, 'shape': 'square'}] 766 #=> ] 767 ``` 768 """ 769 770 def key_fn(item): 771 return tuple([item[key] for key in keys]) 772 773 return list(__groupByHashable__(key_fn, data).values()) 774 775 def groupWith(self, fn, data: list): 776 """ 777 Function: 778 779 - Splits a list into a list of sublists where each sublist is determined by adjacent pairwise comparisons from a provided function 780 781 Requires: 782 783 - `fn`: 784 - Type: function | method 785 - What: The function or method to groub with 786 - Note: Must return a boolean value 787 - Note: This function must have an arity of two (take two inputs) 788 - Note: This function is applied to each item plus the next adjacent item in the list recursively 789 - `data`: 790 - Type: list 791 - What: List of items to apply the function to and then group the results 792 793 Examples: 794 795 ``` 796 def areEqual(a,b): 797 return a==b 798 799 data=[1,2,3,1,1,2,2,3,3,3] 800 pamda.groupWith(areEqual,data) #=> [[1], [2], [3], [1, 1], [2, 2], [3, 3, 3]] 801 ``` 802 """ 803 curried_fn = self.curry(fn) 804 if curried_fn.__arity__ != 2: 805 raise Exception("groupWith `fn` must take exactly two parameters") 806 previous = data[0] 807 output = [] 808 sublist = [previous] 809 for i in data[1:]: 810 if fn(i, previous): 811 sublist.append(i) 812 else: 813 output.append(sublist) 814 sublist = [i] 815 previous = i 816 output.append(sublist) 817 return output 818 819 def hasPath(self, path: list | str, data: dict): 820 """ 821 Function: 822 823 - Checks if a path exists within a nested dictionary 824 825 Requires: 826 827 - `path`: 828 - Type: list of strs | str 829 - What: The path to check 830 - Note: If a string is passed, assumes a single item path list with that string 831 - `data`: 832 - Type: dict 833 - What: A dictionary to check if the path exists 834 835 Example: 836 837 ``` 838 data={'a':{'b':1}} 839 pamda.hasPath(path=['a','b'], data=data) #=> True 840 pamda.hasPath(path=['a','d'], data=data) #=> False 841 ``` 842 """ 843 if isinstance(path, str): 844 path = [path] 845 return path[-1] in reduce(lambda x, y: x.get(y, {}), path[:-1], data) 846 847 def hardRound(self, decimal_places: int, a: int | float): 848 """ 849 Function: 850 851 - Rounds to a set number of decimal places regardless of floating point math in python 852 853 Requires: 854 855 - `decimal_places`: 856 - Type: int 857 - What: The number of decimal places to round to 858 - Default: 0 859 - Notes: Negative numbers accepted (EG -1 rounds to the nearest 10) 860 - `a`: 861 - Type: int | float 862 - What: The number to round 863 864 Example: 865 866 ``` 867 a=12.345 868 pamda.hardRound(1,a) #=> 12.3 869 pamda.hardRound(-1,a) #=> 10 870 ``` 871 """ 872 return int(a * (10**decimal_places) + 0.5) / (10**decimal_places) 873 874 def head(self, data: list | str): 875 """ 876 Function: 877 878 - Picks the first item out of a list or string 879 880 Requires: 881 882 - `data`: 883 - Type: list | str 884 - What: A list or string 885 886 Example: 887 888 ``` 889 data=['fe','fi','fo','fum'] 890 pamda.first( 891 data=data 892 ) #=> fe 893 ``` 894 """ 895 if not isinstance(data, (list, str)): 896 raise Exception("`head` can only be called on a `str` or a `list`") 897 if not len(data) > 0: 898 raise Exception("Attempting to call `head` on an empty list or str") 899 return data[0] 900 901 def inc(self, a: int | float): 902 """ 903 Function: 904 905 - Increments a number by one 906 907 Requires: 908 909 - `a`: 910 - Type: int | float 911 - What: The number to increment 912 913 Example: 914 915 ``` 916 pamda.inc(42) #=> 43 917 ``` 918 """ 919 if not isinstance(a, (int, float)): 920 raise Exception("`a` must be an `int` or a `float`") 921 return a + 1 922 923 def intersection(self, a: list, b: list): 924 """ 925 Function: 926 927 - Combines two lists into a list of no duplicates composed of those elements common to both lists 928 929 Requires: 930 931 - `a`: 932 - Type: list 933 - What: List of items in which to look for an intersection 934 - `b`: 935 - Type: list 936 - What: List of items in which to look for an intersection 937 938 Example: 939 940 ``` 941 a=['a','b'] 942 b=['b','c'] 943 pamda.intersection(a=a, b=b) #=> ['b'] 944 ``` 945 """ 946 return list(set(a).intersection(set(b))) 947 948 def map(self, fn, data: list | dict): 949 """ 950 Function: 951 952 - Maps a function over a list or a dictionary 953 954 Requires: 955 956 - `fn`: 957 - Type: function | method 958 - What: The function or method to map over the list or dictionary 959 - Note: This function should have an arity of 1 960 - `data`: 961 - Type: list | dict 962 - What: The list or dict of items to map the function over 963 964 Examples: 965 966 ``` 967 data=[1,2,3] 968 pamda.map( 969 fn=pamda.inc, 970 data=data 971 ) 972 #=> [2,3,4] 973 ``` 974 975 ``` 976 data={'a':1,'b':2,'c':3} 977 pamda.map( 978 fn=pamda.inc, 979 data=data 980 ) 981 #=> {'a':2,'b':3,'c':4} 982 ``` 983 984 """ 985 # TODO: Check for efficiency gains 986 fn = self.curry(fn) 987 if fn.__arity__ != 1: 988 raise Exception("`map` `fn` must be unary (take one input)") 989 if not len(data) > 0: 990 raise Exception( 991 "`map` `data` has a length of 0 or is an empty dictionary, however it must have at least one element in it" 992 ) 993 if isinstance(data, dict): 994 return {key: fn(value) for key, value in data.items()} 995 else: 996 return [fn(i) for i in data] 997 998 def mean(self, data: list): 999 """ 1000 Function: 1001 1002 - Calculates the mean of a given list 1003 1004 Requires: 1005 1006 - `data`: 1007 - Type: list of (floats | ints) 1008 - What: The list with wich to calculate the mean 1009 - Note: If the length of this list is 0, returns None 1010 1011 Example: 1012 1013 ``` 1014 data=[1,2,3] 1015 pamda.mean(data=data) 1016 #=> 2 1017 ``` 1018 1019 ``` 1020 data=[] 1021 pamda.mean(data=data) 1022 #=> None 1023 ``` 1024 """ 1025 if len(data) == 0: 1026 return None 1027 return sum(data) / len(data) 1028 1029 def median(self, data: list): 1030 """ 1031 Function: 1032 1033 - Calculates the median of a given list 1034 - If the length of the list is even, calculates the mean of the two central values 1035 1036 Requires: 1037 1038 - `data`: 1039 - Type: list of (floats | ints) 1040 - What: The list with wich to calculate the mean 1041 - Note: If the length of this list is 0, returns None 1042 1043 Examples: 1044 1045 ``` 1046 data=[7,2,8,9] 1047 pamda.median(data=data) 1048 #=> 7.5 1049 ``` 1050 1051 ``` 1052 data=[7,8,9] 1053 pamda.median(data=data) 1054 #=> 8 1055 ``` 1056 1057 ``` 1058 data=[] 1059 pamda.median(data=data) 1060 #=> None 1061 ``` 1062 """ 1063 if not isinstance(data, (list)): 1064 raise Exception("`median` `data` must be a list") 1065 length = len(data) 1066 if length == 0: 1067 return None 1068 data = sorted(data) 1069 if length % 2 == 0: 1070 return (data[int(length / 2)] + data[int(length / 2) - 1]) / 2 1071 return data[int(length / 2)] 1072 1073 def mergeDeep(self, update_data, data): 1074 """ 1075 Function: 1076 1077 - Recursively merges two nested dictionaries keeping all keys at each layer 1078 - Values from `update_data` are used when keys are present in both dictionaries 1079 1080 Requires: 1081 1082 - `update_data`: 1083 - Type: any 1084 - What: The new data that will take precedence during merging 1085 - `data`: 1086 - Type: any 1087 - What: The original data that will be merged into 1088 1089 Example: 1090 1091 ``` 1092 data={'a':{'b':{'c':'d'},'e':'f'}} 1093 update_data={'a':{'b':{'h':'i'},'e':'g'}} 1094 pamda.mergeDeep( 1095 update_data=update_data, 1096 data=data 1097 ) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}} 1098 ``` 1099 """ 1100 return __mergeDeep__(update_data, data) 1101 1102 def nest(self, path_keys: list, value_key: str, data: list): 1103 """ 1104 Function: 1105 1106 - Nests a list of dictionaries into a nested dictionary 1107 - Similar items are appended to a list in the end of the nested dictionary 1108 1109 Requires: 1110 1111 - `path_keys`: 1112 - Type: list of strs 1113 - What: The variables to pull from each item in data 1114 - Note: Used to build out the nested dicitonary 1115 - Note: Order matters as the nesting occurs in order of variable 1116 - `value_key`: 1117 - Type: str 1118 - What: The variable to add to the list at the end of the nested dictionary path 1119 - `data`: 1120 - Type: list of dicts 1121 - What: A list of dictionaries to use for nesting purposes 1122 1123 Example: 1124 1125 ``` 1126 data=[ 1127 {'x_1':'a','x_2':'b', 'output':'c'}, 1128 {'x_1':'a','x_2':'b', 'output':'d'}, 1129 {'x_1':'a','x_2':'e', 'output':'f'} 1130 ] 1131 pamda.nest( 1132 path_keys=['x_1','x_2'], 1133 value_key='output', 1134 data=data 1135 ) #=> {'a':{'b':['c','d'], 'e':['f']}} 1136 ``` 1137 """ 1138 if not isinstance(data, list): 1139 raise Exception("Attempting to `nest` an object that is not a list") 1140 if len(data) == 0: 1141 raise Exception("Attempting to `nest` from an empty list") 1142 nested_output = {} 1143 for item in self.groupKeys(keys=path_keys, data=data): 1144 nested_output = __assocPath__( 1145 path=__getKeyValues__(path_keys, item[0]), 1146 value=[i.get(value_key) for i in item], 1147 data=nested_output, 1148 ) 1149 return nested_output 1150 1151 def nestItem(self, path_keys: list, data: list): 1152 """ 1153 Function: 1154 1155 - Nests a list of dictionaries into a nested dictionary 1156 - Similar items are appended to a list in the end of the nested dictionary 1157 - Similar to `nest`, except no values are plucked for the aggregated list 1158 1159 Requires: 1160 1161 - `path_keys`: 1162 - Type: list of strs 1163 - What: The variables to pull from each item in data 1164 - Note: Used to build out the nested dicitonary 1165 - Note: Order matters as the nesting occurs in order of variable 1166 - `data`: 1167 - Type: list of dicts 1168 - What: A list of dictionaries to use for nesting purposes 1169 1170 Example: 1171 1172 ``` 1173 data=[ 1174 {'x_1':'a','x_2':'b'}, 1175 {'x_1':'a','x_2':'b'}, 1176 {'x_1':'a','x_2':'e'} 1177 ] 1178 pamda.nestItem 1179 path_keys=['x_1','x_2'], 1180 data=data 1181 ) 1182 #=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}} 1183 1184 ``` 1185 """ 1186 if not isinstance(data, list): 1187 raise Exception("Attempting to `nest` an object that is not a list") 1188 if len(data) == 0: 1189 raise Exception("Attempting to `nest` from an empty list") 1190 nested_output = {} 1191 for item in self.groupKeys(keys=path_keys, data=data): 1192 nested_output = __assocPath__( 1193 path=__getKeyValues__(path_keys, item[0]), 1194 value=item, 1195 data=nested_output, 1196 ) 1197 return nested_output 1198 1199 def path(self, path: list | str, data: dict): 1200 """ 1201 Function: 1202 1203 - Returns the value of a path within a nested dictionary or None if the path does not exist 1204 1205 Requires: 1206 1207 - `path`: 1208 - Type: list of strs | str 1209 - What: The path to pull given the data 1210 - Note: If a string is passed, assumes a single item path list with that string 1211 - `data`: 1212 - Type: dict 1213 - What: A dictionary to get the path from 1214 1215 Example: 1216 1217 ``` 1218 data={'a':{'b':1}} 1219 pamda.path(path=['a','b'], data=data) #=> 1 1220 ``` 1221 """ 1222 if isinstance(path, str): 1223 path = [path] 1224 return __pathOr__(None, path, data) 1225 1226 def pathOr(self, default, path: list | str, data: dict): 1227 """ 1228 Function: 1229 1230 - Returns the value of a path within a nested dictionary or a default value if that path does not exist 1231 1232 Requires: 1233 1234 - `default`: 1235 - Type: any 1236 - What: The object to return if the path does not exist 1237 - `path`: 1238 - Type: list of strs | str 1239 - What: The path to pull given the data 1240 - Note: If a string is passed, assumes a single item path list with that string 1241 - `data`: 1242 - Type: dict 1243 - What: A dictionary to get the path from 1244 1245 Example: 1246 1247 ``` 1248 data={'a':{'b':1}} 1249 pamda.path(default=2, path=['a','c'], data=data) #=> 2 1250 ``` 1251 """ 1252 if isinstance(path, str): 1253 path = [path] 1254 return reduce(lambda x, y: x.get(y, {}), path[:-1], data).get( 1255 path[-1], default 1256 ) 1257 1258 def pipe(self, fns: list, args: tuple, kwargs: dict): 1259 """ 1260 Function: 1261 1262 - Pipes data through n functions in order (left to right composition) and returns the output 1263 1264 Requires: 1265 1266 - `fns`: 1267 - Type: list of (functions | methods) 1268 - What: The list of functions and methods to pipe the data through 1269 - Notes: The first function in the list can be any arity (accepting any number of inputs) 1270 - Notes: Any further function in the list can only be unary (single input) 1271 - Notes: A function can be curried, but is not required to be 1272 - Notes: You may opt to curry functions and add inputs to make them unary 1273 - `args`: 1274 - Type: tuple 1275 - What: a tuple of positional arguments to pass to the first function in `fns` 1276 - `kwargs`: 1277 - Type: dict 1278 - What: a dictionary of keyword arguments to pass to the first function in `fns` 1279 1280 Examples: 1281 1282 ``` 1283 data=['abc','def'] 1284 pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c' 1285 pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c' 1286 ``` 1287 1288 ``` 1289 data={'a':{'b':'c'}} 1290 curriedPath=pamda.curry(pamda.path) 1291 pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c' 1292 ``` 1293 """ 1294 if len(fns) == 0: 1295 raise Exception("`fns` must be a list with at least one function") 1296 if self.getArity(fns[0]) == 0: 1297 raise Exception( 1298 "The first function in `fns` can have n arity (accepting n args), but this must be greater than 0." 1299 ) 1300 if not all([(self.getArity(fn) == 1) for fn in fns[1:]]): 1301 raise Exception( 1302 "Only the first function in `fns` can have n arity (accept n args). All other functions must have an arity of one (accepting one argument)." 1303 ) 1304 out = fns[0](*args, **kwargs) 1305 for fn in fns[1:]: 1306 out = fn(out) 1307 return out 1308 1309 def pivot(self, data: list[dict] | dict[Any, list]): 1310 """ 1311 Function: 1312 1313 - Pivots a list of dictionaries into a dictionary of lists 1314 - Pivots a dictionary of lists into a list of dictionaries 1315 1316 Requires: 1317 1318 - `data`: 1319 - Type: list of dicts | dict of lists 1320 - What: The data to pivot 1321 - Note: If a list of dictionaries is passed, all dictionaries must have the same keys 1322 - Note: If a dictionary of lists is passed, all lists must have the same length 1323 1324 Example: 1325 1326 ``` 1327 data=[ 1328 {'a':1,'b':2}, 1329 {'a':3,'b':4} 1330 ] 1331 pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]} 1332 1333 data={'a':[1,3],'b':[2,4]} 1334 pamda.pivot(data=data) 1335 #=> [ 1336 #=> {'a':1,'b':2}, 1337 #=> {'a':3,'b':4} 1338 #=> ] 1339 ``` 1340 """ 1341 if isinstance(data, list): 1342 return { 1343 key: [record[key] for record in data] for key in data[0].keys() 1344 } 1345 else: 1346 return [ 1347 {key: data[key][i] for key in data.keys()} 1348 for i in range(len(data[list(data.keys())[0]])) 1349 ] 1350 1351 def pluck(self, path: list | str, data: list): 1352 """ 1353 Function: 1354 1355 - Returns the values of a path within a list of nested dictionaries 1356 1357 Requires: 1358 1359 - `path`: 1360 - Type: list of strs 1361 - What: The path to pull given the data 1362 - Note: If a string is passed, assumes a single item path list with that string 1363 - `data`: 1364 - Type: list of dicts 1365 - What: A list of dictionaries to get the path from 1366 1367 Example: 1368 1369 ``` 1370 data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}] 1371 pamda.pluck(path=['a','b'], data=data) #=> [1,2] 1372 ``` 1373 """ 1374 if len(data) == 0: 1375 raise Exception("Attempting to pluck from an empty list") 1376 if isinstance(path, str): 1377 path = [path] 1378 return [__pathOr__(default=None, path=path, data=i) for i in data] 1379 1380 def pluckIf(self, fn, path: list | str, data: list): 1381 """ 1382 Function: 1383 1384 - Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value 1385 1386 Requires: 1387 1388 - `fn`: 1389 - Type: function 1390 - What: A function to take in each item in data and return a boolean 1391 - Note: Only items that return true are plucked 1392 - Note: Should be a unary function (take one input) 1393 - `path`: 1394 - Type: list of strs 1395 - What: The path to pull given the data 1396 - Note: If a string is passed, assumes a single item path list with that string 1397 - `data`: 1398 - Type: list of dicts 1399 - What: A list of dictionary to get the path from 1400 1401 Example: 1402 1403 ``` 1404 1405 data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}] 1406 pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d'] 1407 ``` 1408 """ 1409 if len(data) == 0: 1410 raise Exception("Attempting to pluck from an empty list") 1411 curried_fn = self.curry(fn) 1412 if curried_fn.__arity__ != 1: 1413 raise Exception( 1414 "`pluckIf` `fn` must have an arity of 1 (take one input)" 1415 ) 1416 if isinstance(path, str): 1417 path = [path] 1418 return [ 1419 __pathOr__(default=None, path=path, data=i) for i in data if fn(i) 1420 ] 1421 1422 def project(self, keys: list[str], data: list[dict]): 1423 """ 1424 Function: 1425 1426 - Returns a list of dictionaries with only the keys provided 1427 - Analogous to SQL's `SELECT` statement 1428 1429 Requires: 1430 1431 - `keys`: 1432 - Type: list of strs 1433 - What: The keys to select from each dictionary in the data list 1434 - `data`: 1435 - Type: list of dicts 1436 - What: The list of dictionaries to select from 1437 1438 Example: 1439 1440 ``` 1441 data=[ 1442 {'a':1,'b':2,'c':3}, 1443 {'a':4,'b':5,'c':6} 1444 ] 1445 pamda.project(keys=['a','c'], data=data) 1446 #=> [ 1447 #=> {'a':1,'c':3}, 1448 #=> {'a':4,'c':6} 1449 #=> ] 1450 ``` 1451 """ 1452 return [{key: record[key] for key in keys} for record in data] 1453 1454 def props(self, keys: list[str], data: dict): 1455 """ 1456 Function: 1457 1458 - Returns the values of a list of keys within a dictionary 1459 1460 Requires: 1461 1462 - `keys`: 1463 - Type: list of strs 1464 - What: The keys to pull given the data 1465 - `data`: 1466 - Type: dict 1467 - What: A dictionary to get the keys from 1468 1469 Example: 1470 ``` 1471 data={'a':1,'b':2,'c':3} 1472 pamda.props(keys=['a','c'], data=data) 1473 #=> [1,3] 1474 ``` 1475 """ 1476 return [data[key] for key in keys] 1477 1478 def reduce(self, fn, initial_accumulator, data: list): 1479 """ 1480 Function: 1481 1482 - Returns a single item by iterating a function starting with an accumulator over a list 1483 1484 Requires: 1485 1486 - `fn`: 1487 - Type: function | method 1488 - What: The function or method to reduce 1489 - Note: This function should have an arity of 2 (take two inputs) 1490 - Note: The first input should take the accumulator value 1491 - Note: The second input should take the data value 1492 -`initial_accumulator`: 1493 - Type: any 1494 - What: The initial item to pass into the function when starting the accumulation process 1495 - `data`: 1496 - Type: list 1497 - What: The list of items to iterate over 1498 1499 Example: 1500 1501 ``` 1502 data=[1,2,3,4] 1503 pamda.reduce( 1504 fn=pamda.add, 1505 initial_accumulator=0, 1506 data=data 1507 ) 1508 #=> 10 1509 1510 ``` 1511 """ 1512 fn = self.curry(fn) 1513 if fn.__arity__ != 2: 1514 raise Exception( 1515 "`reduce` `fn` must have an arity of 2 (take two inputs)" 1516 ) 1517 if not isinstance(data, (list)): 1518 raise Exception("`reduce` `data` must be a list") 1519 if not len(data) > 0: 1520 raise Exception( 1521 "`reduce` `data` has a length of 0, however it must have a length of at least 1" 1522 ) 1523 acc = initial_accumulator 1524 for i in data: 1525 acc = fn(acc, i) 1526 return acc 1527 1528 def safeDivide(self, denominator: int | float, a: int | float): 1529 """ 1530 Function: 1531 1532 - Forces division to work by enforcing a denominator of 1 if the provided denominator is zero 1533 1534 Requires: 1535 1536 - `denominator`: 1537 - Type: int | float 1538 - What: The denominator 1539 1540 - `a`: 1541 - Type: int | float 1542 - What: The numerator 1543 1544 Example: 1545 1546 ``` 1547 pamda.safeDivide(2,10) #=> 5 1548 pamda.safeDivide(0,10) #=> 10 1549 ``` 1550 """ 1551 return a / denominator if denominator != 0 else a 1552 1553 def safeDivideDefault( 1554 self, 1555 default_denominator: int | float, 1556 denominator: int | float, 1557 a: int | float, 1558 ): 1559 """ 1560 Function: 1561 1562 - Forces division to work by enforcing a non zero default denominator if the provided denominator is zero 1563 1564 Requires: 1565 1566 - `default_denominator`: 1567 - Type: int | float 1568 - What: A non zero denominator to use if denominator is zero 1569 - Default: 1 1570 - `denominator`: 1571 - Type: int | float 1572 - What: The denominator 1573 - `a`: 1574 - Type: int | float 1575 - What: The numerator 1576 1577 Example: 1578 1579 ``` 1580 pamda.safeDivideDefault(2,5,10) #=> 2 1581 pamda.safeDivideDefault(2,0,10) #=> 5 1582 ``` 1583 """ 1584 if default_denominator == 0: 1585 raise Exception( 1586 "`safeDivideDefault` `default_denominator` can not be 0" 1587 ) 1588 return a / denominator if denominator != 0 else a / default_denominator 1589 1590 def symmetricDifference(self, a: list, b: list): 1591 """ 1592 Function: 1593 1594 - Combines two lists into a list of no duplicates items present in one list but not the other 1595 1596 Requires: 1597 1598 - `a`: 1599 - Type: list 1600 - What: List of items in which to look for a difference 1601 - `b`: 1602 - Type: list 1603 - What: List of items in which to look for a difference 1604 1605 Example: 1606 1607 ``` 1608 a=['a','b'] 1609 b=['b','c'] 1610 pamda.symmetricDifference(a=a, b=b) #=> ['a','c'] 1611 ``` 1612 """ 1613 return list(set(a).difference(set(b))) + list(set(b).difference(set(a))) 1614 1615 def tail(self, data: list | str): 1616 """ 1617 Function: 1618 1619 - Picks the last item out of a list or string 1620 1621 Requires: 1622 1623 - `data`: 1624 - Type: list | str 1625 - What: A list or string 1626 1627 Example: 1628 1629 ``` 1630 data=['fe','fi','fo','fum'] 1631 pamda.tail( 1632 data=data 1633 ) #=> fum 1634 ``` 1635 """ 1636 if not len(data) > 0: 1637 raise Exception("Attempting to call `tail` on an empty list or str") 1638 return data[-1] 1639 1640 def thunkify(self, fn): 1641 """ 1642 Function: 1643 1644 - Creates a curried thunk out of a function 1645 - Evaluation of the thunk lazy and is delayed until called 1646 1647 Requires: 1648 1649 - `fn`: 1650 - Type: function | method 1651 - What: The function or method to thunkify 1652 - Note: Thunkified functions are automatically curried 1653 - Note: Class methods auto apply self during thunkify 1654 1655 Notes: 1656 1657 - Input functions are not thunkified in place 1658 - The returned function is a thunkified version of the input function 1659 - A curried function can be thunkified in place by calling fn.thunkify() 1660 1661 Examples: 1662 1663 ``` 1664 def add(a,b): 1665 return a+b 1666 1667 addThunk=pamda.thunkify(add) 1668 1669 add(1,2) #=> 3 1670 addThunk(1,2) 1671 addThunk(1,2)() #=> 3 1672 1673 x=addThunk(1,2) 1674 x() #=> 3 1675 ``` 1676 1677 ``` 1678 @pamda.curry 1679 def add(a,b): 1680 return a+b 1681 1682 add(1,2) #=> 3 1683 1684 add.thunkify() 1685 1686 add(1,2) 1687 add(1,2)() #=> 3 1688 ``` 1689 """ 1690 fn = self.curry(fn) 1691 return fn.thunkify() 1692 1693 def unnest(self, data: list): 1694 """ 1695 Function: 1696 1697 - Removes one level of depth for all items in a list 1698 1699 Requires: 1700 1701 - `data`: 1702 - Type: list 1703 - What: A list of items to unnest by one level 1704 1705 Examples: 1706 1707 ``` 1708 data=['fe','fi',['fo',['fum']]] 1709 pamda.unnest( 1710 data=data 1711 ) #=> ['fe','fi','fo',['fum']] 1712 ``` 1713 """ 1714 if not len(data) > 0: 1715 raise Exception("Attempting to call `unnest` on an empty list") 1716 output = [] 1717 for i in data: 1718 if isinstance(i, list): 1719 output += i 1720 else: 1721 output.append(i) 1722 return output 1723 1724 def zip(self, a: list, b: list): 1725 """ 1726 Function: 1727 1728 - Creates a new list out of the two supplied by pairing up equally-positioned items from both lists 1729 1730 Requires: 1731 1732 - `a`: 1733 - Type: list 1734 - What: List of items to appear in new list first 1735 - `b`: 1736 - Type: list 1737 - What: List of items to appear in new list second 1738 1739 Example: 1740 1741 ``` 1742 a=['a','b'] 1743 b=[1,2] 1744 pamda.zip(a=a, b=b) #=> [['a',1],['b',2]] 1745 ``` 1746 """ 1747 return list(map(list, zip(a, b))) 1748 1749 def zipObj(self, a: list, b: list): 1750 """ 1751 Function: 1752 1753 - Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists 1754 - The first list represents keys and the second values 1755 1756 Requires: 1757 1758 - `a`: 1759 - Type: list 1760 - What: List of items to appear in new list first 1761 - `b`: 1762 - Type: list 1763 - What: List of items to appear in new list second 1764 1765 Example: 1766 1767 ``` 1768 a=['a','b'] 1769 b=[1,2] 1770 pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2} 1771 ``` 1772 """ 1773 return dict(zip(a, b))
17@pamda_wrappers.typed_curry_wrap 18@pamda_wrappers.classmethod_wrap 19class pamda(pamda_utils): 20 def accumulate(self, fn, initial_accumulator, data: list): 21 """ 22 Function: 23 24 - Returns an accumulated list of items by iterating a function starting with an accumulator over a list 25 26 Requires: 27 28 - `fn`: 29 - Type: function | method 30 - What: The function or method to reduce 31 - Note: This function should have an arity of 2 (take two inputs) 32 - Note: The first input should take the accumulator value 33 - Note: The second input should take the data value 34 -`initial_accumulator`: 35 - Type: any 36 - What: The initial item to pass into the function when starting the accumulation process 37 - `data`: 38 - Type: list 39 - What: The list of items to iterate over 40 41 Example: 42 43 ``` 44 data=[1,2,3,4] 45 pamda.accumulate( 46 fn=pamda.add, 47 initial_accumulator=0, 48 data=data 49 ) 50 #=> [1,3,6,10] 51 52 ``` 53 """ 54 fn = self.curry(fn) 55 if fn.__arity__ != 2: 56 raise Exception("`fn` must have an arity of 2 (take two inputs)") 57 if not len(data) > 0: 58 raise Exception( 59 "`data` has a length of 0, however it must have a length of at least 1" 60 ) 61 acc = initial_accumulator 62 out = [] 63 for i in data: 64 acc = fn(acc, i) 65 out.append(acc) 66 return out 67 68 def add(self, a: int | float, b: int | float): 69 """ 70 Function: 71 72 - Adds two numbers 73 74 Requires: 75 76 - `a`: 77 - Type: int | float 78 - What: The first number to add 79 - `b`: 80 - Type: int | float 81 - What: The second number to add 82 83 Example: 84 85 ``` 86 pamda.add(1, 2) #=> 3 87 ``` 88 """ 89 return a + b 90 91 def adjust(self, index: int, fn, data: list): 92 """ 93 Function: 94 95 - Adjusts an item in a list by applying a function to it 96 97 Requires: 98 99 - `index`: 100 - Type: int 101 - What: The 0 based index of the item in the list to adjust 102 - Note: Indicies are accepted 103 - Note: If the index is out of range, picks the (-)first / (+)last item 104 - `fn`: 105 - Type: function | method 106 - What: The function to apply the index item to 107 - Note: This is automatically curried 108 - `data`: 109 - Type: list 110 - What: The list to adjust 111 112 Example: 113 114 ``` 115 data=[1,5,9] 116 pamda.adjust( 117 index=1, 118 fn=pamda.inc, 119 data=data 120 ) #=> [1,6,9] 121 ``` 122 """ 123 fn = self.curry(fn) 124 index = self.clamp(-len(data), len(data) - 1, index) 125 data[index] = fn(data[index]) 126 return data 127 128 def assocPath(self, path: list | str | int | tuple, value, data: dict): 129 """ 130 Function: 131 132 - Ensures a path exists within a nested dictionary 133 - Note: This updates the object in place, but also returns the object 134 135 Requires: 136 137 - `path`: 138 - Type: list[str | int | tuple] | str | int | tuple 139 - What: The path to check 140 - Note: If a string is passed, assumes a single item path list with that string 141 - `value`: 142 - Type: any 143 - What: The value to appropriate to the end of the path 144 - `data`: 145 - Type: dict 146 - What: A dictionary in which to associate the given value to the given path 147 148 Example: 149 150 ``` 151 data={'a':{'b':1}} 152 pamda.assocPath(path=['a','c'], value=3, data=data) #=> {'a':{'b':1, 'c':3}} 153 ``` 154 """ 155 if not isinstance(path, list): 156 path = [path] 157 reduce(__getForceDict__, path[:-1], data).__setitem__(path[-1], value) 158 return data 159 160 def assocPathComplex( 161 self, default, default_fn, path: list | int | float | tuple, data: dict 162 ): 163 """ 164 Function: 165 166 - Ensures a path exists within a nested dictionary 167 - Note: This updates the object in place, but also returns the object 168 169 Requires: 170 171 - `default`: 172 - Type: any 173 - What: The default item to add to a path that does not yet exist 174 - `default_fn`: 175 - Type: function | method 176 - What: A unary (single input) function that takes in the current path item (or default) and adjusts it 177 - Example: `lambda x: x` # Returns the value in the dict or the default value if none was present 178 - `path`: 179 - Type: list[str | int | tuple] | str | int | tuple 180 - What: The path to check 181 - `data`: 182 - Type: dict 183 - What: A dictionary to check if the path exists 184 185 Example: 186 187 ``` 188 data={'a':{'b':1}} 189 pamda.assocPathComplex(default=[2], default_fn=lambda x:x+[1], path=['a','c'], data=data) #=> {'a':{'b':1,'c':[2,1]}} 190 ``` 191 """ 192 if self.getArity(default_fn) != 1: 193 raise Exception( 194 "`assocPathComplex` `default_fn` must be an unary (single input) function." 195 ) 196 if not isinstance(path, list): 197 path = [path] 198 path_object = reduce(__getForceDict__, path[:-1], data) 199 path_object.__setitem__( 200 path[-1], default_fn(path_object.get(path[-1], default)) 201 ) 202 return data 203 204 def asyncKill(self, fn: curry_obj): 205 """ 206 Function: 207 208 - Kills an asynchronous function that is currently running 209 - Returns: 210 - `None` if the function has not yet finished running 211 - The result of the function if it has finished running 212 213 Requires: 214 215 - `fn`: 216 - Type: thunkified function | thunkified method 217 - What: The function or method to run asychronously 218 - Note: The supplied `fn` must already be asynchronously running 219 220 Notes: 221 222 - See also `asyncRun` and `asyncWait` 223 - A thunkified function currently running asynchronously can call `asyncKill` on itself 224 - If a function has already finished running, calling `asyncKill` on it will have no effect 225 - `asyncKill` does not kill threads that are sleeping (EG: `time.sleep`), but will kill the thread once the sleep is finished 226 227 Example: 228 229 ``` 230 import time 231 from pamda import pamda 232 233 @pamda.thunkify 234 def test(name, wait): 235 waited = 0 236 while waited < wait: 237 time.sleep(1) 238 waited += 1 239 print(f'{name} has waited {waited} seconds') 240 241 async_test = pamda.asyncRun(test('a',3)) 242 time.sleep(1) 243 pamda.asyncKill(async_test) 244 # Alternatively: 245 # async_test.asyncKill() 246 ``` 247 """ 248 return fn.asyncKill() 249 250 def asyncRun(self, fn: curry_obj): 251 """ 252 Function: 253 254 - Runs the supplied function asychronously 255 256 Requires: 257 258 - `fn`: 259 - Type: thunkified function | thunkified method 260 - What: The function or method to run asychronously 261 - Note: The supplied `fn` must have an arity of 0 262 263 Notes: 264 265 - To pass inputs to a function in asyncRun, first thunkify the function and pass all arguments before calling `asyncRun` on it 266 - To get the results of an `asyncRun` call `asyncWait` 267 - To kill an `asyncRun` mid process call `asyncKill` 268 - A thunkified function with arity of 0 can call `asyncRun` on itself 269 270 Examples: 271 272 Input: 273 ``` 274 import time 275 276 @pamda.thunkify 277 def test(name, wait): 278 print(f'{name} start') 279 time.sleep(wait) 280 print(f'{name} end') 281 282 async_test = pamda.asyncRun(test('a',2)) 283 sync_test = test('b',1)() 284 ``` 285 Output: 286 ``` 287 a start 288 b start 289 b end 290 a end 291 ``` 292 293 294 Input: 295 ``` 296 import time 297 298 @pamda.thunkify 299 def test(name, wait): 300 time.sleep(wait) 301 return f"{name}: {wait}" 302 303 async_test = pamda.asyncRun(test('a',2)) 304 print(async_test.asyncWait()) #=> a: 2 305 ``` 306 307 308 Input: 309 ``` 310 import time 311 312 @pamda.thunkify 313 def test(name, wait): 314 time.sleep(wait) 315 return f"{name}: {wait}" 316 317 async_test = test('a',2).asyncRun() 318 print(async_test.asyncWait()) #=> a: 2 319 ``` 320 """ 321 return fn.asyncRun() 322 323 def asyncWait(self, fn: curry_obj): 324 """ 325 Function: 326 327 - Waits for a supplied function (if needed) and returns the results 328 329 Requires: 330 331 - `fn`: 332 - Type: function | method 333 - What: The function or method for which to wait 334 - Note: The supplied `fn` must have previously called `asyncRun` 335 336 Notes: 337 338 - A thunkified function that has called `asyncRun` can call `asyncWait` on itself 339 340 Examples: 341 342 ``` 343 import time 344 345 @pamda.thunkify 346 def test(name, wait): 347 time.sleep(wait) 348 return f"{name}: {wait}" 349 350 async_test = pamda.asyncRun(test('a',2)) 351 print(pamda.asyncWait(async_test)) #=> a: 2 352 ``` 353 354 355 ``` 356 import time 357 358 @pamda.thunkify 359 def test(name, wait): 360 time.sleep(wait) 361 return f"{name}: {wait}" 362 363 async_test = pamda.asyncRun(test('a',2)) 364 print(async_test.asyncWait()) #=> a: 2 365 ``` 366 """ 367 return fn.asyncWait() 368 369 def clamp(self, minimum: int | float, maximum: int | float, a: int | float): 370 """ 371 Function: 372 373 - Forces data to be within minimum and maximum 374 375 Requires: 376 377 - `minimum`: 378 - Type: int | float 379 - What: The minimum number 380 - `maximum`: 381 - Type: int | float 382 - What: The maximum number 383 - `a`: 384 - Type: int | float 385 - What: The number to clamp 386 387 Example: 388 389 ``` 390 pamda.clamp(1, 3, 2) #=> 2 391 pamda.clamp(1, 3, 5) #=> 3 392 ``` 393 """ 394 return min(max(a, minimum), maximum) 395 396 def curry(self, fn): 397 """ 398 Function: 399 400 - Curries a function such that inputs can be added interatively 401 402 Requires: 403 404 - `fn`: 405 - Type: function | method 406 - What: The function or method to curry 407 - Note: Class methods auto apply self during curry 408 409 Notes: 410 411 - Once curried, the function | method becomes a curry_obj object 412 - The initial function is only called once all inputs are passed 413 414 415 Examples: 416 417 ``` 418 curriedZip=pamda.curry(pamda.zip) 419 curriedZip(['a','b'])([1,2]) #=> [['a',1],['b',2]] 420 421 # Curried functions can be thunkified at any time 422 # See also thunkify 423 zipThunk=curriedZip.thunkify()(['a','b'])([1,2]) 424 zipThunk() #=> [['a',1],['b',2]] 425 ``` 426 427 ``` 428 def myFunction(a,b,c): 429 return [a,b,c] 430 431 curriedMyFn=pamda.curry(myFunction) 432 433 curriedMyFn(1,2,3) #=> [1,2,3] 434 curriedMyFn(1)(2,3) #=> [1,2,3] 435 436 x=curriedMyFn(1)(2) 437 x(3) #=> [1,2,3] 438 x(4) #=> [1,2,4] 439 440 441 ``` 442 """ 443 if fn.__dict__.get("__isCurried__"): 444 return fn() 445 return curry_obj(fn) 446 447 def curryTyped(self, fn): 448 """ 449 Function: 450 451 - Curries a function such that inputs can be added interatively and function annotations are type checked at runtime 452 453 Requires: 454 455 - `fn`: 456 - Type: function | method 457 - What: The function or method to curry 458 - Note: Class methods auto apply self during curry 459 460 Notes: 461 462 - Once curried, the function | method becomes a curry_obj object 463 - The initial function is only called once all inputs are passed 464 465 466 Examples: 467 468 ``` 469 @pamda.curryTyped 470 def add(a:int,b:int): 471 return a+b 472 473 add(1)(1) #=> 2 474 add(1)(1.5) #=> Raises type exception 475 ``` 476 """ 477 if fn.__dict__.get("__isCurried__"): 478 return fn().typeEnforce() 479 return curry_obj(fn).typeEnforce() 480 481 def dec(self, a: int | float): 482 """ 483 Function: 484 485 - Decrements a number by one 486 487 Requires: 488 489 - `a`: 490 - Type: int | float 491 - What: The number to decrement 492 493 Example: 494 495 ``` 496 pamda.dec(42) #=> 41 497 ``` 498 """ 499 if not isinstance(a, (int, float)): 500 raise Exception("`a` must be an `int` or a `float`") 501 return a - 1 502 503 def difference(self, a: list, b: list): 504 """ 505 Function: 506 507 - Combines two lists into a list of no duplicate items present in the first list but not the second 508 509 Requires: 510 511 - `a`: 512 - Type: list 513 - What: List of items in which to look for a difference 514 - `b`: 515 - Type: list 516 - What: List of items in which to compare when looking for the difference 517 518 Example: 519 520 ``` 521 a=['a','b'] 522 b=['b','c'] 523 pamda.difference(a=a, b=b) #=> ['a'] 524 pamda.difference(a=b, b=a) #=> ['c'] 525 ``` 526 """ 527 return list(set(a).difference(set(b))) 528 529 def dissocPath(self, path: list | str | int | tuple, data: dict): 530 """ 531 Function: 532 533 - Removes the value at the end of a path within a nested dictionary 534 - Note: This updates the object in place, but also returns the object 535 536 Requires: 537 538 - `path`: 539 - Type: list of strs | str 540 - What: The path to remove from the dictionary 541 - Note: If a string is passed, assumes a single item path list with that string 542 - `data`: 543 - Type: dict 544 - What: A dictionary with a path to be removed 545 546 Example: 547 548 ``` 549 data={'a':{'b':{'c':0,'d':1}}} 550 pamda.dissocPath(path=['a','b','c'], data=data) #=> {'a':{'b':{'d':1}}} 551 ``` 552 """ 553 if not isinstance(path, list): 554 path = [path] 555 if not self.hasPath(path=path, data=data): 556 raise Exception("Path does not exist") 557 else: 558 reduce(__getForceDict__, path[:-1], data).pop(path[-1]) 559 return data 560 561 def flatten(self, data: list): 562 """ 563 Function: 564 565 - Flattens a list of lists of lists ... into a single list depth first 566 567 Requires: 568 569 - `data`: 570 - Type: list of lists 571 - What: The list of lists to reduce to a single list 572 Example: 573 574 ``` 575 data=[['a','b'],[1,[2]]] 576 pamda.flatten(data=data) #=> ['a','b',1,2] 577 ``` 578 """ 579 580 def iter_flatten(data): 581 out = [] 582 for i in data: 583 if isinstance(i, list): 584 out.extend(iter_flatten(i)) 585 else: 586 out.append(i) 587 return out 588 589 return iter_flatten(data) 590 591 def flip(self, fn): 592 """ 593 Function: 594 595 - Returns a new function equivalent to the supplied function except that the first two inputs are flipped 596 597 Requires: 598 599 - `fn`: 600 - Type: function | method 601 - What: The function or method to flip 602 - Note: This function must have an arity of at least 2 (take two inputs) 603 - Note: Only args are flipped, kwargs are passed as normal 604 605 Notes: 606 607 - Input functions are not flipped in place 608 - The returned function is a flipped version of the input function 609 - A curried function can be flipped in place by calling fn.flip() 610 - A function can be flipped multiple times: 611 - At each flip, the first and second inputs for the function as it is currently curried are switched 612 - Flipping a function two times before adding an input will return the initial value 613 614 Examples: 615 616 ``` 617 def concat(a,b,c,d): 618 return str(a)+str(b)+str(c)+str(d) 619 620 flip_concat=pamda.flip(concat) 621 622 concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum' 623 flip_concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum' 624 ``` 625 626 ``` 627 @pamda.curry 628 def concat(a,b,c,d): 629 return str(a)+str(b)+str(c)+str(d) 630 631 concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum' 632 633 concat.flip() 634 635 concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum' 636 ``` 637 638 ``` 639 @pamda.curry 640 def concat(a,b,c,d): 641 return str(a)+str(b)+str(c)+str(d) 642 643 a=pamda.flip(concat)('fi-') 644 b=pamda.flip(a)('fo-') 645 c=pamda.flip(b)('fum') 646 c('fe-') #=> 'fe-fi-fo-fum' 647 ``` 648 649 ``` 650 def concat(a,b,c,d): 651 return str(a)+str(b)+str(c)+str(d) 652 653 a=pamda.flip(concat)('fi-').flip()('fo-').flip()('fum') 654 a('fe-') #=> 'fe-fi-fo-fum' 655 ``` 656 """ 657 fn = self.curry(fn) 658 return fn.flip() 659 660 def getArity(self, fn): 661 """ 662 Function: 663 664 - Gets the arity (number of inputs left to be specified) of a function or method (curried or uncurried) 665 666 Requires: 667 668 - `fn`: 669 - Type: function | method 670 - What: The function or method to get the arity of 671 - Note: Class methods remove one arity to account for self 672 673 Examples: 674 675 ``` 676 pamda.getArity(pamda.zip) #=> 2 677 curriedZip=pamda.curry(pamda.zip) 678 ABCuriedZip=curriedZip(['a','b']) 679 pamda.getArity(ABCuriedZip) #=> 1 680 ``` 681 """ 682 fn = self.curry(fn) 683 return fn.__arity__ 684 685 def groupBy(self, fn, data: list): 686 """ 687 Function: 688 689 - Splits a list into a dictionary of sublists keyed by the return string of a provided function 690 691 Requires: 692 693 - `fn`: 694 - Type: function | method 695 - What: The function or method to group by 696 - Note: Must return a string (or other hashable object) 697 - Note: This function must be unary (take one input) 698 - Note: This function is applied to each item in the list recursively 699 - `data`: 700 - Type: list 701 - What: List of items to apply the function to and then group by the results 702 703 Examples: 704 705 ``` 706 def getGrade(item): 707 score=item['score'] 708 if score>90: 709 return 'A' 710 elif score>80: 711 return 'B' 712 elif score>70: 713 return 'C' 714 elif score>60: 715 return 'D' 716 else: 717 return 'F' 718 719 data=[ 720 {'name':'Connor', 'score':75}, 721 {'name':'Fred', 'score':79}, 722 {'name':'Joe', 'score':84}, 723 ] 724 pamda.groupBy(getGrade,data) 725 #=>{ 726 #=> 'B':[{'name':'Joe', 'score':84}] 727 #=> 'C':[{'name':'Connor', 'score':75},{'name':'Fred', 'score':79}] 728 #=>} 729 ``` 730 """ 731 curried_fn = self.curry(fn) 732 if curried_fn.__arity__ != 1: 733 raise Exception( 734 "groupBy `fn` must only take one parameter as its input" 735 ) 736 return __groupByHashable__(fn=fn, data=data) 737 738 def groupKeys(self, keys: list, data: list): 739 """ 740 Function: 741 742 - Splits a list of dicts into a list of sublists of dicts separated by values with equal keys 743 744 Requires: 745 746 - `keys`: 747 - Type: list of strs 748 - What: The keys to group by 749 - `data`: 750 - Type: list of dicts 751 - What: List of dictionaries with which to match keys 752 753 Examples: 754 755 ``` 756 data=[ 757 {'color':'red', 'size':9, 'shape':'ball'}, 758 {'color':'red', 'size':10, 'shape':'ball'}, 759 {'color':'green', 'size':11, 'shape':'ball'}, 760 {'color':'green', 'size':12, 'shape':'square'} 761 ] 762 pamda.groupKeys(['color','shape'],data) 763 #=> [ 764 #=> [{'color': 'red', 'size': 9, 'shape': 'ball'}, {'color': 'red', 'size': 10, 'shape': 'ball'}], 765 #=> [{'color': 'green', 'size': 11, 'shape': 'ball'}], 766 #=> [{'color': 'green', 'size': 12, 'shape': 'square'}] 767 #=> ] 768 ``` 769 """ 770 771 def key_fn(item): 772 return tuple([item[key] for key in keys]) 773 774 return list(__groupByHashable__(key_fn, data).values()) 775 776 def groupWith(self, fn, data: list): 777 """ 778 Function: 779 780 - Splits a list into a list of sublists where each sublist is determined by adjacent pairwise comparisons from a provided function 781 782 Requires: 783 784 - `fn`: 785 - Type: function | method 786 - What: The function or method to groub with 787 - Note: Must return a boolean value 788 - Note: This function must have an arity of two (take two inputs) 789 - Note: This function is applied to each item plus the next adjacent item in the list recursively 790 - `data`: 791 - Type: list 792 - What: List of items to apply the function to and then group the results 793 794 Examples: 795 796 ``` 797 def areEqual(a,b): 798 return a==b 799 800 data=[1,2,3,1,1,2,2,3,3,3] 801 pamda.groupWith(areEqual,data) #=> [[1], [2], [3], [1, 1], [2, 2], [3, 3, 3]] 802 ``` 803 """ 804 curried_fn = self.curry(fn) 805 if curried_fn.__arity__ != 2: 806 raise Exception("groupWith `fn` must take exactly two parameters") 807 previous = data[0] 808 output = [] 809 sublist = [previous] 810 for i in data[1:]: 811 if fn(i, previous): 812 sublist.append(i) 813 else: 814 output.append(sublist) 815 sublist = [i] 816 previous = i 817 output.append(sublist) 818 return output 819 820 def hasPath(self, path: list | str, data: dict): 821 """ 822 Function: 823 824 - Checks if a path exists within a nested dictionary 825 826 Requires: 827 828 - `path`: 829 - Type: list of strs | str 830 - What: The path to check 831 - Note: If a string is passed, assumes a single item path list with that string 832 - `data`: 833 - Type: dict 834 - What: A dictionary to check if the path exists 835 836 Example: 837 838 ``` 839 data={'a':{'b':1}} 840 pamda.hasPath(path=['a','b'], data=data) #=> True 841 pamda.hasPath(path=['a','d'], data=data) #=> False 842 ``` 843 """ 844 if isinstance(path, str): 845 path = [path] 846 return path[-1] in reduce(lambda x, y: x.get(y, {}), path[:-1], data) 847 848 def hardRound(self, decimal_places: int, a: int | float): 849 """ 850 Function: 851 852 - Rounds to a set number of decimal places regardless of floating point math in python 853 854 Requires: 855 856 - `decimal_places`: 857 - Type: int 858 - What: The number of decimal places to round to 859 - Default: 0 860 - Notes: Negative numbers accepted (EG -1 rounds to the nearest 10) 861 - `a`: 862 - Type: int | float 863 - What: The number to round 864 865 Example: 866 867 ``` 868 a=12.345 869 pamda.hardRound(1,a) #=> 12.3 870 pamda.hardRound(-1,a) #=> 10 871 ``` 872 """ 873 return int(a * (10**decimal_places) + 0.5) / (10**decimal_places) 874 875 def head(self, data: list | str): 876 """ 877 Function: 878 879 - Picks the first item out of a list or string 880 881 Requires: 882 883 - `data`: 884 - Type: list | str 885 - What: A list or string 886 887 Example: 888 889 ``` 890 data=['fe','fi','fo','fum'] 891 pamda.first( 892 data=data 893 ) #=> fe 894 ``` 895 """ 896 if not isinstance(data, (list, str)): 897 raise Exception("`head` can only be called on a `str` or a `list`") 898 if not len(data) > 0: 899 raise Exception("Attempting to call `head` on an empty list or str") 900 return data[0] 901 902 def inc(self, a: int | float): 903 """ 904 Function: 905 906 - Increments a number by one 907 908 Requires: 909 910 - `a`: 911 - Type: int | float 912 - What: The number to increment 913 914 Example: 915 916 ``` 917 pamda.inc(42) #=> 43 918 ``` 919 """ 920 if not isinstance(a, (int, float)): 921 raise Exception("`a` must be an `int` or a `float`") 922 return a + 1 923 924 def intersection(self, a: list, b: list): 925 """ 926 Function: 927 928 - Combines two lists into a list of no duplicates composed of those elements common to both lists 929 930 Requires: 931 932 - `a`: 933 - Type: list 934 - What: List of items in which to look for an intersection 935 - `b`: 936 - Type: list 937 - What: List of items in which to look for an intersection 938 939 Example: 940 941 ``` 942 a=['a','b'] 943 b=['b','c'] 944 pamda.intersection(a=a, b=b) #=> ['b'] 945 ``` 946 """ 947 return list(set(a).intersection(set(b))) 948 949 def map(self, fn, data: list | dict): 950 """ 951 Function: 952 953 - Maps a function over a list or a dictionary 954 955 Requires: 956 957 - `fn`: 958 - Type: function | method 959 - What: The function or method to map over the list or dictionary 960 - Note: This function should have an arity of 1 961 - `data`: 962 - Type: list | dict 963 - What: The list or dict of items to map the function over 964 965 Examples: 966 967 ``` 968 data=[1,2,3] 969 pamda.map( 970 fn=pamda.inc, 971 data=data 972 ) 973 #=> [2,3,4] 974 ``` 975 976 ``` 977 data={'a':1,'b':2,'c':3} 978 pamda.map( 979 fn=pamda.inc, 980 data=data 981 ) 982 #=> {'a':2,'b':3,'c':4} 983 ``` 984 985 """ 986 # TODO: Check for efficiency gains 987 fn = self.curry(fn) 988 if fn.__arity__ != 1: 989 raise Exception("`map` `fn` must be unary (take one input)") 990 if not len(data) > 0: 991 raise Exception( 992 "`map` `data` has a length of 0 or is an empty dictionary, however it must have at least one element in it" 993 ) 994 if isinstance(data, dict): 995 return {key: fn(value) for key, value in data.items()} 996 else: 997 return [fn(i) for i in data] 998 999 def mean(self, data: list): 1000 """ 1001 Function: 1002 1003 - Calculates the mean of a given list 1004 1005 Requires: 1006 1007 - `data`: 1008 - Type: list of (floats | ints) 1009 - What: The list with wich to calculate the mean 1010 - Note: If the length of this list is 0, returns None 1011 1012 Example: 1013 1014 ``` 1015 data=[1,2,3] 1016 pamda.mean(data=data) 1017 #=> 2 1018 ``` 1019 1020 ``` 1021 data=[] 1022 pamda.mean(data=data) 1023 #=> None 1024 ``` 1025 """ 1026 if len(data) == 0: 1027 return None 1028 return sum(data) / len(data) 1029 1030 def median(self, data: list): 1031 """ 1032 Function: 1033 1034 - Calculates the median of a given list 1035 - If the length of the list is even, calculates the mean of the two central values 1036 1037 Requires: 1038 1039 - `data`: 1040 - Type: list of (floats | ints) 1041 - What: The list with wich to calculate the mean 1042 - Note: If the length of this list is 0, returns None 1043 1044 Examples: 1045 1046 ``` 1047 data=[7,2,8,9] 1048 pamda.median(data=data) 1049 #=> 7.5 1050 ``` 1051 1052 ``` 1053 data=[7,8,9] 1054 pamda.median(data=data) 1055 #=> 8 1056 ``` 1057 1058 ``` 1059 data=[] 1060 pamda.median(data=data) 1061 #=> None 1062 ``` 1063 """ 1064 if not isinstance(data, (list)): 1065 raise Exception("`median` `data` must be a list") 1066 length = len(data) 1067 if length == 0: 1068 return None 1069 data = sorted(data) 1070 if length % 2 == 0: 1071 return (data[int(length / 2)] + data[int(length / 2) - 1]) / 2 1072 return data[int(length / 2)] 1073 1074 def mergeDeep(self, update_data, data): 1075 """ 1076 Function: 1077 1078 - Recursively merges two nested dictionaries keeping all keys at each layer 1079 - Values from `update_data` are used when keys are present in both dictionaries 1080 1081 Requires: 1082 1083 - `update_data`: 1084 - Type: any 1085 - What: The new data that will take precedence during merging 1086 - `data`: 1087 - Type: any 1088 - What: The original data that will be merged into 1089 1090 Example: 1091 1092 ``` 1093 data={'a':{'b':{'c':'d'},'e':'f'}} 1094 update_data={'a':{'b':{'h':'i'},'e':'g'}} 1095 pamda.mergeDeep( 1096 update_data=update_data, 1097 data=data 1098 ) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}} 1099 ``` 1100 """ 1101 return __mergeDeep__(update_data, data) 1102 1103 def nest(self, path_keys: list, value_key: str, data: list): 1104 """ 1105 Function: 1106 1107 - Nests a list of dictionaries into a nested dictionary 1108 - Similar items are appended to a list in the end of the nested dictionary 1109 1110 Requires: 1111 1112 - `path_keys`: 1113 - Type: list of strs 1114 - What: The variables to pull from each item in data 1115 - Note: Used to build out the nested dicitonary 1116 - Note: Order matters as the nesting occurs in order of variable 1117 - `value_key`: 1118 - Type: str 1119 - What: The variable to add to the list at the end of the nested dictionary path 1120 - `data`: 1121 - Type: list of dicts 1122 - What: A list of dictionaries to use for nesting purposes 1123 1124 Example: 1125 1126 ``` 1127 data=[ 1128 {'x_1':'a','x_2':'b', 'output':'c'}, 1129 {'x_1':'a','x_2':'b', 'output':'d'}, 1130 {'x_1':'a','x_2':'e', 'output':'f'} 1131 ] 1132 pamda.nest( 1133 path_keys=['x_1','x_2'], 1134 value_key='output', 1135 data=data 1136 ) #=> {'a':{'b':['c','d'], 'e':['f']}} 1137 ``` 1138 """ 1139 if not isinstance(data, list): 1140 raise Exception("Attempting to `nest` an object that is not a list") 1141 if len(data) == 0: 1142 raise Exception("Attempting to `nest` from an empty list") 1143 nested_output = {} 1144 for item in self.groupKeys(keys=path_keys, data=data): 1145 nested_output = __assocPath__( 1146 path=__getKeyValues__(path_keys, item[0]), 1147 value=[i.get(value_key) for i in item], 1148 data=nested_output, 1149 ) 1150 return nested_output 1151 1152 def nestItem(self, path_keys: list, data: list): 1153 """ 1154 Function: 1155 1156 - Nests a list of dictionaries into a nested dictionary 1157 - Similar items are appended to a list in the end of the nested dictionary 1158 - Similar to `nest`, except no values are plucked for the aggregated list 1159 1160 Requires: 1161 1162 - `path_keys`: 1163 - Type: list of strs 1164 - What: The variables to pull from each item in data 1165 - Note: Used to build out the nested dicitonary 1166 - Note: Order matters as the nesting occurs in order of variable 1167 - `data`: 1168 - Type: list of dicts 1169 - What: A list of dictionaries to use for nesting purposes 1170 1171 Example: 1172 1173 ``` 1174 data=[ 1175 {'x_1':'a','x_2':'b'}, 1176 {'x_1':'a','x_2':'b'}, 1177 {'x_1':'a','x_2':'e'} 1178 ] 1179 pamda.nestItem 1180 path_keys=['x_1','x_2'], 1181 data=data 1182 ) 1183 #=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}} 1184 1185 ``` 1186 """ 1187 if not isinstance(data, list): 1188 raise Exception("Attempting to `nest` an object that is not a list") 1189 if len(data) == 0: 1190 raise Exception("Attempting to `nest` from an empty list") 1191 nested_output = {} 1192 for item in self.groupKeys(keys=path_keys, data=data): 1193 nested_output = __assocPath__( 1194 path=__getKeyValues__(path_keys, item[0]), 1195 value=item, 1196 data=nested_output, 1197 ) 1198 return nested_output 1199 1200 def path(self, path: list | str, data: dict): 1201 """ 1202 Function: 1203 1204 - Returns the value of a path within a nested dictionary or None if the path does not exist 1205 1206 Requires: 1207 1208 - `path`: 1209 - Type: list of strs | str 1210 - What: The path to pull given the data 1211 - Note: If a string is passed, assumes a single item path list with that string 1212 - `data`: 1213 - Type: dict 1214 - What: A dictionary to get the path from 1215 1216 Example: 1217 1218 ``` 1219 data={'a':{'b':1}} 1220 pamda.path(path=['a','b'], data=data) #=> 1 1221 ``` 1222 """ 1223 if isinstance(path, str): 1224 path = [path] 1225 return __pathOr__(None, path, data) 1226 1227 def pathOr(self, default, path: list | str, data: dict): 1228 """ 1229 Function: 1230 1231 - Returns the value of a path within a nested dictionary or a default value if that path does not exist 1232 1233 Requires: 1234 1235 - `default`: 1236 - Type: any 1237 - What: The object to return if the path does not exist 1238 - `path`: 1239 - Type: list of strs | str 1240 - What: The path to pull given the data 1241 - Note: If a string is passed, assumes a single item path list with that string 1242 - `data`: 1243 - Type: dict 1244 - What: A dictionary to get the path from 1245 1246 Example: 1247 1248 ``` 1249 data={'a':{'b':1}} 1250 pamda.path(default=2, path=['a','c'], data=data) #=> 2 1251 ``` 1252 """ 1253 if isinstance(path, str): 1254 path = [path] 1255 return reduce(lambda x, y: x.get(y, {}), path[:-1], data).get( 1256 path[-1], default 1257 ) 1258 1259 def pipe(self, fns: list, args: tuple, kwargs: dict): 1260 """ 1261 Function: 1262 1263 - Pipes data through n functions in order (left to right composition) and returns the output 1264 1265 Requires: 1266 1267 - `fns`: 1268 - Type: list of (functions | methods) 1269 - What: The list of functions and methods to pipe the data through 1270 - Notes: The first function in the list can be any arity (accepting any number of inputs) 1271 - Notes: Any further function in the list can only be unary (single input) 1272 - Notes: A function can be curried, but is not required to be 1273 - Notes: You may opt to curry functions and add inputs to make them unary 1274 - `args`: 1275 - Type: tuple 1276 - What: a tuple of positional arguments to pass to the first function in `fns` 1277 - `kwargs`: 1278 - Type: dict 1279 - What: a dictionary of keyword arguments to pass to the first function in `fns` 1280 1281 Examples: 1282 1283 ``` 1284 data=['abc','def'] 1285 pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c' 1286 pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c' 1287 ``` 1288 1289 ``` 1290 data={'a':{'b':'c'}} 1291 curriedPath=pamda.curry(pamda.path) 1292 pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c' 1293 ``` 1294 """ 1295 if len(fns) == 0: 1296 raise Exception("`fns` must be a list with at least one function") 1297 if self.getArity(fns[0]) == 0: 1298 raise Exception( 1299 "The first function in `fns` can have n arity (accepting n args), but this must be greater than 0." 1300 ) 1301 if not all([(self.getArity(fn) == 1) for fn in fns[1:]]): 1302 raise Exception( 1303 "Only the first function in `fns` can have n arity (accept n args). All other functions must have an arity of one (accepting one argument)." 1304 ) 1305 out = fns[0](*args, **kwargs) 1306 for fn in fns[1:]: 1307 out = fn(out) 1308 return out 1309 1310 def pivot(self, data: list[dict] | dict[Any, list]): 1311 """ 1312 Function: 1313 1314 - Pivots a list of dictionaries into a dictionary of lists 1315 - Pivots a dictionary of lists into a list of dictionaries 1316 1317 Requires: 1318 1319 - `data`: 1320 - Type: list of dicts | dict of lists 1321 - What: The data to pivot 1322 - Note: If a list of dictionaries is passed, all dictionaries must have the same keys 1323 - Note: If a dictionary of lists is passed, all lists must have the same length 1324 1325 Example: 1326 1327 ``` 1328 data=[ 1329 {'a':1,'b':2}, 1330 {'a':3,'b':4} 1331 ] 1332 pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]} 1333 1334 data={'a':[1,3],'b':[2,4]} 1335 pamda.pivot(data=data) 1336 #=> [ 1337 #=> {'a':1,'b':2}, 1338 #=> {'a':3,'b':4} 1339 #=> ] 1340 ``` 1341 """ 1342 if isinstance(data, list): 1343 return { 1344 key: [record[key] for record in data] for key in data[0].keys() 1345 } 1346 else: 1347 return [ 1348 {key: data[key][i] for key in data.keys()} 1349 for i in range(len(data[list(data.keys())[0]])) 1350 ] 1351 1352 def pluck(self, path: list | str, data: list): 1353 """ 1354 Function: 1355 1356 - Returns the values of a path within a list of nested dictionaries 1357 1358 Requires: 1359 1360 - `path`: 1361 - Type: list of strs 1362 - What: The path to pull given the data 1363 - Note: If a string is passed, assumes a single item path list with that string 1364 - `data`: 1365 - Type: list of dicts 1366 - What: A list of dictionaries to get the path from 1367 1368 Example: 1369 1370 ``` 1371 data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}] 1372 pamda.pluck(path=['a','b'], data=data) #=> [1,2] 1373 ``` 1374 """ 1375 if len(data) == 0: 1376 raise Exception("Attempting to pluck from an empty list") 1377 if isinstance(path, str): 1378 path = [path] 1379 return [__pathOr__(default=None, path=path, data=i) for i in data] 1380 1381 def pluckIf(self, fn, path: list | str, data: list): 1382 """ 1383 Function: 1384 1385 - Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value 1386 1387 Requires: 1388 1389 - `fn`: 1390 - Type: function 1391 - What: A function to take in each item in data and return a boolean 1392 - Note: Only items that return true are plucked 1393 - Note: Should be a unary function (take one input) 1394 - `path`: 1395 - Type: list of strs 1396 - What: The path to pull given the data 1397 - Note: If a string is passed, assumes a single item path list with that string 1398 - `data`: 1399 - Type: list of dicts 1400 - What: A list of dictionary to get the path from 1401 1402 Example: 1403 1404 ``` 1405 1406 data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}] 1407 pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d'] 1408 ``` 1409 """ 1410 if len(data) == 0: 1411 raise Exception("Attempting to pluck from an empty list") 1412 curried_fn = self.curry(fn) 1413 if curried_fn.__arity__ != 1: 1414 raise Exception( 1415 "`pluckIf` `fn` must have an arity of 1 (take one input)" 1416 ) 1417 if isinstance(path, str): 1418 path = [path] 1419 return [ 1420 __pathOr__(default=None, path=path, data=i) for i in data if fn(i) 1421 ] 1422 1423 def project(self, keys: list[str], data: list[dict]): 1424 """ 1425 Function: 1426 1427 - Returns a list of dictionaries with only the keys provided 1428 - Analogous to SQL's `SELECT` statement 1429 1430 Requires: 1431 1432 - `keys`: 1433 - Type: list of strs 1434 - What: The keys to select from each dictionary in the data list 1435 - `data`: 1436 - Type: list of dicts 1437 - What: The list of dictionaries to select from 1438 1439 Example: 1440 1441 ``` 1442 data=[ 1443 {'a':1,'b':2,'c':3}, 1444 {'a':4,'b':5,'c':6} 1445 ] 1446 pamda.project(keys=['a','c'], data=data) 1447 #=> [ 1448 #=> {'a':1,'c':3}, 1449 #=> {'a':4,'c':6} 1450 #=> ] 1451 ``` 1452 """ 1453 return [{key: record[key] for key in keys} for record in data] 1454 1455 def props(self, keys: list[str], data: dict): 1456 """ 1457 Function: 1458 1459 - Returns the values of a list of keys within a dictionary 1460 1461 Requires: 1462 1463 - `keys`: 1464 - Type: list of strs 1465 - What: The keys to pull given the data 1466 - `data`: 1467 - Type: dict 1468 - What: A dictionary to get the keys from 1469 1470 Example: 1471 ``` 1472 data={'a':1,'b':2,'c':3} 1473 pamda.props(keys=['a','c'], data=data) 1474 #=> [1,3] 1475 ``` 1476 """ 1477 return [data[key] for key in keys] 1478 1479 def reduce(self, fn, initial_accumulator, data: list): 1480 """ 1481 Function: 1482 1483 - Returns a single item by iterating a function starting with an accumulator over a list 1484 1485 Requires: 1486 1487 - `fn`: 1488 - Type: function | method 1489 - What: The function or method to reduce 1490 - Note: This function should have an arity of 2 (take two inputs) 1491 - Note: The first input should take the accumulator value 1492 - Note: The second input should take the data value 1493 -`initial_accumulator`: 1494 - Type: any 1495 - What: The initial item to pass into the function when starting the accumulation process 1496 - `data`: 1497 - Type: list 1498 - What: The list of items to iterate over 1499 1500 Example: 1501 1502 ``` 1503 data=[1,2,3,4] 1504 pamda.reduce( 1505 fn=pamda.add, 1506 initial_accumulator=0, 1507 data=data 1508 ) 1509 #=> 10 1510 1511 ``` 1512 """ 1513 fn = self.curry(fn) 1514 if fn.__arity__ != 2: 1515 raise Exception( 1516 "`reduce` `fn` must have an arity of 2 (take two inputs)" 1517 ) 1518 if not isinstance(data, (list)): 1519 raise Exception("`reduce` `data` must be a list") 1520 if not len(data) > 0: 1521 raise Exception( 1522 "`reduce` `data` has a length of 0, however it must have a length of at least 1" 1523 ) 1524 acc = initial_accumulator 1525 for i in data: 1526 acc = fn(acc, i) 1527 return acc 1528 1529 def safeDivide(self, denominator: int | float, a: int | float): 1530 """ 1531 Function: 1532 1533 - Forces division to work by enforcing a denominator of 1 if the provided denominator is zero 1534 1535 Requires: 1536 1537 - `denominator`: 1538 - Type: int | float 1539 - What: The denominator 1540 1541 - `a`: 1542 - Type: int | float 1543 - What: The numerator 1544 1545 Example: 1546 1547 ``` 1548 pamda.safeDivide(2,10) #=> 5 1549 pamda.safeDivide(0,10) #=> 10 1550 ``` 1551 """ 1552 return a / denominator if denominator != 0 else a 1553 1554 def safeDivideDefault( 1555 self, 1556 default_denominator: int | float, 1557 denominator: int | float, 1558 a: int | float, 1559 ): 1560 """ 1561 Function: 1562 1563 - Forces division to work by enforcing a non zero default denominator if the provided denominator is zero 1564 1565 Requires: 1566 1567 - `default_denominator`: 1568 - Type: int | float 1569 - What: A non zero denominator to use if denominator is zero 1570 - Default: 1 1571 - `denominator`: 1572 - Type: int | float 1573 - What: The denominator 1574 - `a`: 1575 - Type: int | float 1576 - What: The numerator 1577 1578 Example: 1579 1580 ``` 1581 pamda.safeDivideDefault(2,5,10) #=> 2 1582 pamda.safeDivideDefault(2,0,10) #=> 5 1583 ``` 1584 """ 1585 if default_denominator == 0: 1586 raise Exception( 1587 "`safeDivideDefault` `default_denominator` can not be 0" 1588 ) 1589 return a / denominator if denominator != 0 else a / default_denominator 1590 1591 def symmetricDifference(self, a: list, b: list): 1592 """ 1593 Function: 1594 1595 - Combines two lists into a list of no duplicates items present in one list but not the other 1596 1597 Requires: 1598 1599 - `a`: 1600 - Type: list 1601 - What: List of items in which to look for a difference 1602 - `b`: 1603 - Type: list 1604 - What: List of items in which to look for a difference 1605 1606 Example: 1607 1608 ``` 1609 a=['a','b'] 1610 b=['b','c'] 1611 pamda.symmetricDifference(a=a, b=b) #=> ['a','c'] 1612 ``` 1613 """ 1614 return list(set(a).difference(set(b))) + list(set(b).difference(set(a))) 1615 1616 def tail(self, data: list | str): 1617 """ 1618 Function: 1619 1620 - Picks the last item out of a list or string 1621 1622 Requires: 1623 1624 - `data`: 1625 - Type: list | str 1626 - What: A list or string 1627 1628 Example: 1629 1630 ``` 1631 data=['fe','fi','fo','fum'] 1632 pamda.tail( 1633 data=data 1634 ) #=> fum 1635 ``` 1636 """ 1637 if not len(data) > 0: 1638 raise Exception("Attempting to call `tail` on an empty list or str") 1639 return data[-1] 1640 1641 def thunkify(self, fn): 1642 """ 1643 Function: 1644 1645 - Creates a curried thunk out of a function 1646 - Evaluation of the thunk lazy and is delayed until called 1647 1648 Requires: 1649 1650 - `fn`: 1651 - Type: function | method 1652 - What: The function or method to thunkify 1653 - Note: Thunkified functions are automatically curried 1654 - Note: Class methods auto apply self during thunkify 1655 1656 Notes: 1657 1658 - Input functions are not thunkified in place 1659 - The returned function is a thunkified version of the input function 1660 - A curried function can be thunkified in place by calling fn.thunkify() 1661 1662 Examples: 1663 1664 ``` 1665 def add(a,b): 1666 return a+b 1667 1668 addThunk=pamda.thunkify(add) 1669 1670 add(1,2) #=> 3 1671 addThunk(1,2) 1672 addThunk(1,2)() #=> 3 1673 1674 x=addThunk(1,2) 1675 x() #=> 3 1676 ``` 1677 1678 ``` 1679 @pamda.curry 1680 def add(a,b): 1681 return a+b 1682 1683 add(1,2) #=> 3 1684 1685 add.thunkify() 1686 1687 add(1,2) 1688 add(1,2)() #=> 3 1689 ``` 1690 """ 1691 fn = self.curry(fn) 1692 return fn.thunkify() 1693 1694 def unnest(self, data: list): 1695 """ 1696 Function: 1697 1698 - Removes one level of depth for all items in a list 1699 1700 Requires: 1701 1702 - `data`: 1703 - Type: list 1704 - What: A list of items to unnest by one level 1705 1706 Examples: 1707 1708 ``` 1709 data=['fe','fi',['fo',['fum']]] 1710 pamda.unnest( 1711 data=data 1712 ) #=> ['fe','fi','fo',['fum']] 1713 ``` 1714 """ 1715 if not len(data) > 0: 1716 raise Exception("Attempting to call `unnest` on an empty list") 1717 output = [] 1718 for i in data: 1719 if isinstance(i, list): 1720 output += i 1721 else: 1722 output.append(i) 1723 return output 1724 1725 def zip(self, a: list, b: list): 1726 """ 1727 Function: 1728 1729 - Creates a new list out of the two supplied by pairing up equally-positioned items from both lists 1730 1731 Requires: 1732 1733 - `a`: 1734 - Type: list 1735 - What: List of items to appear in new list first 1736 - `b`: 1737 - Type: list 1738 - What: List of items to appear in new list second 1739 1740 Example: 1741 1742 ``` 1743 a=['a','b'] 1744 b=[1,2] 1745 pamda.zip(a=a, b=b) #=> [['a',1],['b',2]] 1746 ``` 1747 """ 1748 return list(map(list, zip(a, b))) 1749 1750 def zipObj(self, a: list, b: list): 1751 """ 1752 Function: 1753 1754 - Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists 1755 - The first list represents keys and the second values 1756 1757 Requires: 1758 1759 - `a`: 1760 - Type: list 1761 - What: List of items to appear in new list first 1762 - `b`: 1763 - Type: list 1764 - What: List of items to appear in new list second 1765 1766 Example: 1767 1768 ``` 1769 a=['a','b'] 1770 b=[1,2] 1771 pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2} 1772 ``` 1773 """ 1774 return dict(zip(a, b))
20 def accumulate(self, fn, initial_accumulator, data: list): 21 """ 22 Function: 23 24 - Returns an accumulated list of items by iterating a function starting with an accumulator over a list 25 26 Requires: 27 28 - `fn`: 29 - Type: function | method 30 - What: The function or method to reduce 31 - Note: This function should have an arity of 2 (take two inputs) 32 - Note: The first input should take the accumulator value 33 - Note: The second input should take the data value 34 -`initial_accumulator`: 35 - Type: any 36 - What: The initial item to pass into the function when starting the accumulation process 37 - `data`: 38 - Type: list 39 - What: The list of items to iterate over 40 41 Example: 42 43 ``` 44 data=[1,2,3,4] 45 pamda.accumulate( 46 fn=pamda.add, 47 initial_accumulator=0, 48 data=data 49 ) 50 #=> [1,3,6,10] 51 52 ``` 53 """ 54 fn = self.curry(fn) 55 if fn.__arity__ != 2: 56 raise Exception("`fn` must have an arity of 2 (take two inputs)") 57 if not len(data) > 0: 58 raise Exception( 59 "`data` has a length of 0, however it must have a length of at least 1" 60 ) 61 acc = initial_accumulator 62 out = [] 63 for i in data: 64 acc = fn(acc, i) 65 out.append(acc) 66 return out
Function:
- Returns an accumulated list of items by iterating a function starting with an accumulator over a list
Requires:
fn
:- Type: function | method
- What: The function or method to reduce
- Note: This function should have an arity of 2 (take two inputs)
- Note: The first input should take the accumulator value
- Note: The second input should take the data value
-
initial_accumulator
: - Type: any
- What: The initial item to pass into the function when starting the accumulation process
data
:- Type: list
- What: The list of items to iterate over
Example:
data=[1,2,3,4]
pamda.accumulate(
fn=pamda.add,
initial_accumulator=0,
data=data
)
#=> [1,3,6,10]
68 def add(self, a: int | float, b: int | float): 69 """ 70 Function: 71 72 - Adds two numbers 73 74 Requires: 75 76 - `a`: 77 - Type: int | float 78 - What: The first number to add 79 - `b`: 80 - Type: int | float 81 - What: The second number to add 82 83 Example: 84 85 ``` 86 pamda.add(1, 2) #=> 3 87 ``` 88 """ 89 return a + b
Function:
- Adds two numbers
Requires:
a
:- Type: int | float
- What: The first number to add
b
:- Type: int | float
- What: The second number to add
Example:
pamda.add(1, 2) #=> 3
91 def adjust(self, index: int, fn, data: list): 92 """ 93 Function: 94 95 - Adjusts an item in a list by applying a function to it 96 97 Requires: 98 99 - `index`: 100 - Type: int 101 - What: The 0 based index of the item in the list to adjust 102 - Note: Indicies are accepted 103 - Note: If the index is out of range, picks the (-)first / (+)last item 104 - `fn`: 105 - Type: function | method 106 - What: The function to apply the index item to 107 - Note: This is automatically curried 108 - `data`: 109 - Type: list 110 - What: The list to adjust 111 112 Example: 113 114 ``` 115 data=[1,5,9] 116 pamda.adjust( 117 index=1, 118 fn=pamda.inc, 119 data=data 120 ) #=> [1,6,9] 121 ``` 122 """ 123 fn = self.curry(fn) 124 index = self.clamp(-len(data), len(data) - 1, index) 125 data[index] = fn(data[index]) 126 return data
Function:
- Adjusts an item in a list by applying a function to it
Requires:
index
:- Type: int
- What: The 0 based index of the item in the list to adjust
- Note: Indicies are accepted
- Note: If the index is out of range, picks the (-)first / (+)last item
fn
:- Type: function | method
- What: The function to apply the index item to
- Note: This is automatically curried
data
:- Type: list
- What: The list to adjust
Example:
data=[1,5,9]
pamda.adjust(
index=1,
fn=pamda.inc,
data=data
) #=> [1,6,9]
128 def assocPath(self, path: list | str | int | tuple, value, data: dict): 129 """ 130 Function: 131 132 - Ensures a path exists within a nested dictionary 133 - Note: This updates the object in place, but also returns the object 134 135 Requires: 136 137 - `path`: 138 - Type: list[str | int | tuple] | str | int | tuple 139 - What: The path to check 140 - Note: If a string is passed, assumes a single item path list with that string 141 - `value`: 142 - Type: any 143 - What: The value to appropriate to the end of the path 144 - `data`: 145 - Type: dict 146 - What: A dictionary in which to associate the given value to the given path 147 148 Example: 149 150 ``` 151 data={'a':{'b':1}} 152 pamda.assocPath(path=['a','c'], value=3, data=data) #=> {'a':{'b':1, 'c':3}} 153 ``` 154 """ 155 if not isinstance(path, list): 156 path = [path] 157 reduce(__getForceDict__, path[:-1], data).__setitem__(path[-1], value) 158 return data
Function:
- Ensures a path exists within a nested dictionary
- Note: This updates the object in place, but also returns the object
Requires:
path
:- Type: list[str | int | tuple] | str | int | tuple
- What: The path to check
- Note: If a string is passed, assumes a single item path list with that string
value
:- Type: any
- What: The value to appropriate to the end of the path
data
:- Type: dict
- What: A dictionary in which to associate the given value to the given path
Example:
data={'a':{'b':1}}
pamda.assocPath(path=['a','c'], value=3, data=data) #=> {'a':{'b':1, 'c':3}}
160 def assocPathComplex( 161 self, default, default_fn, path: list | int | float | tuple, data: dict 162 ): 163 """ 164 Function: 165 166 - Ensures a path exists within a nested dictionary 167 - Note: This updates the object in place, but also returns the object 168 169 Requires: 170 171 - `default`: 172 - Type: any 173 - What: The default item to add to a path that does not yet exist 174 - `default_fn`: 175 - Type: function | method 176 - What: A unary (single input) function that takes in the current path item (or default) and adjusts it 177 - Example: `lambda x: x` # Returns the value in the dict or the default value if none was present 178 - `path`: 179 - Type: list[str | int | tuple] | str | int | tuple 180 - What: The path to check 181 - `data`: 182 - Type: dict 183 - What: A dictionary to check if the path exists 184 185 Example: 186 187 ``` 188 data={'a':{'b':1}} 189 pamda.assocPathComplex(default=[2], default_fn=lambda x:x+[1], path=['a','c'], data=data) #=> {'a':{'b':1,'c':[2,1]}} 190 ``` 191 """ 192 if self.getArity(default_fn) != 1: 193 raise Exception( 194 "`assocPathComplex` `default_fn` must be an unary (single input) function." 195 ) 196 if not isinstance(path, list): 197 path = [path] 198 path_object = reduce(__getForceDict__, path[:-1], data) 199 path_object.__setitem__( 200 path[-1], default_fn(path_object.get(path[-1], default)) 201 ) 202 return data
Function:
- Ensures a path exists within a nested dictionary
- Note: This updates the object in place, but also returns the object
Requires:
default
:- Type: any
- What: The default item to add to a path that does not yet exist
default_fn
:- Type: function | method
- What: A unary (single input) function that takes in the current path item (or default) and adjusts it
- Example:
lambda x: x
# Returns the value in the dict or the default value if none was present
path
:- Type: list[str | int | tuple] | str | int | tuple
- What: The path to check
data
:- Type: dict
- What: A dictionary to check if the path exists
Example:
data={'a':{'b':1}}
pamda.assocPathComplex(default=[2], default_fn=lambda x:x+[1], path=['a','c'], data=data) #=> {'a':{'b':1,'c':[2,1]}}
204 def asyncKill(self, fn: curry_obj): 205 """ 206 Function: 207 208 - Kills an asynchronous function that is currently running 209 - Returns: 210 - `None` if the function has not yet finished running 211 - The result of the function if it has finished running 212 213 Requires: 214 215 - `fn`: 216 - Type: thunkified function | thunkified method 217 - What: The function or method to run asychronously 218 - Note: The supplied `fn` must already be asynchronously running 219 220 Notes: 221 222 - See also `asyncRun` and `asyncWait` 223 - A thunkified function currently running asynchronously can call `asyncKill` on itself 224 - If a function has already finished running, calling `asyncKill` on it will have no effect 225 - `asyncKill` does not kill threads that are sleeping (EG: `time.sleep`), but will kill the thread once the sleep is finished 226 227 Example: 228 229 ``` 230 import time 231 from pamda import pamda 232 233 @pamda.thunkify 234 def test(name, wait): 235 waited = 0 236 while waited < wait: 237 time.sleep(1) 238 waited += 1 239 print(f'{name} has waited {waited} seconds') 240 241 async_test = pamda.asyncRun(test('a',3)) 242 time.sleep(1) 243 pamda.asyncKill(async_test) 244 # Alternatively: 245 # async_test.asyncKill() 246 ``` 247 """ 248 return fn.asyncKill()
Function:
- Kills an asynchronous function that is currently running
- Returns:
None
if the function has not yet finished running- The result of the function if it has finished running
Requires:
fn
:- Type: thunkified function | thunkified method
- What: The function or method to run asychronously
- Note: The supplied
fn
must already be asynchronously running
Notes:
- See also
asyncRun
andasyncWait
- A thunkified function currently running asynchronously can call
asyncKill
on itself - If a function has already finished running, calling
asyncKill
on it will have no effect asyncKill
does not kill threads that are sleeping (EG:time.sleep
), but will kill the thread once the sleep is finished
Example:
import time
from pamda import pamda
@pamda.thunkify
def test(name, wait):
waited = 0
while waited < wait:
time.sleep(1)
waited += 1
print(f'{name} has waited {waited} seconds')
async_test = pamda.asyncRun(test('a',3))
time.sleep(1)
pamda.asyncKill(async_test)
# Alternatively:
# async_test.asyncKill()
250 def asyncRun(self, fn: curry_obj): 251 """ 252 Function: 253 254 - Runs the supplied function asychronously 255 256 Requires: 257 258 - `fn`: 259 - Type: thunkified function | thunkified method 260 - What: The function or method to run asychronously 261 - Note: The supplied `fn` must have an arity of 0 262 263 Notes: 264 265 - To pass inputs to a function in asyncRun, first thunkify the function and pass all arguments before calling `asyncRun` on it 266 - To get the results of an `asyncRun` call `asyncWait` 267 - To kill an `asyncRun` mid process call `asyncKill` 268 - A thunkified function with arity of 0 can call `asyncRun` on itself 269 270 Examples: 271 272 Input: 273 ``` 274 import time 275 276 @pamda.thunkify 277 def test(name, wait): 278 print(f'{name} start') 279 time.sleep(wait) 280 print(f'{name} end') 281 282 async_test = pamda.asyncRun(test('a',2)) 283 sync_test = test('b',1)() 284 ``` 285 Output: 286 ``` 287 a start 288 b start 289 b end 290 a end 291 ``` 292 293 294 Input: 295 ``` 296 import time 297 298 @pamda.thunkify 299 def test(name, wait): 300 time.sleep(wait) 301 return f"{name}: {wait}" 302 303 async_test = pamda.asyncRun(test('a',2)) 304 print(async_test.asyncWait()) #=> a: 2 305 ``` 306 307 308 Input: 309 ``` 310 import time 311 312 @pamda.thunkify 313 def test(name, wait): 314 time.sleep(wait) 315 return f"{name}: {wait}" 316 317 async_test = test('a',2).asyncRun() 318 print(async_test.asyncWait()) #=> a: 2 319 ``` 320 """ 321 return fn.asyncRun()
Function:
- Runs the supplied function asychronously
Requires:
fn
:- Type: thunkified function | thunkified method
- What: The function or method to run asychronously
- Note: The supplied
fn
must have an arity of 0
Notes:
- To pass inputs to a function in asyncRun, first thunkify the function and pass all arguments before calling
asyncRun
on it - To get the results of an
asyncRun
callasyncWait
- To kill an
asyncRun
mid process callasyncKill
- A thunkified function with arity of 0 can call
asyncRun
on itself
Examples:
Input:
import time
@pamda.thunkify
def test(name, wait):
print(f'{name} start')
time.sleep(wait)
print(f'{name} end')
async_test = pamda.asyncRun(test('a',2))
sync_test = test('b',1)()
Output:
a start
b start
b end
a end
Input:
import time
@pamda.thunkify
def test(name, wait):
time.sleep(wait)
return f"{name}: {wait}"
async_test = pamda.asyncRun(test('a',2))
print(async_test.asyncWait()) #=> a: 2
Input:
import time
@pamda.thunkify
def test(name, wait):
time.sleep(wait)
return f"{name}: {wait}"
async_test = test('a',2).asyncRun()
print(async_test.asyncWait()) #=> a: 2
323 def asyncWait(self, fn: curry_obj): 324 """ 325 Function: 326 327 - Waits for a supplied function (if needed) and returns the results 328 329 Requires: 330 331 - `fn`: 332 - Type: function | method 333 - What: The function or method for which to wait 334 - Note: The supplied `fn` must have previously called `asyncRun` 335 336 Notes: 337 338 - A thunkified function that has called `asyncRun` can call `asyncWait` on itself 339 340 Examples: 341 342 ``` 343 import time 344 345 @pamda.thunkify 346 def test(name, wait): 347 time.sleep(wait) 348 return f"{name}: {wait}" 349 350 async_test = pamda.asyncRun(test('a',2)) 351 print(pamda.asyncWait(async_test)) #=> a: 2 352 ``` 353 354 355 ``` 356 import time 357 358 @pamda.thunkify 359 def test(name, wait): 360 time.sleep(wait) 361 return f"{name}: {wait}" 362 363 async_test = pamda.asyncRun(test('a',2)) 364 print(async_test.asyncWait()) #=> a: 2 365 ``` 366 """ 367 return fn.asyncWait()
Function:
- Waits for a supplied function (if needed) and returns the results
Requires:
fn
:- Type: function | method
- What: The function or method for which to wait
- Note: The supplied
fn
must have previously calledasyncRun
Notes:
Examples:
import time
@pamda.thunkify
def test(name, wait):
time.sleep(wait)
return f"{name}: {wait}"
async_test = pamda.asyncRun(test('a',2))
print(pamda.asyncWait(async_test)) #=> a: 2
import time
@pamda.thunkify
def test(name, wait):
time.sleep(wait)
return f"{name}: {wait}"
async_test = pamda.asyncRun(test('a',2))
print(async_test.asyncWait()) #=> a: 2
369 def clamp(self, minimum: int | float, maximum: int | float, a: int | float): 370 """ 371 Function: 372 373 - Forces data to be within minimum and maximum 374 375 Requires: 376 377 - `minimum`: 378 - Type: int | float 379 - What: The minimum number 380 - `maximum`: 381 - Type: int | float 382 - What: The maximum number 383 - `a`: 384 - Type: int | float 385 - What: The number to clamp 386 387 Example: 388 389 ``` 390 pamda.clamp(1, 3, 2) #=> 2 391 pamda.clamp(1, 3, 5) #=> 3 392 ``` 393 """ 394 return min(max(a, minimum), maximum)
Function:
- Forces data to be within minimum and maximum
Requires:
minimum
:- Type: int | float
- What: The minimum number
maximum
:- Type: int | float
- What: The maximum number
a
:- Type: int | float
- What: The number to clamp
Example:
pamda.clamp(1, 3, 2) #=> 2
pamda.clamp(1, 3, 5) #=> 3
396 def curry(self, fn): 397 """ 398 Function: 399 400 - Curries a function such that inputs can be added interatively 401 402 Requires: 403 404 - `fn`: 405 - Type: function | method 406 - What: The function or method to curry 407 - Note: Class methods auto apply self during curry 408 409 Notes: 410 411 - Once curried, the function | method becomes a curry_obj object 412 - The initial function is only called once all inputs are passed 413 414 415 Examples: 416 417 ``` 418 curriedZip=pamda.curry(pamda.zip) 419 curriedZip(['a','b'])([1,2]) #=> [['a',1],['b',2]] 420 421 # Curried functions can be thunkified at any time 422 # See also thunkify 423 zipThunk=curriedZip.thunkify()(['a','b'])([1,2]) 424 zipThunk() #=> [['a',1],['b',2]] 425 ``` 426 427 ``` 428 def myFunction(a,b,c): 429 return [a,b,c] 430 431 curriedMyFn=pamda.curry(myFunction) 432 433 curriedMyFn(1,2,3) #=> [1,2,3] 434 curriedMyFn(1)(2,3) #=> [1,2,3] 435 436 x=curriedMyFn(1)(2) 437 x(3) #=> [1,2,3] 438 x(4) #=> [1,2,4] 439 440 441 ``` 442 """ 443 if fn.__dict__.get("__isCurried__"): 444 return fn() 445 return curry_obj(fn)
Function:
- Curries a function such that inputs can be added interatively
Requires:
fn
:- Type: function | method
- What: The function or method to curry
- Note: Class methods auto apply self during curry
Notes:
- Once curried, the function | method becomes a curry_obj object
- The initial function is only called once all inputs are passed
Examples:
curriedZip=pamda.curry(pamda.zip)
curriedZip(['a','b'])([1,2]) #=> [['a',1],['b',2]]
# Curried functions can be thunkified at any time
# See also thunkify
zipThunk=curriedZip.thunkify()(['a','b'])([1,2])
zipThunk() #=> [['a',1],['b',2]]
def myFunction(a,b,c):
return [a,b,c]
curriedMyFn=pamda.curry(myFunction)
curriedMyFn(1,2,3) #=> [1,2,3]
curriedMyFn(1)(2,3) #=> [1,2,3]
x=curriedMyFn(1)(2)
x(3) #=> [1,2,3]
x(4) #=> [1,2,4]
447 def curryTyped(self, fn): 448 """ 449 Function: 450 451 - Curries a function such that inputs can be added interatively and function annotations are type checked at runtime 452 453 Requires: 454 455 - `fn`: 456 - Type: function | method 457 - What: The function or method to curry 458 - Note: Class methods auto apply self during curry 459 460 Notes: 461 462 - Once curried, the function | method becomes a curry_obj object 463 - The initial function is only called once all inputs are passed 464 465 466 Examples: 467 468 ``` 469 @pamda.curryTyped 470 def add(a:int,b:int): 471 return a+b 472 473 add(1)(1) #=> 2 474 add(1)(1.5) #=> Raises type exception 475 ``` 476 """ 477 if fn.__dict__.get("__isCurried__"): 478 return fn().typeEnforce() 479 return curry_obj(fn).typeEnforce()
Function:
- Curries a function such that inputs can be added interatively and function annotations are type checked at runtime
Requires:
fn
:- Type: function | method
- What: The function or method to curry
- Note: Class methods auto apply self during curry
Notes:
- Once curried, the function | method becomes a curry_obj object
- The initial function is only called once all inputs are passed
Examples:
@pamda.curryTyped
def add(a:int,b:int):
return a+b
add(1)(1) #=> 2
add(1)(1.5) #=> Raises type exception
481 def dec(self, a: int | float): 482 """ 483 Function: 484 485 - Decrements a number by one 486 487 Requires: 488 489 - `a`: 490 - Type: int | float 491 - What: The number to decrement 492 493 Example: 494 495 ``` 496 pamda.dec(42) #=> 41 497 ``` 498 """ 499 if not isinstance(a, (int, float)): 500 raise Exception("`a` must be an `int` or a `float`") 501 return a - 1
Function:
- Decrements a number by one
Requires:
a
:- Type: int | float
- What: The number to decrement
Example:
pamda.dec(42) #=> 41
503 def difference(self, a: list, b: list): 504 """ 505 Function: 506 507 - Combines two lists into a list of no duplicate items present in the first list but not the second 508 509 Requires: 510 511 - `a`: 512 - Type: list 513 - What: List of items in which to look for a difference 514 - `b`: 515 - Type: list 516 - What: List of items in which to compare when looking for the difference 517 518 Example: 519 520 ``` 521 a=['a','b'] 522 b=['b','c'] 523 pamda.difference(a=a, b=b) #=> ['a'] 524 pamda.difference(a=b, b=a) #=> ['c'] 525 ``` 526 """ 527 return list(set(a).difference(set(b)))
Function:
- Combines two lists into a list of no duplicate items present in the first list but not the second
Requires:
a
:- Type: list
- What: List of items in which to look for a difference
b
:- Type: list
- What: List of items in which to compare when looking for the difference
Example:
a=['a','b']
b=['b','c']
pamda.difference(a=a, b=b) #=> ['a']
pamda.difference(a=b, b=a) #=> ['c']
529 def dissocPath(self, path: list | str | int | tuple, data: dict): 530 """ 531 Function: 532 533 - Removes the value at the end of a path within a nested dictionary 534 - Note: This updates the object in place, but also returns the object 535 536 Requires: 537 538 - `path`: 539 - Type: list of strs | str 540 - What: The path to remove from the dictionary 541 - Note: If a string is passed, assumes a single item path list with that string 542 - `data`: 543 - Type: dict 544 - What: A dictionary with a path to be removed 545 546 Example: 547 548 ``` 549 data={'a':{'b':{'c':0,'d':1}}} 550 pamda.dissocPath(path=['a','b','c'], data=data) #=> {'a':{'b':{'d':1}}} 551 ``` 552 """ 553 if not isinstance(path, list): 554 path = [path] 555 if not self.hasPath(path=path, data=data): 556 raise Exception("Path does not exist") 557 else: 558 reduce(__getForceDict__, path[:-1], data).pop(path[-1]) 559 return data
Function:
- Removes the value at the end of a path within a nested dictionary
- Note: This updates the object in place, but also returns the object
Requires:
path
:- Type: list of strs | str
- What: The path to remove from the dictionary
- Note: If a string is passed, assumes a single item path list with that string
data
:- Type: dict
- What: A dictionary with a path to be removed
Example:
data={'a':{'b':{'c':0,'d':1}}}
pamda.dissocPath(path=['a','b','c'], data=data) #=> {'a':{'b':{'d':1}}}
561 def flatten(self, data: list): 562 """ 563 Function: 564 565 - Flattens a list of lists of lists ... into a single list depth first 566 567 Requires: 568 569 - `data`: 570 - Type: list of lists 571 - What: The list of lists to reduce to a single list 572 Example: 573 574 ``` 575 data=[['a','b'],[1,[2]]] 576 pamda.flatten(data=data) #=> ['a','b',1,2] 577 ``` 578 """ 579 580 def iter_flatten(data): 581 out = [] 582 for i in data: 583 if isinstance(i, list): 584 out.extend(iter_flatten(i)) 585 else: 586 out.append(i) 587 return out 588 589 return iter_flatten(data)
Function:
- Flattens a list of lists of lists ... into a single list depth first
Requires:
data
:- Type: list of lists
- What: The list of lists to reduce to a single list Example:
data=[['a','b'],[1,[2]]]
pamda.flatten(data=data) #=> ['a','b',1,2]
591 def flip(self, fn): 592 """ 593 Function: 594 595 - Returns a new function equivalent to the supplied function except that the first two inputs are flipped 596 597 Requires: 598 599 - `fn`: 600 - Type: function | method 601 - What: The function or method to flip 602 - Note: This function must have an arity of at least 2 (take two inputs) 603 - Note: Only args are flipped, kwargs are passed as normal 604 605 Notes: 606 607 - Input functions are not flipped in place 608 - The returned function is a flipped version of the input function 609 - A curried function can be flipped in place by calling fn.flip() 610 - A function can be flipped multiple times: 611 - At each flip, the first and second inputs for the function as it is currently curried are switched 612 - Flipping a function two times before adding an input will return the initial value 613 614 Examples: 615 616 ``` 617 def concat(a,b,c,d): 618 return str(a)+str(b)+str(c)+str(d) 619 620 flip_concat=pamda.flip(concat) 621 622 concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum' 623 flip_concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum' 624 ``` 625 626 ``` 627 @pamda.curry 628 def concat(a,b,c,d): 629 return str(a)+str(b)+str(c)+str(d) 630 631 concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum' 632 633 concat.flip() 634 635 concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum' 636 ``` 637 638 ``` 639 @pamda.curry 640 def concat(a,b,c,d): 641 return str(a)+str(b)+str(c)+str(d) 642 643 a=pamda.flip(concat)('fi-') 644 b=pamda.flip(a)('fo-') 645 c=pamda.flip(b)('fum') 646 c('fe-') #=> 'fe-fi-fo-fum' 647 ``` 648 649 ``` 650 def concat(a,b,c,d): 651 return str(a)+str(b)+str(c)+str(d) 652 653 a=pamda.flip(concat)('fi-').flip()('fo-').flip()('fum') 654 a('fe-') #=> 'fe-fi-fo-fum' 655 ``` 656 """ 657 fn = self.curry(fn) 658 return fn.flip()
Function:
- Returns a new function equivalent to the supplied function except that the first two inputs are flipped
Requires:
fn
:- Type: function | method
- What: The function or method to flip
- Note: This function must have an arity of at least 2 (take two inputs)
- Note: Only args are flipped, kwargs are passed as normal
Notes:
- Input functions are not flipped in place
- The returned function is a flipped version of the input function
- A curried function can be flipped in place by calling fn.flip()
- A function can be flipped multiple times:
- At each flip, the first and second inputs for the function as it is currently curried are switched
- Flipping a function two times before adding an input will return the initial value
Examples:
def concat(a,b,c,d):
return str(a)+str(b)+str(c)+str(d)
flip_concat=pamda.flip(concat)
concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum'
flip_concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum'
@pamda.curry
def concat(a,b,c,d):
return str(a)+str(b)+str(c)+str(d)
concat('fe-','fi-','fo-','fum') #=> 'fe-fi-fo-fum'
concat.flip()
concat('fe-','fi-','fo-','fum') #=> 'fi-fe-fo-fum'
@pamda.curry
def concat(a,b,c,d):
return str(a)+str(b)+str(c)+str(d)
a=pamda.flip(concat)('fi-')
b=pamda.flip(a)('fo-')
c=pamda.flip(b)('fum')
c('fe-') #=> 'fe-fi-fo-fum'
def concat(a,b,c,d):
return str(a)+str(b)+str(c)+str(d)
a=pamda.flip(concat)('fi-').flip()('fo-').flip()('fum')
a('fe-') #=> 'fe-fi-fo-fum'
660 def getArity(self, fn): 661 """ 662 Function: 663 664 - Gets the arity (number of inputs left to be specified) of a function or method (curried or uncurried) 665 666 Requires: 667 668 - `fn`: 669 - Type: function | method 670 - What: The function or method to get the arity of 671 - Note: Class methods remove one arity to account for self 672 673 Examples: 674 675 ``` 676 pamda.getArity(pamda.zip) #=> 2 677 curriedZip=pamda.curry(pamda.zip) 678 ABCuriedZip=curriedZip(['a','b']) 679 pamda.getArity(ABCuriedZip) #=> 1 680 ``` 681 """ 682 fn = self.curry(fn) 683 return fn.__arity__
Function:
- Gets the arity (number of inputs left to be specified) of a function or method (curried or uncurried)
Requires:
fn
:- Type: function | method
- What: The function or method to get the arity of
- Note: Class methods remove one arity to account for self
Examples:
pamda.getArity(pamda.zip) #=> 2
curriedZip=pamda.curry(pamda.zip)
ABCuriedZip=curriedZip(['a','b'])
pamda.getArity(ABCuriedZip) #=> 1
685 def groupBy(self, fn, data: list): 686 """ 687 Function: 688 689 - Splits a list into a dictionary of sublists keyed by the return string of a provided function 690 691 Requires: 692 693 - `fn`: 694 - Type: function | method 695 - What: The function or method to group by 696 - Note: Must return a string (or other hashable object) 697 - Note: This function must be unary (take one input) 698 - Note: This function is applied to each item in the list recursively 699 - `data`: 700 - Type: list 701 - What: List of items to apply the function to and then group by the results 702 703 Examples: 704 705 ``` 706 def getGrade(item): 707 score=item['score'] 708 if score>90: 709 return 'A' 710 elif score>80: 711 return 'B' 712 elif score>70: 713 return 'C' 714 elif score>60: 715 return 'D' 716 else: 717 return 'F' 718 719 data=[ 720 {'name':'Connor', 'score':75}, 721 {'name':'Fred', 'score':79}, 722 {'name':'Joe', 'score':84}, 723 ] 724 pamda.groupBy(getGrade,data) 725 #=>{ 726 #=> 'B':[{'name':'Joe', 'score':84}] 727 #=> 'C':[{'name':'Connor', 'score':75},{'name':'Fred', 'score':79}] 728 #=>} 729 ``` 730 """ 731 curried_fn = self.curry(fn) 732 if curried_fn.__arity__ != 1: 733 raise Exception( 734 "groupBy `fn` must only take one parameter as its input" 735 ) 736 return __groupByHashable__(fn=fn, data=data)
Function:
- Splits a list into a dictionary of sublists keyed by the return string of a provided function
Requires:
fn
:- Type: function | method
- What: The function or method to group by
- Note: Must return a string (or other hashable object)
- Note: This function must be unary (take one input)
- Note: This function is applied to each item in the list recursively
data
:- Type: list
- What: List of items to apply the function to and then group by the results
Examples:
def getGrade(item):
score=item['score']
if score>90:
return 'A'
elif score>80:
return 'B'
elif score>70:
return 'C'
elif score>60:
return 'D'
else:
return 'F'
data=[
{'name':'Connor', 'score':75},
{'name':'Fred', 'score':79},
{'name':'Joe', 'score':84},
]
pamda.groupBy(getGrade,data)
#=>{
#=> 'B':[{'name':'Joe', 'score':84}]
#=> 'C':[{'name':'Connor', 'score':75},{'name':'Fred', 'score':79}]
#=>}
738 def groupKeys(self, keys: list, data: list): 739 """ 740 Function: 741 742 - Splits a list of dicts into a list of sublists of dicts separated by values with equal keys 743 744 Requires: 745 746 - `keys`: 747 - Type: list of strs 748 - What: The keys to group by 749 - `data`: 750 - Type: list of dicts 751 - What: List of dictionaries with which to match keys 752 753 Examples: 754 755 ``` 756 data=[ 757 {'color':'red', 'size':9, 'shape':'ball'}, 758 {'color':'red', 'size':10, 'shape':'ball'}, 759 {'color':'green', 'size':11, 'shape':'ball'}, 760 {'color':'green', 'size':12, 'shape':'square'} 761 ] 762 pamda.groupKeys(['color','shape'],data) 763 #=> [ 764 #=> [{'color': 'red', 'size': 9, 'shape': 'ball'}, {'color': 'red', 'size': 10, 'shape': 'ball'}], 765 #=> [{'color': 'green', 'size': 11, 'shape': 'ball'}], 766 #=> [{'color': 'green', 'size': 12, 'shape': 'square'}] 767 #=> ] 768 ``` 769 """ 770 771 def key_fn(item): 772 return tuple([item[key] for key in keys]) 773 774 return list(__groupByHashable__(key_fn, data).values())
Function:
- Splits a list of dicts into a list of sublists of dicts separated by values with equal keys
Requires:
keys
:- Type: list of strs
- What: The keys to group by
data
:- Type: list of dicts
- What: List of dictionaries with which to match keys
Examples:
data=[
{'color':'red', 'size':9, 'shape':'ball'},
{'color':'red', 'size':10, 'shape':'ball'},
{'color':'green', 'size':11, 'shape':'ball'},
{'color':'green', 'size':12, 'shape':'square'}
]
pamda.groupKeys(['color','shape'],data)
#=> [
#=> [{'color': 'red', 'size': 9, 'shape': 'ball'}, {'color': 'red', 'size': 10, 'shape': 'ball'}],
#=> [{'color': 'green', 'size': 11, 'shape': 'ball'}],
#=> [{'color': 'green', 'size': 12, 'shape': 'square'}]
#=> ]
776 def groupWith(self, fn, data: list): 777 """ 778 Function: 779 780 - Splits a list into a list of sublists where each sublist is determined by adjacent pairwise comparisons from a provided function 781 782 Requires: 783 784 - `fn`: 785 - Type: function | method 786 - What: The function or method to groub with 787 - Note: Must return a boolean value 788 - Note: This function must have an arity of two (take two inputs) 789 - Note: This function is applied to each item plus the next adjacent item in the list recursively 790 - `data`: 791 - Type: list 792 - What: List of items to apply the function to and then group the results 793 794 Examples: 795 796 ``` 797 def areEqual(a,b): 798 return a==b 799 800 data=[1,2,3,1,1,2,2,3,3,3] 801 pamda.groupWith(areEqual,data) #=> [[1], [2], [3], [1, 1], [2, 2], [3, 3, 3]] 802 ``` 803 """ 804 curried_fn = self.curry(fn) 805 if curried_fn.__arity__ != 2: 806 raise Exception("groupWith `fn` must take exactly two parameters") 807 previous = data[0] 808 output = [] 809 sublist = [previous] 810 for i in data[1:]: 811 if fn(i, previous): 812 sublist.append(i) 813 else: 814 output.append(sublist) 815 sublist = [i] 816 previous = i 817 output.append(sublist) 818 return output
Function:
- Splits a list into a list of sublists where each sublist is determined by adjacent pairwise comparisons from a provided function
Requires:
fn
:- Type: function | method
- What: The function or method to groub with
- Note: Must return a boolean value
- Note: This function must have an arity of two (take two inputs)
- Note: This function is applied to each item plus the next adjacent item in the list recursively
data
:- Type: list
- What: List of items to apply the function to and then group the results
Examples:
def areEqual(a,b):
return a==b
data=[1,2,3,1,1,2,2,3,3,3]
pamda.groupWith(areEqual,data) #=> [[1], [2], [3], [1, 1], [2, 2], [3, 3, 3]]
820 def hasPath(self, path: list | str, data: dict): 821 """ 822 Function: 823 824 - Checks if a path exists within a nested dictionary 825 826 Requires: 827 828 - `path`: 829 - Type: list of strs | str 830 - What: The path to check 831 - Note: If a string is passed, assumes a single item path list with that string 832 - `data`: 833 - Type: dict 834 - What: A dictionary to check if the path exists 835 836 Example: 837 838 ``` 839 data={'a':{'b':1}} 840 pamda.hasPath(path=['a','b'], data=data) #=> True 841 pamda.hasPath(path=['a','d'], data=data) #=> False 842 ``` 843 """ 844 if isinstance(path, str): 845 path = [path] 846 return path[-1] in reduce(lambda x, y: x.get(y, {}), path[:-1], data)
Function:
- Checks if a path exists within a nested dictionary
Requires:
path
:- Type: list of strs | str
- What: The path to check
- Note: If a string is passed, assumes a single item path list with that string
data
:- Type: dict
- What: A dictionary to check if the path exists
Example:
data={'a':{'b':1}}
pamda.hasPath(path=['a','b'], data=data) #=> True
pamda.hasPath(path=['a','d'], data=data) #=> False
848 def hardRound(self, decimal_places: int, a: int | float): 849 """ 850 Function: 851 852 - Rounds to a set number of decimal places regardless of floating point math in python 853 854 Requires: 855 856 - `decimal_places`: 857 - Type: int 858 - What: The number of decimal places to round to 859 - Default: 0 860 - Notes: Negative numbers accepted (EG -1 rounds to the nearest 10) 861 - `a`: 862 - Type: int | float 863 - What: The number to round 864 865 Example: 866 867 ``` 868 a=12.345 869 pamda.hardRound(1,a) #=> 12.3 870 pamda.hardRound(-1,a) #=> 10 871 ``` 872 """ 873 return int(a * (10**decimal_places) + 0.5) / (10**decimal_places)
Function:
- Rounds to a set number of decimal places regardless of floating point math in python
Requires:
decimal_places
:- Type: int
- What: The number of decimal places to round to
- Default: 0
- Notes: Negative numbers accepted (EG -1 rounds to the nearest 10)
a
:- Type: int | float
- What: The number to round
Example:
a=12.345
pamda.hardRound(1,a) #=> 12.3
pamda.hardRound(-1,a) #=> 10
875 def head(self, data: list | str): 876 """ 877 Function: 878 879 - Picks the first item out of a list or string 880 881 Requires: 882 883 - `data`: 884 - Type: list | str 885 - What: A list or string 886 887 Example: 888 889 ``` 890 data=['fe','fi','fo','fum'] 891 pamda.first( 892 data=data 893 ) #=> fe 894 ``` 895 """ 896 if not isinstance(data, (list, str)): 897 raise Exception("`head` can only be called on a `str` or a `list`") 898 if not len(data) > 0: 899 raise Exception("Attempting to call `head` on an empty list or str") 900 return data[0]
Function:
- Picks the first item out of a list or string
Requires:
data
:- Type: list | str
- What: A list or string
Example:
data=['fe','fi','fo','fum']
pamda.first(
data=data
) #=> fe
902 def inc(self, a: int | float): 903 """ 904 Function: 905 906 - Increments a number by one 907 908 Requires: 909 910 - `a`: 911 - Type: int | float 912 - What: The number to increment 913 914 Example: 915 916 ``` 917 pamda.inc(42) #=> 43 918 ``` 919 """ 920 if not isinstance(a, (int, float)): 921 raise Exception("`a` must be an `int` or a `float`") 922 return a + 1
Function:
- Increments a number by one
Requires:
a
:- Type: int | float
- What: The number to increment
Example:
pamda.inc(42) #=> 43
924 def intersection(self, a: list, b: list): 925 """ 926 Function: 927 928 - Combines two lists into a list of no duplicates composed of those elements common to both lists 929 930 Requires: 931 932 - `a`: 933 - Type: list 934 - What: List of items in which to look for an intersection 935 - `b`: 936 - Type: list 937 - What: List of items in which to look for an intersection 938 939 Example: 940 941 ``` 942 a=['a','b'] 943 b=['b','c'] 944 pamda.intersection(a=a, b=b) #=> ['b'] 945 ``` 946 """ 947 return list(set(a).intersection(set(b)))
Function:
- Combines two lists into a list of no duplicates composed of those elements common to both lists
Requires:
a
:- Type: list
- What: List of items in which to look for an intersection
b
:- Type: list
- What: List of items in which to look for an intersection
Example:
a=['a','b']
b=['b','c']
pamda.intersection(a=a, b=b) #=> ['b']
949 def map(self, fn, data: list | dict): 950 """ 951 Function: 952 953 - Maps a function over a list or a dictionary 954 955 Requires: 956 957 - `fn`: 958 - Type: function | method 959 - What: The function or method to map over the list or dictionary 960 - Note: This function should have an arity of 1 961 - `data`: 962 - Type: list | dict 963 - What: The list or dict of items to map the function over 964 965 Examples: 966 967 ``` 968 data=[1,2,3] 969 pamda.map( 970 fn=pamda.inc, 971 data=data 972 ) 973 #=> [2,3,4] 974 ``` 975 976 ``` 977 data={'a':1,'b':2,'c':3} 978 pamda.map( 979 fn=pamda.inc, 980 data=data 981 ) 982 #=> {'a':2,'b':3,'c':4} 983 ``` 984 985 """ 986 # TODO: Check for efficiency gains 987 fn = self.curry(fn) 988 if fn.__arity__ != 1: 989 raise Exception("`map` `fn` must be unary (take one input)") 990 if not len(data) > 0: 991 raise Exception( 992 "`map` `data` has a length of 0 or is an empty dictionary, however it must have at least one element in it" 993 ) 994 if isinstance(data, dict): 995 return {key: fn(value) for key, value in data.items()} 996 else: 997 return [fn(i) for i in data]
Function:
- Maps a function over a list or a dictionary
Requires:
fn
:- Type: function | method
- What: The function or method to map over the list or dictionary
- Note: This function should have an arity of 1
data
:- Type: list | dict
- What: The list or dict of items to map the function over
Examples:
data=[1,2,3]
pamda.map(
fn=pamda.inc,
data=data
)
#=> [2,3,4]
data={'a':1,'b':2,'c':3}
pamda.map(
fn=pamda.inc,
data=data
)
#=> {'a':2,'b':3,'c':4}
999 def mean(self, data: list): 1000 """ 1001 Function: 1002 1003 - Calculates the mean of a given list 1004 1005 Requires: 1006 1007 - `data`: 1008 - Type: list of (floats | ints) 1009 - What: The list with wich to calculate the mean 1010 - Note: If the length of this list is 0, returns None 1011 1012 Example: 1013 1014 ``` 1015 data=[1,2,3] 1016 pamda.mean(data=data) 1017 #=> 2 1018 ``` 1019 1020 ``` 1021 data=[] 1022 pamda.mean(data=data) 1023 #=> None 1024 ``` 1025 """ 1026 if len(data) == 0: 1027 return None 1028 return sum(data) / len(data)
Function:
- Calculates the mean of a given list
Requires:
data
:- Type: list of (floats | ints)
- What: The list with wich to calculate the mean
- Note: If the length of this list is 0, returns None
Example:
data=[1,2,3]
pamda.mean(data=data)
#=> 2
data=[]
pamda.mean(data=data)
#=> None
1030 def median(self, data: list): 1031 """ 1032 Function: 1033 1034 - Calculates the median of a given list 1035 - If the length of the list is even, calculates the mean of the two central values 1036 1037 Requires: 1038 1039 - `data`: 1040 - Type: list of (floats | ints) 1041 - What: The list with wich to calculate the mean 1042 - Note: If the length of this list is 0, returns None 1043 1044 Examples: 1045 1046 ``` 1047 data=[7,2,8,9] 1048 pamda.median(data=data) 1049 #=> 7.5 1050 ``` 1051 1052 ``` 1053 data=[7,8,9] 1054 pamda.median(data=data) 1055 #=> 8 1056 ``` 1057 1058 ``` 1059 data=[] 1060 pamda.median(data=data) 1061 #=> None 1062 ``` 1063 """ 1064 if not isinstance(data, (list)): 1065 raise Exception("`median` `data` must be a list") 1066 length = len(data) 1067 if length == 0: 1068 return None 1069 data = sorted(data) 1070 if length % 2 == 0: 1071 return (data[int(length / 2)] + data[int(length / 2) - 1]) / 2 1072 return data[int(length / 2)]
Function:
- Calculates the median of a given list
- If the length of the list is even, calculates the mean of the two central values
Requires:
data
:- Type: list of (floats | ints)
- What: The list with wich to calculate the mean
- Note: If the length of this list is 0, returns None
Examples:
data=[7,2,8,9]
pamda.median(data=data)
#=> 7.5
data=[7,8,9]
pamda.median(data=data)
#=> 8
data=[]
pamda.median(data=data)
#=> None
1074 def mergeDeep(self, update_data, data): 1075 """ 1076 Function: 1077 1078 - Recursively merges two nested dictionaries keeping all keys at each layer 1079 - Values from `update_data` are used when keys are present in both dictionaries 1080 1081 Requires: 1082 1083 - `update_data`: 1084 - Type: any 1085 - What: The new data that will take precedence during merging 1086 - `data`: 1087 - Type: any 1088 - What: The original data that will be merged into 1089 1090 Example: 1091 1092 ``` 1093 data={'a':{'b':{'c':'d'},'e':'f'}} 1094 update_data={'a':{'b':{'h':'i'},'e':'g'}} 1095 pamda.mergeDeep( 1096 update_data=update_data, 1097 data=data 1098 ) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}} 1099 ``` 1100 """ 1101 return __mergeDeep__(update_data, data)
Function:
- Recursively merges two nested dictionaries keeping all keys at each layer
- Values from
update_data
are used when keys are present in both dictionaries
Requires:
update_data
:- Type: any
- What: The new data that will take precedence during merging
data
:- Type: any
- What: The original data that will be merged into
Example:
data={'a':{'b':{'c':'d'},'e':'f'}}
update_data={'a':{'b':{'h':'i'},'e':'g'}}
pamda.mergeDeep(
update_data=update_data,
data=data
) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}}
1103 def nest(self, path_keys: list, value_key: str, data: list): 1104 """ 1105 Function: 1106 1107 - Nests a list of dictionaries into a nested dictionary 1108 - Similar items are appended to a list in the end of the nested dictionary 1109 1110 Requires: 1111 1112 - `path_keys`: 1113 - Type: list of strs 1114 - What: The variables to pull from each item in data 1115 - Note: Used to build out the nested dicitonary 1116 - Note: Order matters as the nesting occurs in order of variable 1117 - `value_key`: 1118 - Type: str 1119 - What: The variable to add to the list at the end of the nested dictionary path 1120 - `data`: 1121 - Type: list of dicts 1122 - What: A list of dictionaries to use for nesting purposes 1123 1124 Example: 1125 1126 ``` 1127 data=[ 1128 {'x_1':'a','x_2':'b', 'output':'c'}, 1129 {'x_1':'a','x_2':'b', 'output':'d'}, 1130 {'x_1':'a','x_2':'e', 'output':'f'} 1131 ] 1132 pamda.nest( 1133 path_keys=['x_1','x_2'], 1134 value_key='output', 1135 data=data 1136 ) #=> {'a':{'b':['c','d'], 'e':['f']}} 1137 ``` 1138 """ 1139 if not isinstance(data, list): 1140 raise Exception("Attempting to `nest` an object that is not a list") 1141 if len(data) == 0: 1142 raise Exception("Attempting to `nest` from an empty list") 1143 nested_output = {} 1144 for item in self.groupKeys(keys=path_keys, data=data): 1145 nested_output = __assocPath__( 1146 path=__getKeyValues__(path_keys, item[0]), 1147 value=[i.get(value_key) for i in item], 1148 data=nested_output, 1149 ) 1150 return nested_output
Function:
- Nests a list of dictionaries into a nested dictionary
- Similar items are appended to a list in the end of the nested dictionary
Requires:
path_keys
:- Type: list of strs
- What: The variables to pull from each item in data
- Note: Used to build out the nested dicitonary
- Note: Order matters as the nesting occurs in order of variable
value_key
:- Type: str
- What: The variable to add to the list at the end of the nested dictionary path
data
:- Type: list of dicts
- What: A list of dictionaries to use for nesting purposes
Example:
data=[
{'x_1':'a','x_2':'b', 'output':'c'},
{'x_1':'a','x_2':'b', 'output':'d'},
{'x_1':'a','x_2':'e', 'output':'f'}
]
pamda.nest(
path_keys=['x_1','x_2'],
value_key='output',
data=data
) #=> {'a':{'b':['c','d'], 'e':['f']}}
1152 def nestItem(self, path_keys: list, data: list): 1153 """ 1154 Function: 1155 1156 - Nests a list of dictionaries into a nested dictionary 1157 - Similar items are appended to a list in the end of the nested dictionary 1158 - Similar to `nest`, except no values are plucked for the aggregated list 1159 1160 Requires: 1161 1162 - `path_keys`: 1163 - Type: list of strs 1164 - What: The variables to pull from each item in data 1165 - Note: Used to build out the nested dicitonary 1166 - Note: Order matters as the nesting occurs in order of variable 1167 - `data`: 1168 - Type: list of dicts 1169 - What: A list of dictionaries to use for nesting purposes 1170 1171 Example: 1172 1173 ``` 1174 data=[ 1175 {'x_1':'a','x_2':'b'}, 1176 {'x_1':'a','x_2':'b'}, 1177 {'x_1':'a','x_2':'e'} 1178 ] 1179 pamda.nestItem 1180 path_keys=['x_1','x_2'], 1181 data=data 1182 ) 1183 #=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}} 1184 1185 ``` 1186 """ 1187 if not isinstance(data, list): 1188 raise Exception("Attempting to `nest` an object that is not a list") 1189 if len(data) == 0: 1190 raise Exception("Attempting to `nest` from an empty list") 1191 nested_output = {} 1192 for item in self.groupKeys(keys=path_keys, data=data): 1193 nested_output = __assocPath__( 1194 path=__getKeyValues__(path_keys, item[0]), 1195 value=item, 1196 data=nested_output, 1197 ) 1198 return nested_output
Function:
- Nests a list of dictionaries into a nested dictionary
- Similar items are appended to a list in the end of the nested dictionary
- Similar to
nest
, except no values are plucked for the aggregated list
Requires:
path_keys
:- Type: list of strs
- What: The variables to pull from each item in data
- Note: Used to build out the nested dicitonary
- Note: Order matters as the nesting occurs in order of variable
data
:- Type: list of dicts
- What: A list of dictionaries to use for nesting purposes
Example:
data=[
{'x_1':'a','x_2':'b'},
{'x_1':'a','x_2':'b'},
{'x_1':'a','x_2':'e'}
]
pamda.nestItem
path_keys=['x_1','x_2'],
data=data
)
#=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}}
1200 def path(self, path: list | str, data: dict): 1201 """ 1202 Function: 1203 1204 - Returns the value of a path within a nested dictionary or None if the path does not exist 1205 1206 Requires: 1207 1208 - `path`: 1209 - Type: list of strs | str 1210 - What: The path to pull given the data 1211 - Note: If a string is passed, assumes a single item path list with that string 1212 - `data`: 1213 - Type: dict 1214 - What: A dictionary to get the path from 1215 1216 Example: 1217 1218 ``` 1219 data={'a':{'b':1}} 1220 pamda.path(path=['a','b'], data=data) #=> 1 1221 ``` 1222 """ 1223 if isinstance(path, str): 1224 path = [path] 1225 return __pathOr__(None, path, data)
Function:
- Returns the value of a path within a nested dictionary or None if the path does not exist
Requires:
path
:- Type: list of strs | str
- What: The path to pull given the data
- Note: If a string is passed, assumes a single item path list with that string
data
:- Type: dict
- What: A dictionary to get the path from
Example:
data={'a':{'b':1}}
pamda.path(path=['a','b'], data=data) #=> 1
1227 def pathOr(self, default, path: list | str, data: dict): 1228 """ 1229 Function: 1230 1231 - Returns the value of a path within a nested dictionary or a default value if that path does not exist 1232 1233 Requires: 1234 1235 - `default`: 1236 - Type: any 1237 - What: The object to return if the path does not exist 1238 - `path`: 1239 - Type: list of strs | str 1240 - What: The path to pull given the data 1241 - Note: If a string is passed, assumes a single item path list with that string 1242 - `data`: 1243 - Type: dict 1244 - What: A dictionary to get the path from 1245 1246 Example: 1247 1248 ``` 1249 data={'a':{'b':1}} 1250 pamda.path(default=2, path=['a','c'], data=data) #=> 2 1251 ``` 1252 """ 1253 if isinstance(path, str): 1254 path = [path] 1255 return reduce(lambda x, y: x.get(y, {}), path[:-1], data).get( 1256 path[-1], default 1257 )
Function:
- Returns the value of a path within a nested dictionary or a default value if that path does not exist
Requires:
default
:- Type: any
- What: The object to return if the path does not exist
path
:- Type: list of strs | str
- What: The path to pull given the data
- Note: If a string is passed, assumes a single item path list with that string
data
:- Type: dict
- What: A dictionary to get the path from
Example:
data={'a':{'b':1}}
pamda.path(default=2, path=['a','c'], data=data) #=> 2
1259 def pipe(self, fns: list, args: tuple, kwargs: dict): 1260 """ 1261 Function: 1262 1263 - Pipes data through n functions in order (left to right composition) and returns the output 1264 1265 Requires: 1266 1267 - `fns`: 1268 - Type: list of (functions | methods) 1269 - What: The list of functions and methods to pipe the data through 1270 - Notes: The first function in the list can be any arity (accepting any number of inputs) 1271 - Notes: Any further function in the list can only be unary (single input) 1272 - Notes: A function can be curried, but is not required to be 1273 - Notes: You may opt to curry functions and add inputs to make them unary 1274 - `args`: 1275 - Type: tuple 1276 - What: a tuple of positional arguments to pass to the first function in `fns` 1277 - `kwargs`: 1278 - Type: dict 1279 - What: a dictionary of keyword arguments to pass to the first function in `fns` 1280 1281 Examples: 1282 1283 ``` 1284 data=['abc','def'] 1285 pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c' 1286 pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c' 1287 ``` 1288 1289 ``` 1290 data={'a':{'b':'c'}} 1291 curriedPath=pamda.curry(pamda.path) 1292 pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c' 1293 ``` 1294 """ 1295 if len(fns) == 0: 1296 raise Exception("`fns` must be a list with at least one function") 1297 if self.getArity(fns[0]) == 0: 1298 raise Exception( 1299 "The first function in `fns` can have n arity (accepting n args), but this must be greater than 0." 1300 ) 1301 if not all([(self.getArity(fn) == 1) for fn in fns[1:]]): 1302 raise Exception( 1303 "Only the first function in `fns` can have n arity (accept n args). All other functions must have an arity of one (accepting one argument)." 1304 ) 1305 out = fns[0](*args, **kwargs) 1306 for fn in fns[1:]: 1307 out = fn(out) 1308 return out
Function:
- Pipes data through n functions in order (left to right composition) and returns the output
Requires:
fns
:- Type: list of (functions | methods)
- What: The list of functions and methods to pipe the data through
- Notes: The first function in the list can be any arity (accepting any number of inputs)
- Notes: Any further function in the list can only be unary (single input)
- Notes: A function can be curried, but is not required to be
- Notes: You may opt to curry functions and add inputs to make them unary
args
:- Type: tuple
- What: a tuple of positional arguments to pass to the first function in
fns
kwargs
:- Type: dict
- What: a dictionary of keyword arguments to pass to the first function in
fns
Examples:
data=['abc','def']
pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c'
pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c'
data={'a':{'b':'c'}}
curriedPath=pamda.curry(pamda.path)
pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c'
1310 def pivot(self, data: list[dict] | dict[Any, list]): 1311 """ 1312 Function: 1313 1314 - Pivots a list of dictionaries into a dictionary of lists 1315 - Pivots a dictionary of lists into a list of dictionaries 1316 1317 Requires: 1318 1319 - `data`: 1320 - Type: list of dicts | dict of lists 1321 - What: The data to pivot 1322 - Note: If a list of dictionaries is passed, all dictionaries must have the same keys 1323 - Note: If a dictionary of lists is passed, all lists must have the same length 1324 1325 Example: 1326 1327 ``` 1328 data=[ 1329 {'a':1,'b':2}, 1330 {'a':3,'b':4} 1331 ] 1332 pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]} 1333 1334 data={'a':[1,3],'b':[2,4]} 1335 pamda.pivot(data=data) 1336 #=> [ 1337 #=> {'a':1,'b':2}, 1338 #=> {'a':3,'b':4} 1339 #=> ] 1340 ``` 1341 """ 1342 if isinstance(data, list): 1343 return { 1344 key: [record[key] for record in data] for key in data[0].keys() 1345 } 1346 else: 1347 return [ 1348 {key: data[key][i] for key in data.keys()} 1349 for i in range(len(data[list(data.keys())[0]])) 1350 ]
Function:
- Pivots a list of dictionaries into a dictionary of lists
- Pivots a dictionary of lists into a list of dictionaries
Requires:
data
:- Type: list of dicts | dict of lists
- What: The data to pivot
- Note: If a list of dictionaries is passed, all dictionaries must have the same keys
- Note: If a dictionary of lists is passed, all lists must have the same length
Example:
data=[
{'a':1,'b':2},
{'a':3,'b':4}
]
pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]}
data={'a':[1,3],'b':[2,4]}
pamda.pivot(data=data)
#=> [
#=> {'a':1,'b':2},
#=> {'a':3,'b':4}
#=> ]
1352 def pluck(self, path: list | str, data: list): 1353 """ 1354 Function: 1355 1356 - Returns the values of a path within a list of nested dictionaries 1357 1358 Requires: 1359 1360 - `path`: 1361 - Type: list of strs 1362 - What: The path to pull given the data 1363 - Note: If a string is passed, assumes a single item path list with that string 1364 - `data`: 1365 - Type: list of dicts 1366 - What: A list of dictionaries to get the path from 1367 1368 Example: 1369 1370 ``` 1371 data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}] 1372 pamda.pluck(path=['a','b'], data=data) #=> [1,2] 1373 ``` 1374 """ 1375 if len(data) == 0: 1376 raise Exception("Attempting to pluck from an empty list") 1377 if isinstance(path, str): 1378 path = [path] 1379 return [__pathOr__(default=None, path=path, data=i) for i in data]
Function:
- Returns the values of a path within a list of nested dictionaries
Requires:
path
:- Type: list of strs
- What: The path to pull given the data
- Note: If a string is passed, assumes a single item path list with that string
data
:- Type: list of dicts
- What: A list of dictionaries to get the path from
Example:
data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
pamda.pluck(path=['a','b'], data=data) #=> [1,2]
1381 def pluckIf(self, fn, path: list | str, data: list): 1382 """ 1383 Function: 1384 1385 - Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value 1386 1387 Requires: 1388 1389 - `fn`: 1390 - Type: function 1391 - What: A function to take in each item in data and return a boolean 1392 - Note: Only items that return true are plucked 1393 - Note: Should be a unary function (take one input) 1394 - `path`: 1395 - Type: list of strs 1396 - What: The path to pull given the data 1397 - Note: If a string is passed, assumes a single item path list with that string 1398 - `data`: 1399 - Type: list of dicts 1400 - What: A list of dictionary to get the path from 1401 1402 Example: 1403 1404 ``` 1405 1406 data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}] 1407 pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d'] 1408 ``` 1409 """ 1410 if len(data) == 0: 1411 raise Exception("Attempting to pluck from an empty list") 1412 curried_fn = self.curry(fn) 1413 if curried_fn.__arity__ != 1: 1414 raise Exception( 1415 "`pluckIf` `fn` must have an arity of 1 (take one input)" 1416 ) 1417 if isinstance(path, str): 1418 path = [path] 1419 return [ 1420 __pathOr__(default=None, path=path, data=i) for i in data if fn(i) 1421 ]
Function:
- Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value
Requires:
fn
:- Type: function
- What: A function to take in each item in data and return a boolean
- Note: Only items that return true are plucked
- Note: Should be a unary function (take one input)
path
:- Type: list of strs
- What: The path to pull given the data
- Note: If a string is passed, assumes a single item path list with that string
data
:- Type: list of dicts
- What: A list of dictionary to get the path from
Example:
data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}]
pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d']
1423 def project(self, keys: list[str], data: list[dict]): 1424 """ 1425 Function: 1426 1427 - Returns a list of dictionaries with only the keys provided 1428 - Analogous to SQL's `SELECT` statement 1429 1430 Requires: 1431 1432 - `keys`: 1433 - Type: list of strs 1434 - What: The keys to select from each dictionary in the data list 1435 - `data`: 1436 - Type: list of dicts 1437 - What: The list of dictionaries to select from 1438 1439 Example: 1440 1441 ``` 1442 data=[ 1443 {'a':1,'b':2,'c':3}, 1444 {'a':4,'b':5,'c':6} 1445 ] 1446 pamda.project(keys=['a','c'], data=data) 1447 #=> [ 1448 #=> {'a':1,'c':3}, 1449 #=> {'a':4,'c':6} 1450 #=> ] 1451 ``` 1452 """ 1453 return [{key: record[key] for key in keys} for record in data]
Function:
- Returns a list of dictionaries with only the keys provided
- Analogous to SQL's
SELECT
statement
Requires:
keys
:- Type: list of strs
- What: The keys to select from each dictionary in the data list
data
:- Type: list of dicts
- What: The list of dictionaries to select from
Example:
data=[
{'a':1,'b':2,'c':3},
{'a':4,'b':5,'c':6}
]
pamda.project(keys=['a','c'], data=data)
#=> [
#=> {'a':1,'c':3},
#=> {'a':4,'c':6}
#=> ]
1455 def props(self, keys: list[str], data: dict): 1456 """ 1457 Function: 1458 1459 - Returns the values of a list of keys within a dictionary 1460 1461 Requires: 1462 1463 - `keys`: 1464 - Type: list of strs 1465 - What: The keys to pull given the data 1466 - `data`: 1467 - Type: dict 1468 - What: A dictionary to get the keys from 1469 1470 Example: 1471 ``` 1472 data={'a':1,'b':2,'c':3} 1473 pamda.props(keys=['a','c'], data=data) 1474 #=> [1,3] 1475 ``` 1476 """ 1477 return [data[key] for key in keys]
Function:
- Returns the values of a list of keys within a dictionary
Requires:
keys
:- Type: list of strs
- What: The keys to pull given the data
data
:- Type: dict
- What: A dictionary to get the keys from
Example:
data={'a':1,'b':2,'c':3}
pamda.props(keys=['a','c'], data=data)
#=> [1,3]
1479 def reduce(self, fn, initial_accumulator, data: list): 1480 """ 1481 Function: 1482 1483 - Returns a single item by iterating a function starting with an accumulator over a list 1484 1485 Requires: 1486 1487 - `fn`: 1488 - Type: function | method 1489 - What: The function or method to reduce 1490 - Note: This function should have an arity of 2 (take two inputs) 1491 - Note: The first input should take the accumulator value 1492 - Note: The second input should take the data value 1493 -`initial_accumulator`: 1494 - Type: any 1495 - What: The initial item to pass into the function when starting the accumulation process 1496 - `data`: 1497 - Type: list 1498 - What: The list of items to iterate over 1499 1500 Example: 1501 1502 ``` 1503 data=[1,2,3,4] 1504 pamda.reduce( 1505 fn=pamda.add, 1506 initial_accumulator=0, 1507 data=data 1508 ) 1509 #=> 10 1510 1511 ``` 1512 """ 1513 fn = self.curry(fn) 1514 if fn.__arity__ != 2: 1515 raise Exception( 1516 "`reduce` `fn` must have an arity of 2 (take two inputs)" 1517 ) 1518 if not isinstance(data, (list)): 1519 raise Exception("`reduce` `data` must be a list") 1520 if not len(data) > 0: 1521 raise Exception( 1522 "`reduce` `data` has a length of 0, however it must have a length of at least 1" 1523 ) 1524 acc = initial_accumulator 1525 for i in data: 1526 acc = fn(acc, i) 1527 return acc
Function:
- Returns a single item by iterating a function starting with an accumulator over a list
Requires:
fn
:- Type: function | method
- What: The function or method to reduce
- Note: This function should have an arity of 2 (take two inputs)
- Note: The first input should take the accumulator value
- Note: The second input should take the data value
-
initial_accumulator
: - Type: any
- What: The initial item to pass into the function when starting the accumulation process
data
:- Type: list
- What: The list of items to iterate over
Example:
data=[1,2,3,4]
pamda.reduce(
fn=pamda.add,
initial_accumulator=0,
data=data
)
#=> 10
1529 def safeDivide(self, denominator: int | float, a: int | float): 1530 """ 1531 Function: 1532 1533 - Forces division to work by enforcing a denominator of 1 if the provided denominator is zero 1534 1535 Requires: 1536 1537 - `denominator`: 1538 - Type: int | float 1539 - What: The denominator 1540 1541 - `a`: 1542 - Type: int | float 1543 - What: The numerator 1544 1545 Example: 1546 1547 ``` 1548 pamda.safeDivide(2,10) #=> 5 1549 pamda.safeDivide(0,10) #=> 10 1550 ``` 1551 """ 1552 return a / denominator if denominator != 0 else a
Function:
- Forces division to work by enforcing a denominator of 1 if the provided denominator is zero
Requires:
denominator
:- Type: int | float
- What: The denominator
a
:- Type: int | float
- What: The numerator
Example:
pamda.safeDivide(2,10) #=> 5
pamda.safeDivide(0,10) #=> 10
1554 def safeDivideDefault( 1555 self, 1556 default_denominator: int | float, 1557 denominator: int | float, 1558 a: int | float, 1559 ): 1560 """ 1561 Function: 1562 1563 - Forces division to work by enforcing a non zero default denominator if the provided denominator is zero 1564 1565 Requires: 1566 1567 - `default_denominator`: 1568 - Type: int | float 1569 - What: A non zero denominator to use if denominator is zero 1570 - Default: 1 1571 - `denominator`: 1572 - Type: int | float 1573 - What: The denominator 1574 - `a`: 1575 - Type: int | float 1576 - What: The numerator 1577 1578 Example: 1579 1580 ``` 1581 pamda.safeDivideDefault(2,5,10) #=> 2 1582 pamda.safeDivideDefault(2,0,10) #=> 5 1583 ``` 1584 """ 1585 if default_denominator == 0: 1586 raise Exception( 1587 "`safeDivideDefault` `default_denominator` can not be 0" 1588 ) 1589 return a / denominator if denominator != 0 else a / default_denominator
Function:
- Forces division to work by enforcing a non zero default denominator if the provided denominator is zero
Requires:
default_denominator
:- Type: int | float
- What: A non zero denominator to use if denominator is zero
- Default: 1
denominator
:- Type: int | float
- What: The denominator
a
:- Type: int | float
- What: The numerator
Example:
pamda.safeDivideDefault(2,5,10) #=> 2
pamda.safeDivideDefault(2,0,10) #=> 5
1591 def symmetricDifference(self, a: list, b: list): 1592 """ 1593 Function: 1594 1595 - Combines two lists into a list of no duplicates items present in one list but not the other 1596 1597 Requires: 1598 1599 - `a`: 1600 - Type: list 1601 - What: List of items in which to look for a difference 1602 - `b`: 1603 - Type: list 1604 - What: List of items in which to look for a difference 1605 1606 Example: 1607 1608 ``` 1609 a=['a','b'] 1610 b=['b','c'] 1611 pamda.symmetricDifference(a=a, b=b) #=> ['a','c'] 1612 ``` 1613 """ 1614 return list(set(a).difference(set(b))) + list(set(b).difference(set(a)))
Function:
- Combines two lists into a list of no duplicates items present in one list but not the other
Requires:
a
:- Type: list
- What: List of items in which to look for a difference
b
:- Type: list
- What: List of items in which to look for a difference
Example:
a=['a','b']
b=['b','c']
pamda.symmetricDifference(a=a, b=b) #=> ['a','c']
1616 def tail(self, data: list | str): 1617 """ 1618 Function: 1619 1620 - Picks the last item out of a list or string 1621 1622 Requires: 1623 1624 - `data`: 1625 - Type: list | str 1626 - What: A list or string 1627 1628 Example: 1629 1630 ``` 1631 data=['fe','fi','fo','fum'] 1632 pamda.tail( 1633 data=data 1634 ) #=> fum 1635 ``` 1636 """ 1637 if not len(data) > 0: 1638 raise Exception("Attempting to call `tail` on an empty list or str") 1639 return data[-1]
Function:
- Picks the last item out of a list or string
Requires:
data
:- Type: list | str
- What: A list or string
Example:
data=['fe','fi','fo','fum']
pamda.tail(
data=data
) #=> fum
1641 def thunkify(self, fn): 1642 """ 1643 Function: 1644 1645 - Creates a curried thunk out of a function 1646 - Evaluation of the thunk lazy and is delayed until called 1647 1648 Requires: 1649 1650 - `fn`: 1651 - Type: function | method 1652 - What: The function or method to thunkify 1653 - Note: Thunkified functions are automatically curried 1654 - Note: Class methods auto apply self during thunkify 1655 1656 Notes: 1657 1658 - Input functions are not thunkified in place 1659 - The returned function is a thunkified version of the input function 1660 - A curried function can be thunkified in place by calling fn.thunkify() 1661 1662 Examples: 1663 1664 ``` 1665 def add(a,b): 1666 return a+b 1667 1668 addThunk=pamda.thunkify(add) 1669 1670 add(1,2) #=> 3 1671 addThunk(1,2) 1672 addThunk(1,2)() #=> 3 1673 1674 x=addThunk(1,2) 1675 x() #=> 3 1676 ``` 1677 1678 ``` 1679 @pamda.curry 1680 def add(a,b): 1681 return a+b 1682 1683 add(1,2) #=> 3 1684 1685 add.thunkify() 1686 1687 add(1,2) 1688 add(1,2)() #=> 3 1689 ``` 1690 """ 1691 fn = self.curry(fn) 1692 return fn.thunkify()
Function:
- Creates a curried thunk out of a function
- Evaluation of the thunk lazy and is delayed until called
Requires:
fn
:- Type: function | method
- What: The function or method to thunkify
- Note: Thunkified functions are automatically curried
- Note: Class methods auto apply self during thunkify
Notes:
- Input functions are not thunkified in place
- The returned function is a thunkified version of the input function
- A curried function can be thunkified in place by calling fn.thunkify()
Examples:
def add(a,b):
return a+b
addThunk=pamda.thunkify(add)
add(1,2) #=> 3
addThunk(1,2)
addThunk(1,2)() #=> 3
x=addThunk(1,2)
x() #=> 3
@pamda.curry
def add(a,b):
return a+b
add(1,2) #=> 3
add.thunkify()
add(1,2)
add(1,2)() #=> 3
1694 def unnest(self, data: list): 1695 """ 1696 Function: 1697 1698 - Removes one level of depth for all items in a list 1699 1700 Requires: 1701 1702 - `data`: 1703 - Type: list 1704 - What: A list of items to unnest by one level 1705 1706 Examples: 1707 1708 ``` 1709 data=['fe','fi',['fo',['fum']]] 1710 pamda.unnest( 1711 data=data 1712 ) #=> ['fe','fi','fo',['fum']] 1713 ``` 1714 """ 1715 if not len(data) > 0: 1716 raise Exception("Attempting to call `unnest` on an empty list") 1717 output = [] 1718 for i in data: 1719 if isinstance(i, list): 1720 output += i 1721 else: 1722 output.append(i) 1723 return output
Function:
- Removes one level of depth for all items in a list
Requires:
data
:- Type: list
- What: A list of items to unnest by one level
Examples:
data=['fe','fi',['fo',['fum']]]
pamda.unnest(
data=data
) #=> ['fe','fi','fo',['fum']]
1725 def zip(self, a: list, b: list): 1726 """ 1727 Function: 1728 1729 - Creates a new list out of the two supplied by pairing up equally-positioned items from both lists 1730 1731 Requires: 1732 1733 - `a`: 1734 - Type: list 1735 - What: List of items to appear in new list first 1736 - `b`: 1737 - Type: list 1738 - What: List of items to appear in new list second 1739 1740 Example: 1741 1742 ``` 1743 a=['a','b'] 1744 b=[1,2] 1745 pamda.zip(a=a, b=b) #=> [['a',1],['b',2]] 1746 ``` 1747 """ 1748 return list(map(list, zip(a, b)))
Function:
- Creates a new list out of the two supplied by pairing up equally-positioned items from both lists
Requires:
a
:- Type: list
- What: List of items to appear in new list first
b
:- Type: list
- What: List of items to appear in new list second
Example:
a=['a','b']
b=[1,2]
pamda.zip(a=a, b=b) #=> [['a',1],['b',2]]
1750 def zipObj(self, a: list, b: list): 1751 """ 1752 Function: 1753 1754 - Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists 1755 - The first list represents keys and the second values 1756 1757 Requires: 1758 1759 - `a`: 1760 - Type: list 1761 - What: List of items to appear in new list first 1762 - `b`: 1763 - Type: list 1764 - What: List of items to appear in new list second 1765 1766 Example: 1767 1768 ``` 1769 a=['a','b'] 1770 b=[1,2] 1771 pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2} 1772 ``` 1773 """ 1774 return dict(zip(a, b))
Function:
- Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists
- The first list represents keys and the second values
Requires:
a
:- Type: list
- What: List of items to appear in new list first
b
:- Type: list
- What: List of items to appear in new list second
Example:
a=['a','b']
b=[1,2]
pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2}