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 try: 846 reduce(lambda x, y: x[y], path, data) 847 return True 848 except (KeyError, IndexError, TypeError): 849 return False 850 851 def hardRound(self, decimal_places: int, a: int | float): 852 """ 853 Function: 854 855 - Rounds to a set number of decimal places regardless of floating point math in python 856 857 Requires: 858 859 - `decimal_places`: 860 - Type: int 861 - What: The number of decimal places to round to 862 - Default: 0 863 - Notes: Negative numbers accepted (EG -1 rounds to the nearest 10) 864 - `a`: 865 - Type: int | float 866 - What: The number to round 867 868 Example: 869 870 ``` 871 a=12.345 872 pamda.hardRound(1,a) #=> 12.3 873 pamda.hardRound(-1,a) #=> 10 874 ``` 875 """ 876 return int(a * (10**decimal_places) + 0.5) / (10**decimal_places) 877 878 def head(self, data: list | str): 879 """ 880 Function: 881 882 - Picks the first item out of a list or string 883 884 Requires: 885 886 - `data`: 887 - Type: list | str 888 - What: A list or string 889 890 Example: 891 892 ``` 893 data=['fe','fi','fo','fum'] 894 pamda.first( 895 data=data 896 ) #=> fe 897 ``` 898 """ 899 if not isinstance(data, (list, str)): 900 raise Exception("`head` can only be called on a `str` or a `list`") 901 if not len(data) > 0: 902 raise Exception("Attempting to call `head` on an empty list or str") 903 return data[0] 904 905 def inc(self, a: int | float): 906 """ 907 Function: 908 909 - Increments a number by one 910 911 Requires: 912 913 - `a`: 914 - Type: int | float 915 - What: The number to increment 916 917 Example: 918 919 ``` 920 pamda.inc(42) #=> 43 921 ``` 922 """ 923 if not isinstance(a, (int, float)): 924 raise Exception("`a` must be an `int` or a `float`") 925 return a + 1 926 927 def intersection(self, a: list, b: list): 928 """ 929 Function: 930 931 - Combines two lists into a list of no duplicates composed of those elements common to both lists 932 933 Requires: 934 935 - `a`: 936 - Type: list 937 - What: List of items in which to look for an intersection 938 - `b`: 939 - Type: list 940 - What: List of items in which to look for an intersection 941 942 Example: 943 944 ``` 945 a=['a','b'] 946 b=['b','c'] 947 pamda.intersection(a=a, b=b) #=> ['b'] 948 ``` 949 """ 950 return list(set(a).intersection(set(b))) 951 952 def map(self, fn, data: list | dict): 953 """ 954 Function: 955 956 - Maps a function over a list or a dictionary 957 958 Requires: 959 960 - `fn`: 961 - Type: function | method 962 - What: The function or method to map over the list or dictionary 963 - Note: This function should have an arity of 1 964 - `data`: 965 - Type: list | dict 966 - What: The list or dict of items to map the function over 967 968 Examples: 969 970 ``` 971 data=[1,2,3] 972 pamda.map( 973 fn=pamda.inc, 974 data=data 975 ) 976 #=> [2,3,4] 977 ``` 978 979 ``` 980 data={'a':1,'b':2,'c':3} 981 pamda.map( 982 fn=pamda.inc, 983 data=data 984 ) 985 #=> {'a':2,'b':3,'c':4} 986 ``` 987 988 """ 989 # TODO: Check for efficiency gains 990 fn = self.curry(fn) 991 if fn.__arity__ != 1: 992 raise Exception("`map` `fn` must be unary (take one input)") 993 if not len(data) > 0: 994 raise Exception( 995 "`map` `data` has a length of 0 or is an empty dictionary, however it must have at least one element in it" 996 ) 997 if isinstance(data, dict): 998 return {key: fn(value) for key, value in data.items()} 999 else: 1000 return [fn(i) for i in data] 1001 1002 def mean(self, data: list): 1003 """ 1004 Function: 1005 1006 - Calculates the mean of a given list 1007 1008 Requires: 1009 1010 - `data`: 1011 - Type: list of (floats | ints) 1012 - What: The list with wich to calculate the mean 1013 - Note: If the length of this list is 0, returns None 1014 1015 Example: 1016 1017 ``` 1018 data=[1,2,3] 1019 pamda.mean(data=data) 1020 #=> 2 1021 ``` 1022 1023 ``` 1024 data=[] 1025 pamda.mean(data=data) 1026 #=> None 1027 ``` 1028 """ 1029 if len(data) == 0: 1030 return None 1031 return sum(data) / len(data) 1032 1033 def median(self, data: list): 1034 """ 1035 Function: 1036 1037 - Calculates the median of a given list 1038 - If the length of the list is even, calculates the mean of the two central values 1039 1040 Requires: 1041 1042 - `data`: 1043 - Type: list of (floats | ints) 1044 - What: The list with wich to calculate the mean 1045 - Note: If the length of this list is 0, returns None 1046 1047 Examples: 1048 1049 ``` 1050 data=[7,2,8,9] 1051 pamda.median(data=data) 1052 #=> 7.5 1053 ``` 1054 1055 ``` 1056 data=[7,8,9] 1057 pamda.median(data=data) 1058 #=> 8 1059 ``` 1060 1061 ``` 1062 data=[] 1063 pamda.median(data=data) 1064 #=> None 1065 ``` 1066 """ 1067 if not isinstance(data, (list)): 1068 raise Exception("`median` `data` must be a list") 1069 length = len(data) 1070 if length == 0: 1071 return None 1072 data = sorted(data) 1073 if length % 2 == 0: 1074 return (data[int(length / 2)] + data[int(length / 2) - 1]) / 2 1075 return data[int(length / 2)] 1076 1077 def mergeDeep(self, update_data, data): 1078 """ 1079 Function: 1080 1081 - Recursively merges two nested dictionaries keeping all keys at each layer 1082 - Values from `update_data` are used when keys are present in both dictionaries 1083 1084 Requires: 1085 1086 - `update_data`: 1087 - Type: any 1088 - What: The new data that will take precedence during merging 1089 - `data`: 1090 - Type: any 1091 - What: The original data that will be merged into 1092 1093 Example: 1094 1095 ``` 1096 data={'a':{'b':{'c':'d'},'e':'f'}} 1097 update_data={'a':{'b':{'h':'i'},'e':'g'}} 1098 pamda.mergeDeep( 1099 update_data=update_data, 1100 data=data 1101 ) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}} 1102 ``` 1103 """ 1104 return __mergeDeep__(update_data, data) 1105 1106 def nest(self, path_keys: list, value_key: str, data: list): 1107 """ 1108 Function: 1109 1110 - Nests a list of dictionaries into a nested dictionary 1111 - Similar items are appended to a list in the end of the nested dictionary 1112 1113 Requires: 1114 1115 - `path_keys`: 1116 - Type: list of strs 1117 - What: The variables to pull from each item in data 1118 - Note: Used to build out the nested dicitonary 1119 - Note: Order matters as the nesting occurs in order of variable 1120 - `value_key`: 1121 - Type: str 1122 - What: The variable to add to the list at the end of the nested dictionary path 1123 - `data`: 1124 - Type: list of dicts 1125 - What: A list of dictionaries to use for nesting purposes 1126 1127 Example: 1128 1129 ``` 1130 data=[ 1131 {'x_1':'a','x_2':'b', 'output':'c'}, 1132 {'x_1':'a','x_2':'b', 'output':'d'}, 1133 {'x_1':'a','x_2':'e', 'output':'f'} 1134 ] 1135 pamda.nest( 1136 path_keys=['x_1','x_2'], 1137 value_key='output', 1138 data=data 1139 ) #=> {'a':{'b':['c','d'], 'e':['f']}} 1140 ``` 1141 """ 1142 if not isinstance(data, list): 1143 raise Exception("Attempting to `nest` an object that is not a list") 1144 if len(data) == 0: 1145 raise Exception("Attempting to `nest` from an empty list") 1146 nested_output = {} 1147 for item in self.groupKeys(keys=path_keys, data=data): 1148 nested_output = __assocPath__( 1149 path=__getKeyValues__(path_keys, item[0]), 1150 value=[i.get(value_key) for i in item], 1151 data=nested_output, 1152 ) 1153 return nested_output 1154 1155 def nestItem(self, path_keys: list, data: list): 1156 """ 1157 Function: 1158 1159 - Nests a list of dictionaries into a nested dictionary 1160 - Similar items are appended to a list in the end of the nested dictionary 1161 - Similar to `nest`, except no values are plucked for the aggregated list 1162 1163 Requires: 1164 1165 - `path_keys`: 1166 - Type: list of strs 1167 - What: The variables to pull from each item in data 1168 - Note: Used to build out the nested dicitonary 1169 - Note: Order matters as the nesting occurs in order of variable 1170 - `data`: 1171 - Type: list of dicts 1172 - What: A list of dictionaries to use for nesting purposes 1173 1174 Example: 1175 1176 ``` 1177 data=[ 1178 {'x_1':'a','x_2':'b'}, 1179 {'x_1':'a','x_2':'b'}, 1180 {'x_1':'a','x_2':'e'} 1181 ] 1182 pamda.nestItem 1183 path_keys=['x_1','x_2'], 1184 data=data 1185 ) 1186 #=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}} 1187 1188 ``` 1189 """ 1190 if not isinstance(data, list): 1191 raise Exception("Attempting to `nest` an object that is not a list") 1192 if len(data) == 0: 1193 raise Exception("Attempting to `nest` from an empty list") 1194 nested_output = {} 1195 for item in self.groupKeys(keys=path_keys, data=data): 1196 nested_output = __assocPath__( 1197 path=__getKeyValues__(path_keys, item[0]), 1198 value=item, 1199 data=nested_output, 1200 ) 1201 return nested_output 1202 1203 def path(self, path: list | str, data: dict): 1204 """ 1205 Function: 1206 1207 - Returns the value of a path within a nested dictionary or None if the path does not exist 1208 1209 Requires: 1210 1211 - `path`: 1212 - Type: list of strs | str 1213 - What: The path to pull given the data 1214 - Note: If a string is passed, assumes a single item path list with that string 1215 - `data`: 1216 - Type: dict 1217 - What: A dictionary to get the path from 1218 1219 Example: 1220 1221 ``` 1222 data={'a':{'b':1}} 1223 pamda.path(path=['a','b'], data=data) #=> 1 1224 ``` 1225 """ 1226 if isinstance(path, str): 1227 path = [path] 1228 return __pathOr__(None, path, data) 1229 1230 def pathOr(self, default, path: list | str, data: dict): 1231 """ 1232 Function: 1233 1234 - Returns the value of a path within a nested dictionary or a default value if that path does not exist 1235 1236 Requires: 1237 1238 - `default`: 1239 - Type: any 1240 - What: The object to return if the path does not exist 1241 - `path`: 1242 - Type: list of strs | str 1243 - What: The path to pull given the data 1244 - Note: If a string is passed, assumes a single item path list with that string 1245 - `data`: 1246 - Type: dict 1247 - What: A dictionary to get the path from 1248 1249 Example: 1250 1251 ``` 1252 data={'a':{'b':1}} 1253 pamda.path(default=2, path=['a','c'], data=data) #=> 2 1254 ``` 1255 """ 1256 if isinstance(path, str): 1257 path = [path] 1258 try: 1259 return reduce(lambda x, y: x[y], path, data) 1260 except (KeyError, IndexError, TypeError): 1261 return default 1262 1263 def pipe(self, fns: list, args: tuple, kwargs: dict): 1264 """ 1265 Function: 1266 1267 - Pipes data through n functions in order (left to right composition) and returns the output 1268 1269 Requires: 1270 1271 - `fns`: 1272 - Type: list of (functions | methods) 1273 - What: The list of functions and methods to pipe the data through 1274 - Notes: The first function in the list can be any arity (accepting any number of inputs) 1275 - Notes: Any further function in the list can only be unary (single input) 1276 - Notes: A function can be curried, but is not required to be 1277 - Notes: You may opt to curry functions and add inputs to make them unary 1278 - `args`: 1279 - Type: tuple 1280 - What: a tuple of positional arguments to pass to the first function in `fns` 1281 - `kwargs`: 1282 - Type: dict 1283 - What: a dictionary of keyword arguments to pass to the first function in `fns` 1284 1285 Examples: 1286 1287 ``` 1288 data=['abc','def'] 1289 pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c' 1290 pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c' 1291 ``` 1292 1293 ``` 1294 data={'a':{'b':'c'}} 1295 curriedPath=pamda.curry(pamda.path) 1296 pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c' 1297 ``` 1298 """ 1299 if len(fns) == 0: 1300 raise Exception("`fns` must be a list with at least one function") 1301 if self.getArity(fns[0]) == 0: 1302 raise Exception( 1303 "The first function in `fns` can have n arity (accepting n args), but this must be greater than 0." 1304 ) 1305 if not all([(self.getArity(fn) == 1) for fn in fns[1:]]): 1306 raise Exception( 1307 "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)." 1308 ) 1309 out = fns[0](*args, **kwargs) 1310 for fn in fns[1:]: 1311 out = fn(out) 1312 return out 1313 1314 def pivot(self, data: list[dict] | dict[Any, list]): 1315 """ 1316 Function: 1317 1318 - Pivots a list of dictionaries into a dictionary of lists 1319 - Pivots a dictionary of lists into a list of dictionaries 1320 1321 Requires: 1322 1323 - `data`: 1324 - Type: list of dicts | dict of lists 1325 - What: The data to pivot 1326 - Note: If a list of dictionaries is passed, all dictionaries must have the same keys 1327 - Note: If a dictionary of lists is passed, all lists must have the same length 1328 1329 Example: 1330 1331 ``` 1332 data=[ 1333 {'a':1,'b':2}, 1334 {'a':3,'b':4} 1335 ] 1336 pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]} 1337 1338 data={'a':[1,3],'b':[2,4]} 1339 pamda.pivot(data=data) 1340 #=> [ 1341 #=> {'a':1,'b':2}, 1342 #=> {'a':3,'b':4} 1343 #=> ] 1344 ``` 1345 """ 1346 if isinstance(data, list): 1347 return { 1348 key: [record[key] for record in data] for key in data[0].keys() 1349 } 1350 else: 1351 return [ 1352 {key: data[key][i] for key in data.keys()} 1353 for i in range(len(data[list(data.keys())[0]])) 1354 ] 1355 1356 def pluck(self, path: list | str, data: list): 1357 """ 1358 Function: 1359 1360 - Returns the values of a path within a list of nested dictionaries 1361 1362 Requires: 1363 1364 - `path`: 1365 - Type: list of strs 1366 - What: The path to pull given the data 1367 - Note: If a string is passed, assumes a single item path list with that string 1368 - `data`: 1369 - Type: list of dicts 1370 - What: A list of dictionaries to get the path from 1371 1372 Example: 1373 1374 ``` 1375 data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}] 1376 pamda.pluck(path=['a','b'], data=data) #=> [1,2] 1377 ``` 1378 """ 1379 if len(data) == 0: 1380 raise Exception("Attempting to pluck from an empty list") 1381 if isinstance(path, str): 1382 path = [path] 1383 return [__pathOr__(default=None, path=path, data=i) for i in data] 1384 1385 def pluckIf(self, fn, path: list | str, data: list): 1386 """ 1387 Function: 1388 1389 - Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value 1390 1391 Requires: 1392 1393 - `fn`: 1394 - Type: function 1395 - What: A function to take in each item in data and return a boolean 1396 - Note: Only items that return true are plucked 1397 - Note: Should be a unary function (take one input) 1398 - `path`: 1399 - Type: list of strs 1400 - What: The path to pull given the data 1401 - Note: If a string is passed, assumes a single item path list with that string 1402 - `data`: 1403 - Type: list of dicts 1404 - What: A list of dictionary to get the path from 1405 1406 Example: 1407 1408 ``` 1409 1410 data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}] 1411 pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d'] 1412 ``` 1413 """ 1414 if len(data) == 0: 1415 raise Exception("Attempting to pluck from an empty list") 1416 curried_fn = self.curry(fn) 1417 if curried_fn.__arity__ != 1: 1418 raise Exception( 1419 "`pluckIf` `fn` must have an arity of 1 (take one input)" 1420 ) 1421 if isinstance(path, str): 1422 path = [path] 1423 return [ 1424 __pathOr__(default=None, path=path, data=i) for i in data if fn(i) 1425 ] 1426 1427 def project(self, keys: list[str], data: list[dict]): 1428 """ 1429 Function: 1430 1431 - Returns a list of dictionaries with only the keys provided 1432 - Analogous to SQL's `SELECT` statement 1433 1434 Requires: 1435 1436 - `keys`: 1437 - Type: list of strs 1438 - What: The keys to select from each dictionary in the data list 1439 - `data`: 1440 - Type: list of dicts 1441 - What: The list of dictionaries to select from 1442 1443 Example: 1444 1445 ``` 1446 data=[ 1447 {'a':1,'b':2,'c':3}, 1448 {'a':4,'b':5,'c':6} 1449 ] 1450 pamda.project(keys=['a','c'], data=data) 1451 #=> [ 1452 #=> {'a':1,'c':3}, 1453 #=> {'a':4,'c':6} 1454 #=> ] 1455 ``` 1456 """ 1457 return [{key: record[key] for key in keys} for record in data] 1458 1459 def props(self, keys: list[str], data: dict): 1460 """ 1461 Function: 1462 1463 - Returns the values of a list of keys within a dictionary 1464 1465 Requires: 1466 1467 - `keys`: 1468 - Type: list of strs 1469 - What: The keys to pull given the data 1470 - `data`: 1471 - Type: dict 1472 - What: A dictionary to get the keys from 1473 1474 Example: 1475 ``` 1476 data={'a':1,'b':2,'c':3} 1477 pamda.props(keys=['a','c'], data=data) 1478 #=> [1,3] 1479 ``` 1480 """ 1481 return [data[key] for key in keys] 1482 1483 def reduce(self, fn, initial_accumulator, data: list): 1484 """ 1485 Function: 1486 1487 - Returns a single item by iterating a function starting with an accumulator over a list 1488 1489 Requires: 1490 1491 - `fn`: 1492 - Type: function | method 1493 - What: The function or method to reduce 1494 - Note: This function should have an arity of 2 (take two inputs) 1495 - Note: The first input should take the accumulator value 1496 - Note: The second input should take the data value 1497 -`initial_accumulator`: 1498 - Type: any 1499 - What: The initial item to pass into the function when starting the accumulation process 1500 - `data`: 1501 - Type: list 1502 - What: The list of items to iterate over 1503 1504 Example: 1505 1506 ``` 1507 data=[1,2,3,4] 1508 pamda.reduce( 1509 fn=pamda.add, 1510 initial_accumulator=0, 1511 data=data 1512 ) 1513 #=> 10 1514 1515 ``` 1516 """ 1517 fn = self.curry(fn) 1518 if fn.__arity__ != 2: 1519 raise Exception( 1520 "`reduce` `fn` must have an arity of 2 (take two inputs)" 1521 ) 1522 if not isinstance(data, (list)): 1523 raise Exception("`reduce` `data` must be a list") 1524 if not len(data) > 0: 1525 raise Exception( 1526 "`reduce` `data` has a length of 0, however it must have a length of at least 1" 1527 ) 1528 acc = initial_accumulator 1529 for i in data: 1530 acc = fn(acc, i) 1531 return acc 1532 1533 def safeDivide(self, denominator: int | float, a: int | float): 1534 """ 1535 Function: 1536 1537 - Forces division to work by enforcing a denominator of 1 if the provided denominator is zero 1538 1539 Requires: 1540 1541 - `denominator`: 1542 - Type: int | float 1543 - What: The denominator 1544 1545 - `a`: 1546 - Type: int | float 1547 - What: The numerator 1548 1549 Example: 1550 1551 ``` 1552 pamda.safeDivide(2,10) #=> 5 1553 pamda.safeDivide(0,10) #=> 10 1554 ``` 1555 """ 1556 return a / denominator if denominator != 0 else a 1557 1558 def safeDivideDefault( 1559 self, 1560 default_denominator: int | float, 1561 denominator: int | float, 1562 a: int | float, 1563 ): 1564 """ 1565 Function: 1566 1567 - Forces division to work by enforcing a non zero default denominator if the provided denominator is zero 1568 1569 Requires: 1570 1571 - `default_denominator`: 1572 - Type: int | float 1573 - What: A non zero denominator to use if denominator is zero 1574 - Default: 1 1575 - `denominator`: 1576 - Type: int | float 1577 - What: The denominator 1578 - `a`: 1579 - Type: int | float 1580 - What: The numerator 1581 1582 Example: 1583 1584 ``` 1585 pamda.safeDivideDefault(2,5,10) #=> 2 1586 pamda.safeDivideDefault(2,0,10) #=> 5 1587 ``` 1588 """ 1589 if default_denominator == 0: 1590 raise Exception( 1591 "`safeDivideDefault` `default_denominator` can not be 0" 1592 ) 1593 return a / denominator if denominator != 0 else a / default_denominator 1594 1595 def symmetricDifference(self, a: list, b: list): 1596 """ 1597 Function: 1598 1599 - Combines two lists into a list of no duplicates items present in one list but not the other 1600 1601 Requires: 1602 1603 - `a`: 1604 - Type: list 1605 - What: List of items in which to look for a difference 1606 - `b`: 1607 - Type: list 1608 - What: List of items in which to look for a difference 1609 1610 Example: 1611 1612 ``` 1613 a=['a','b'] 1614 b=['b','c'] 1615 pamda.symmetricDifference(a=a, b=b) #=> ['a','c'] 1616 ``` 1617 """ 1618 return list(set(a).difference(set(b))) + list(set(b).difference(set(a))) 1619 1620 def tail(self, data: list | str): 1621 """ 1622 Function: 1623 1624 - Picks the last item out of a list or string 1625 1626 Requires: 1627 1628 - `data`: 1629 - Type: list | str 1630 - What: A list or string 1631 1632 Example: 1633 1634 ``` 1635 data=['fe','fi','fo','fum'] 1636 pamda.tail( 1637 data=data 1638 ) #=> fum 1639 ``` 1640 """ 1641 if not len(data) > 0: 1642 raise Exception("Attempting to call `tail` on an empty list or str") 1643 return data[-1] 1644 1645 def thunkify(self, fn): 1646 """ 1647 Function: 1648 1649 - Creates a curried thunk out of a function 1650 - Evaluation of the thunk lazy and is delayed until called 1651 1652 Requires: 1653 1654 - `fn`: 1655 - Type: function | method 1656 - What: The function or method to thunkify 1657 - Note: Thunkified functions are automatically curried 1658 - Note: Class methods auto apply self during thunkify 1659 1660 Notes: 1661 1662 - Input functions are not thunkified in place 1663 - The returned function is a thunkified version of the input function 1664 - A curried function can be thunkified in place by calling fn.thunkify() 1665 1666 Examples: 1667 1668 ``` 1669 def add(a,b): 1670 return a+b 1671 1672 addThunk=pamda.thunkify(add) 1673 1674 add(1,2) #=> 3 1675 addThunk(1,2) 1676 addThunk(1,2)() #=> 3 1677 1678 x=addThunk(1,2) 1679 x() #=> 3 1680 ``` 1681 1682 ``` 1683 @pamda.curry 1684 def add(a,b): 1685 return a+b 1686 1687 add(1,2) #=> 3 1688 1689 add.thunkify() 1690 1691 add(1,2) 1692 add(1,2)() #=> 3 1693 ``` 1694 """ 1695 fn = self.curry(fn) 1696 return fn.thunkify() 1697 1698 def unnest(self, data: list): 1699 """ 1700 Function: 1701 1702 - Removes one level of depth for all items in a list 1703 1704 Requires: 1705 1706 - `data`: 1707 - Type: list 1708 - What: A list of items to unnest by one level 1709 1710 Examples: 1711 1712 ``` 1713 data=['fe','fi',['fo',['fum']]] 1714 pamda.unnest( 1715 data=data 1716 ) #=> ['fe','fi','fo',['fum']] 1717 ``` 1718 """ 1719 if not len(data) > 0: 1720 raise Exception("Attempting to call `unnest` on an empty list") 1721 output = [] 1722 for i in data: 1723 if isinstance(i, list): 1724 output += i 1725 else: 1726 output.append(i) 1727 return output 1728 1729 def zip(self, a: list, b: list): 1730 """ 1731 Function: 1732 1733 - Creates a new list out of the two supplied by pairing up equally-positioned items from both lists 1734 1735 Requires: 1736 1737 - `a`: 1738 - Type: list 1739 - What: List of items to appear in new list first 1740 - `b`: 1741 - Type: list 1742 - What: List of items to appear in new list second 1743 1744 Example: 1745 1746 ``` 1747 a=['a','b'] 1748 b=[1,2] 1749 pamda.zip(a=a, b=b) #=> [['a',1],['b',2]] 1750 ``` 1751 """ 1752 return list(map(list, zip(a, b))) 1753 1754 def zipObj(self, a: list, b: list): 1755 """ 1756 Function: 1757 1758 - Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists 1759 - The first list represents keys and the second values 1760 1761 Requires: 1762 1763 - `a`: 1764 - Type: list 1765 - What: List of items to appear in new list first 1766 - `b`: 1767 - Type: list 1768 - What: List of items to appear in new list second 1769 1770 Example: 1771 1772 ``` 1773 a=['a','b'] 1774 b=[1,2] 1775 pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2} 1776 ``` 1777 """ 1778 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 try: 847 reduce(lambda x, y: x[y], path, data) 848 return True 849 except (KeyError, IndexError, TypeError): 850 return False 851 852 def hardRound(self, decimal_places: int, a: int | float): 853 """ 854 Function: 855 856 - Rounds to a set number of decimal places regardless of floating point math in python 857 858 Requires: 859 860 - `decimal_places`: 861 - Type: int 862 - What: The number of decimal places to round to 863 - Default: 0 864 - Notes: Negative numbers accepted (EG -1 rounds to the nearest 10) 865 - `a`: 866 - Type: int | float 867 - What: The number to round 868 869 Example: 870 871 ``` 872 a=12.345 873 pamda.hardRound(1,a) #=> 12.3 874 pamda.hardRound(-1,a) #=> 10 875 ``` 876 """ 877 return int(a * (10**decimal_places) + 0.5) / (10**decimal_places) 878 879 def head(self, data: list | str): 880 """ 881 Function: 882 883 - Picks the first item out of a list or string 884 885 Requires: 886 887 - `data`: 888 - Type: list | str 889 - What: A list or string 890 891 Example: 892 893 ``` 894 data=['fe','fi','fo','fum'] 895 pamda.first( 896 data=data 897 ) #=> fe 898 ``` 899 """ 900 if not isinstance(data, (list, str)): 901 raise Exception("`head` can only be called on a `str` or a `list`") 902 if not len(data) > 0: 903 raise Exception("Attempting to call `head` on an empty list or str") 904 return data[0] 905 906 def inc(self, a: int | float): 907 """ 908 Function: 909 910 - Increments a number by one 911 912 Requires: 913 914 - `a`: 915 - Type: int | float 916 - What: The number to increment 917 918 Example: 919 920 ``` 921 pamda.inc(42) #=> 43 922 ``` 923 """ 924 if not isinstance(a, (int, float)): 925 raise Exception("`a` must be an `int` or a `float`") 926 return a + 1 927 928 def intersection(self, a: list, b: list): 929 """ 930 Function: 931 932 - Combines two lists into a list of no duplicates composed of those elements common to both lists 933 934 Requires: 935 936 - `a`: 937 - Type: list 938 - What: List of items in which to look for an intersection 939 - `b`: 940 - Type: list 941 - What: List of items in which to look for an intersection 942 943 Example: 944 945 ``` 946 a=['a','b'] 947 b=['b','c'] 948 pamda.intersection(a=a, b=b) #=> ['b'] 949 ``` 950 """ 951 return list(set(a).intersection(set(b))) 952 953 def map(self, fn, data: list | dict): 954 """ 955 Function: 956 957 - Maps a function over a list or a dictionary 958 959 Requires: 960 961 - `fn`: 962 - Type: function | method 963 - What: The function or method to map over the list or dictionary 964 - Note: This function should have an arity of 1 965 - `data`: 966 - Type: list | dict 967 - What: The list or dict of items to map the function over 968 969 Examples: 970 971 ``` 972 data=[1,2,3] 973 pamda.map( 974 fn=pamda.inc, 975 data=data 976 ) 977 #=> [2,3,4] 978 ``` 979 980 ``` 981 data={'a':1,'b':2,'c':3} 982 pamda.map( 983 fn=pamda.inc, 984 data=data 985 ) 986 #=> {'a':2,'b':3,'c':4} 987 ``` 988 989 """ 990 # TODO: Check for efficiency gains 991 fn = self.curry(fn) 992 if fn.__arity__ != 1: 993 raise Exception("`map` `fn` must be unary (take one input)") 994 if not len(data) > 0: 995 raise Exception( 996 "`map` `data` has a length of 0 or is an empty dictionary, however it must have at least one element in it" 997 ) 998 if isinstance(data, dict): 999 return {key: fn(value) for key, value in data.items()} 1000 else: 1001 return [fn(i) for i in data] 1002 1003 def mean(self, data: list): 1004 """ 1005 Function: 1006 1007 - Calculates the mean of a given list 1008 1009 Requires: 1010 1011 - `data`: 1012 - Type: list of (floats | ints) 1013 - What: The list with wich to calculate the mean 1014 - Note: If the length of this list is 0, returns None 1015 1016 Example: 1017 1018 ``` 1019 data=[1,2,3] 1020 pamda.mean(data=data) 1021 #=> 2 1022 ``` 1023 1024 ``` 1025 data=[] 1026 pamda.mean(data=data) 1027 #=> None 1028 ``` 1029 """ 1030 if len(data) == 0: 1031 return None 1032 return sum(data) / len(data) 1033 1034 def median(self, data: list): 1035 """ 1036 Function: 1037 1038 - Calculates the median of a given list 1039 - If the length of the list is even, calculates the mean of the two central values 1040 1041 Requires: 1042 1043 - `data`: 1044 - Type: list of (floats | ints) 1045 - What: The list with wich to calculate the mean 1046 - Note: If the length of this list is 0, returns None 1047 1048 Examples: 1049 1050 ``` 1051 data=[7,2,8,9] 1052 pamda.median(data=data) 1053 #=> 7.5 1054 ``` 1055 1056 ``` 1057 data=[7,8,9] 1058 pamda.median(data=data) 1059 #=> 8 1060 ``` 1061 1062 ``` 1063 data=[] 1064 pamda.median(data=data) 1065 #=> None 1066 ``` 1067 """ 1068 if not isinstance(data, (list)): 1069 raise Exception("`median` `data` must be a list") 1070 length = len(data) 1071 if length == 0: 1072 return None 1073 data = sorted(data) 1074 if length % 2 == 0: 1075 return (data[int(length / 2)] + data[int(length / 2) - 1]) / 2 1076 return data[int(length / 2)] 1077 1078 def mergeDeep(self, update_data, data): 1079 """ 1080 Function: 1081 1082 - Recursively merges two nested dictionaries keeping all keys at each layer 1083 - Values from `update_data` are used when keys are present in both dictionaries 1084 1085 Requires: 1086 1087 - `update_data`: 1088 - Type: any 1089 - What: The new data that will take precedence during merging 1090 - `data`: 1091 - Type: any 1092 - What: The original data that will be merged into 1093 1094 Example: 1095 1096 ``` 1097 data={'a':{'b':{'c':'d'},'e':'f'}} 1098 update_data={'a':{'b':{'h':'i'},'e':'g'}} 1099 pamda.mergeDeep( 1100 update_data=update_data, 1101 data=data 1102 ) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}} 1103 ``` 1104 """ 1105 return __mergeDeep__(update_data, data) 1106 1107 def nest(self, path_keys: list, value_key: str, data: list): 1108 """ 1109 Function: 1110 1111 - Nests a list of dictionaries into a nested dictionary 1112 - Similar items are appended to a list in the end of the nested dictionary 1113 1114 Requires: 1115 1116 - `path_keys`: 1117 - Type: list of strs 1118 - What: The variables to pull from each item in data 1119 - Note: Used to build out the nested dicitonary 1120 - Note: Order matters as the nesting occurs in order of variable 1121 - `value_key`: 1122 - Type: str 1123 - What: The variable to add to the list at the end of the nested dictionary path 1124 - `data`: 1125 - Type: list of dicts 1126 - What: A list of dictionaries to use for nesting purposes 1127 1128 Example: 1129 1130 ``` 1131 data=[ 1132 {'x_1':'a','x_2':'b', 'output':'c'}, 1133 {'x_1':'a','x_2':'b', 'output':'d'}, 1134 {'x_1':'a','x_2':'e', 'output':'f'} 1135 ] 1136 pamda.nest( 1137 path_keys=['x_1','x_2'], 1138 value_key='output', 1139 data=data 1140 ) #=> {'a':{'b':['c','d'], 'e':['f']}} 1141 ``` 1142 """ 1143 if not isinstance(data, list): 1144 raise Exception("Attempting to `nest` an object that is not a list") 1145 if len(data) == 0: 1146 raise Exception("Attempting to `nest` from an empty list") 1147 nested_output = {} 1148 for item in self.groupKeys(keys=path_keys, data=data): 1149 nested_output = __assocPath__( 1150 path=__getKeyValues__(path_keys, item[0]), 1151 value=[i.get(value_key) for i in item], 1152 data=nested_output, 1153 ) 1154 return nested_output 1155 1156 def nestItem(self, path_keys: list, data: list): 1157 """ 1158 Function: 1159 1160 - Nests a list of dictionaries into a nested dictionary 1161 - Similar items are appended to a list in the end of the nested dictionary 1162 - Similar to `nest`, except no values are plucked for the aggregated list 1163 1164 Requires: 1165 1166 - `path_keys`: 1167 - Type: list of strs 1168 - What: The variables to pull from each item in data 1169 - Note: Used to build out the nested dicitonary 1170 - Note: Order matters as the nesting occurs in order of variable 1171 - `data`: 1172 - Type: list of dicts 1173 - What: A list of dictionaries to use for nesting purposes 1174 1175 Example: 1176 1177 ``` 1178 data=[ 1179 {'x_1':'a','x_2':'b'}, 1180 {'x_1':'a','x_2':'b'}, 1181 {'x_1':'a','x_2':'e'} 1182 ] 1183 pamda.nestItem 1184 path_keys=['x_1','x_2'], 1185 data=data 1186 ) 1187 #=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}} 1188 1189 ``` 1190 """ 1191 if not isinstance(data, list): 1192 raise Exception("Attempting to `nest` an object that is not a list") 1193 if len(data) == 0: 1194 raise Exception("Attempting to `nest` from an empty list") 1195 nested_output = {} 1196 for item in self.groupKeys(keys=path_keys, data=data): 1197 nested_output = __assocPath__( 1198 path=__getKeyValues__(path_keys, item[0]), 1199 value=item, 1200 data=nested_output, 1201 ) 1202 return nested_output 1203 1204 def path(self, path: list | str, data: dict): 1205 """ 1206 Function: 1207 1208 - Returns the value of a path within a nested dictionary or None if the path does not exist 1209 1210 Requires: 1211 1212 - `path`: 1213 - Type: list of strs | str 1214 - What: The path to pull given the data 1215 - Note: If a string is passed, assumes a single item path list with that string 1216 - `data`: 1217 - Type: dict 1218 - What: A dictionary to get the path from 1219 1220 Example: 1221 1222 ``` 1223 data={'a':{'b':1}} 1224 pamda.path(path=['a','b'], data=data) #=> 1 1225 ``` 1226 """ 1227 if isinstance(path, str): 1228 path = [path] 1229 return __pathOr__(None, path, data) 1230 1231 def pathOr(self, default, path: list | str, data: dict): 1232 """ 1233 Function: 1234 1235 - Returns the value of a path within a nested dictionary or a default value if that path does not exist 1236 1237 Requires: 1238 1239 - `default`: 1240 - Type: any 1241 - What: The object to return if the path does not exist 1242 - `path`: 1243 - Type: list of strs | str 1244 - What: The path to pull given the data 1245 - Note: If a string is passed, assumes a single item path list with that string 1246 - `data`: 1247 - Type: dict 1248 - What: A dictionary to get the path from 1249 1250 Example: 1251 1252 ``` 1253 data={'a':{'b':1}} 1254 pamda.path(default=2, path=['a','c'], data=data) #=> 2 1255 ``` 1256 """ 1257 if isinstance(path, str): 1258 path = [path] 1259 try: 1260 return reduce(lambda x, y: x[y], path, data) 1261 except (KeyError, IndexError, TypeError): 1262 return default 1263 1264 def pipe(self, fns: list, args: tuple, kwargs: dict): 1265 """ 1266 Function: 1267 1268 - Pipes data through n functions in order (left to right composition) and returns the output 1269 1270 Requires: 1271 1272 - `fns`: 1273 - Type: list of (functions | methods) 1274 - What: The list of functions and methods to pipe the data through 1275 - Notes: The first function in the list can be any arity (accepting any number of inputs) 1276 - Notes: Any further function in the list can only be unary (single input) 1277 - Notes: A function can be curried, but is not required to be 1278 - Notes: You may opt to curry functions and add inputs to make them unary 1279 - `args`: 1280 - Type: tuple 1281 - What: a tuple of positional arguments to pass to the first function in `fns` 1282 - `kwargs`: 1283 - Type: dict 1284 - What: a dictionary of keyword arguments to pass to the first function in `fns` 1285 1286 Examples: 1287 1288 ``` 1289 data=['abc','def'] 1290 pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c' 1291 pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c' 1292 ``` 1293 1294 ``` 1295 data={'a':{'b':'c'}} 1296 curriedPath=pamda.curry(pamda.path) 1297 pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c' 1298 ``` 1299 """ 1300 if len(fns) == 0: 1301 raise Exception("`fns` must be a list with at least one function") 1302 if self.getArity(fns[0]) == 0: 1303 raise Exception( 1304 "The first function in `fns` can have n arity (accepting n args), but this must be greater than 0." 1305 ) 1306 if not all([(self.getArity(fn) == 1) for fn in fns[1:]]): 1307 raise Exception( 1308 "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)." 1309 ) 1310 out = fns[0](*args, **kwargs) 1311 for fn in fns[1:]: 1312 out = fn(out) 1313 return out 1314 1315 def pivot(self, data: list[dict] | dict[Any, list]): 1316 """ 1317 Function: 1318 1319 - Pivots a list of dictionaries into a dictionary of lists 1320 - Pivots a dictionary of lists into a list of dictionaries 1321 1322 Requires: 1323 1324 - `data`: 1325 - Type: list of dicts | dict of lists 1326 - What: The data to pivot 1327 - Note: If a list of dictionaries is passed, all dictionaries must have the same keys 1328 - Note: If a dictionary of lists is passed, all lists must have the same length 1329 1330 Example: 1331 1332 ``` 1333 data=[ 1334 {'a':1,'b':2}, 1335 {'a':3,'b':4} 1336 ] 1337 pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]} 1338 1339 data={'a':[1,3],'b':[2,4]} 1340 pamda.pivot(data=data) 1341 #=> [ 1342 #=> {'a':1,'b':2}, 1343 #=> {'a':3,'b':4} 1344 #=> ] 1345 ``` 1346 """ 1347 if isinstance(data, list): 1348 return { 1349 key: [record[key] for record in data] for key in data[0].keys() 1350 } 1351 else: 1352 return [ 1353 {key: data[key][i] for key in data.keys()} 1354 for i in range(len(data[list(data.keys())[0]])) 1355 ] 1356 1357 def pluck(self, path: list | str, data: list): 1358 """ 1359 Function: 1360 1361 - Returns the values of a path within a list of nested dictionaries 1362 1363 Requires: 1364 1365 - `path`: 1366 - Type: list of strs 1367 - What: The path to pull given the data 1368 - Note: If a string is passed, assumes a single item path list with that string 1369 - `data`: 1370 - Type: list of dicts 1371 - What: A list of dictionaries to get the path from 1372 1373 Example: 1374 1375 ``` 1376 data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}] 1377 pamda.pluck(path=['a','b'], data=data) #=> [1,2] 1378 ``` 1379 """ 1380 if len(data) == 0: 1381 raise Exception("Attempting to pluck from an empty list") 1382 if isinstance(path, str): 1383 path = [path] 1384 return [__pathOr__(default=None, path=path, data=i) for i in data] 1385 1386 def pluckIf(self, fn, path: list | str, data: list): 1387 """ 1388 Function: 1389 1390 - Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value 1391 1392 Requires: 1393 1394 - `fn`: 1395 - Type: function 1396 - What: A function to take in each item in data and return a boolean 1397 - Note: Only items that return true are plucked 1398 - Note: Should be a unary function (take one input) 1399 - `path`: 1400 - Type: list of strs 1401 - What: The path to pull given the data 1402 - Note: If a string is passed, assumes a single item path list with that string 1403 - `data`: 1404 - Type: list of dicts 1405 - What: A list of dictionary to get the path from 1406 1407 Example: 1408 1409 ``` 1410 1411 data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}] 1412 pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d'] 1413 ``` 1414 """ 1415 if len(data) == 0: 1416 raise Exception("Attempting to pluck from an empty list") 1417 curried_fn = self.curry(fn) 1418 if curried_fn.__arity__ != 1: 1419 raise Exception( 1420 "`pluckIf` `fn` must have an arity of 1 (take one input)" 1421 ) 1422 if isinstance(path, str): 1423 path = [path] 1424 return [ 1425 __pathOr__(default=None, path=path, data=i) for i in data if fn(i) 1426 ] 1427 1428 def project(self, keys: list[str], data: list[dict]): 1429 """ 1430 Function: 1431 1432 - Returns a list of dictionaries with only the keys provided 1433 - Analogous to SQL's `SELECT` statement 1434 1435 Requires: 1436 1437 - `keys`: 1438 - Type: list of strs 1439 - What: The keys to select from each dictionary in the data list 1440 - `data`: 1441 - Type: list of dicts 1442 - What: The list of dictionaries to select from 1443 1444 Example: 1445 1446 ``` 1447 data=[ 1448 {'a':1,'b':2,'c':3}, 1449 {'a':4,'b':5,'c':6} 1450 ] 1451 pamda.project(keys=['a','c'], data=data) 1452 #=> [ 1453 #=> {'a':1,'c':3}, 1454 #=> {'a':4,'c':6} 1455 #=> ] 1456 ``` 1457 """ 1458 return [{key: record[key] for key in keys} for record in data] 1459 1460 def props(self, keys: list[str], data: dict): 1461 """ 1462 Function: 1463 1464 - Returns the values of a list of keys within a dictionary 1465 1466 Requires: 1467 1468 - `keys`: 1469 - Type: list of strs 1470 - What: The keys to pull given the data 1471 - `data`: 1472 - Type: dict 1473 - What: A dictionary to get the keys from 1474 1475 Example: 1476 ``` 1477 data={'a':1,'b':2,'c':3} 1478 pamda.props(keys=['a','c'], data=data) 1479 #=> [1,3] 1480 ``` 1481 """ 1482 return [data[key] for key in keys] 1483 1484 def reduce(self, fn, initial_accumulator, data: list): 1485 """ 1486 Function: 1487 1488 - Returns a single item by iterating a function starting with an accumulator over a list 1489 1490 Requires: 1491 1492 - `fn`: 1493 - Type: function | method 1494 - What: The function or method to reduce 1495 - Note: This function should have an arity of 2 (take two inputs) 1496 - Note: The first input should take the accumulator value 1497 - Note: The second input should take the data value 1498 -`initial_accumulator`: 1499 - Type: any 1500 - What: The initial item to pass into the function when starting the accumulation process 1501 - `data`: 1502 - Type: list 1503 - What: The list of items to iterate over 1504 1505 Example: 1506 1507 ``` 1508 data=[1,2,3,4] 1509 pamda.reduce( 1510 fn=pamda.add, 1511 initial_accumulator=0, 1512 data=data 1513 ) 1514 #=> 10 1515 1516 ``` 1517 """ 1518 fn = self.curry(fn) 1519 if fn.__arity__ != 2: 1520 raise Exception( 1521 "`reduce` `fn` must have an arity of 2 (take two inputs)" 1522 ) 1523 if not isinstance(data, (list)): 1524 raise Exception("`reduce` `data` must be a list") 1525 if not len(data) > 0: 1526 raise Exception( 1527 "`reduce` `data` has a length of 0, however it must have a length of at least 1" 1528 ) 1529 acc = initial_accumulator 1530 for i in data: 1531 acc = fn(acc, i) 1532 return acc 1533 1534 def safeDivide(self, denominator: int | float, a: int | float): 1535 """ 1536 Function: 1537 1538 - Forces division to work by enforcing a denominator of 1 if the provided denominator is zero 1539 1540 Requires: 1541 1542 - `denominator`: 1543 - Type: int | float 1544 - What: The denominator 1545 1546 - `a`: 1547 - Type: int | float 1548 - What: The numerator 1549 1550 Example: 1551 1552 ``` 1553 pamda.safeDivide(2,10) #=> 5 1554 pamda.safeDivide(0,10) #=> 10 1555 ``` 1556 """ 1557 return a / denominator if denominator != 0 else a 1558 1559 def safeDivideDefault( 1560 self, 1561 default_denominator: int | float, 1562 denominator: int | float, 1563 a: int | float, 1564 ): 1565 """ 1566 Function: 1567 1568 - Forces division to work by enforcing a non zero default denominator if the provided denominator is zero 1569 1570 Requires: 1571 1572 - `default_denominator`: 1573 - Type: int | float 1574 - What: A non zero denominator to use if denominator is zero 1575 - Default: 1 1576 - `denominator`: 1577 - Type: int | float 1578 - What: The denominator 1579 - `a`: 1580 - Type: int | float 1581 - What: The numerator 1582 1583 Example: 1584 1585 ``` 1586 pamda.safeDivideDefault(2,5,10) #=> 2 1587 pamda.safeDivideDefault(2,0,10) #=> 5 1588 ``` 1589 """ 1590 if default_denominator == 0: 1591 raise Exception( 1592 "`safeDivideDefault` `default_denominator` can not be 0" 1593 ) 1594 return a / denominator if denominator != 0 else a / default_denominator 1595 1596 def symmetricDifference(self, a: list, b: list): 1597 """ 1598 Function: 1599 1600 - Combines two lists into a list of no duplicates items present in one list but not the other 1601 1602 Requires: 1603 1604 - `a`: 1605 - Type: list 1606 - What: List of items in which to look for a difference 1607 - `b`: 1608 - Type: list 1609 - What: List of items in which to look for a difference 1610 1611 Example: 1612 1613 ``` 1614 a=['a','b'] 1615 b=['b','c'] 1616 pamda.symmetricDifference(a=a, b=b) #=> ['a','c'] 1617 ``` 1618 """ 1619 return list(set(a).difference(set(b))) + list(set(b).difference(set(a))) 1620 1621 def tail(self, data: list | str): 1622 """ 1623 Function: 1624 1625 - Picks the last item out of a list or string 1626 1627 Requires: 1628 1629 - `data`: 1630 - Type: list | str 1631 - What: A list or string 1632 1633 Example: 1634 1635 ``` 1636 data=['fe','fi','fo','fum'] 1637 pamda.tail( 1638 data=data 1639 ) #=> fum 1640 ``` 1641 """ 1642 if not len(data) > 0: 1643 raise Exception("Attempting to call `tail` on an empty list or str") 1644 return data[-1] 1645 1646 def thunkify(self, fn): 1647 """ 1648 Function: 1649 1650 - Creates a curried thunk out of a function 1651 - Evaluation of the thunk lazy and is delayed until called 1652 1653 Requires: 1654 1655 - `fn`: 1656 - Type: function | method 1657 - What: The function or method to thunkify 1658 - Note: Thunkified functions are automatically curried 1659 - Note: Class methods auto apply self during thunkify 1660 1661 Notes: 1662 1663 - Input functions are not thunkified in place 1664 - The returned function is a thunkified version of the input function 1665 - A curried function can be thunkified in place by calling fn.thunkify() 1666 1667 Examples: 1668 1669 ``` 1670 def add(a,b): 1671 return a+b 1672 1673 addThunk=pamda.thunkify(add) 1674 1675 add(1,2) #=> 3 1676 addThunk(1,2) 1677 addThunk(1,2)() #=> 3 1678 1679 x=addThunk(1,2) 1680 x() #=> 3 1681 ``` 1682 1683 ``` 1684 @pamda.curry 1685 def add(a,b): 1686 return a+b 1687 1688 add(1,2) #=> 3 1689 1690 add.thunkify() 1691 1692 add(1,2) 1693 add(1,2)() #=> 3 1694 ``` 1695 """ 1696 fn = self.curry(fn) 1697 return fn.thunkify() 1698 1699 def unnest(self, data: list): 1700 """ 1701 Function: 1702 1703 - Removes one level of depth for all items in a list 1704 1705 Requires: 1706 1707 - `data`: 1708 - Type: list 1709 - What: A list of items to unnest by one level 1710 1711 Examples: 1712 1713 ``` 1714 data=['fe','fi',['fo',['fum']]] 1715 pamda.unnest( 1716 data=data 1717 ) #=> ['fe','fi','fo',['fum']] 1718 ``` 1719 """ 1720 if not len(data) > 0: 1721 raise Exception("Attempting to call `unnest` on an empty list") 1722 output = [] 1723 for i in data: 1724 if isinstance(i, list): 1725 output += i 1726 else: 1727 output.append(i) 1728 return output 1729 1730 def zip(self, a: list, b: list): 1731 """ 1732 Function: 1733 1734 - Creates a new list out of the two supplied by pairing up equally-positioned items from both lists 1735 1736 Requires: 1737 1738 - `a`: 1739 - Type: list 1740 - What: List of items to appear in new list first 1741 - `b`: 1742 - Type: list 1743 - What: List of items to appear in new list second 1744 1745 Example: 1746 1747 ``` 1748 a=['a','b'] 1749 b=[1,2] 1750 pamda.zip(a=a, b=b) #=> [['a',1],['b',2]] 1751 ``` 1752 """ 1753 return list(map(list, zip(a, b))) 1754 1755 def zipObj(self, a: list, b: list): 1756 """ 1757 Function: 1758 1759 - Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists 1760 - The first list represents keys and the second values 1761 1762 Requires: 1763 1764 - `a`: 1765 - Type: list 1766 - What: List of items to appear in new list first 1767 - `b`: 1768 - Type: list 1769 - What: List of items to appear in new list second 1770 1771 Example: 1772 1773 ``` 1774 a=['a','b'] 1775 b=[1,2] 1776 pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2} 1777 ``` 1778 """ 1779 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:
Noneif 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
fnmust already be asynchronously running
Notes:
- See also
asyncRunandasyncWait - A thunkified function currently running asynchronously can call
asyncKillon itself - If a function has already finished running, calling
asyncKillon it will have no effect asyncKilldoes 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
fnmust have an arity of 0
Notes:
- To pass inputs to a function in asyncRun, first thunkify the function and pass all arguments before calling
asyncRunon it - To get the results of an
asyncRuncallasyncWait - To kill an
asyncRunmid process callasyncKill - A thunkified function with arity of 0 can call
asyncRunon 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
fnmust 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 try: 847 reduce(lambda x, y: x[y], path, data) 848 return True 849 except (KeyError, IndexError, TypeError): 850 return False
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
852 def hardRound(self, decimal_places: int, a: int | float): 853 """ 854 Function: 855 856 - Rounds to a set number of decimal places regardless of floating point math in python 857 858 Requires: 859 860 - `decimal_places`: 861 - Type: int 862 - What: The number of decimal places to round to 863 - Default: 0 864 - Notes: Negative numbers accepted (EG -1 rounds to the nearest 10) 865 - `a`: 866 - Type: int | float 867 - What: The number to round 868 869 Example: 870 871 ``` 872 a=12.345 873 pamda.hardRound(1,a) #=> 12.3 874 pamda.hardRound(-1,a) #=> 10 875 ``` 876 """ 877 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
879 def head(self, data: list | str): 880 """ 881 Function: 882 883 - Picks the first item out of a list or string 884 885 Requires: 886 887 - `data`: 888 - Type: list | str 889 - What: A list or string 890 891 Example: 892 893 ``` 894 data=['fe','fi','fo','fum'] 895 pamda.first( 896 data=data 897 ) #=> fe 898 ``` 899 """ 900 if not isinstance(data, (list, str)): 901 raise Exception("`head` can only be called on a `str` or a `list`") 902 if not len(data) > 0: 903 raise Exception("Attempting to call `head` on an empty list or str") 904 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
906 def inc(self, a: int | float): 907 """ 908 Function: 909 910 - Increments a number by one 911 912 Requires: 913 914 - `a`: 915 - Type: int | float 916 - What: The number to increment 917 918 Example: 919 920 ``` 921 pamda.inc(42) #=> 43 922 ``` 923 """ 924 if not isinstance(a, (int, float)): 925 raise Exception("`a` must be an `int` or a `float`") 926 return a + 1
Function:
- Increments a number by one
Requires:
a:- Type: int | float
- What: The number to increment
Example:
pamda.inc(42) #=> 43
928 def intersection(self, a: list, b: list): 929 """ 930 Function: 931 932 - Combines two lists into a list of no duplicates composed of those elements common to both lists 933 934 Requires: 935 936 - `a`: 937 - Type: list 938 - What: List of items in which to look for an intersection 939 - `b`: 940 - Type: list 941 - What: List of items in which to look for an intersection 942 943 Example: 944 945 ``` 946 a=['a','b'] 947 b=['b','c'] 948 pamda.intersection(a=a, b=b) #=> ['b'] 949 ``` 950 """ 951 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']
953 def map(self, fn, data: list | dict): 954 """ 955 Function: 956 957 - Maps a function over a list or a dictionary 958 959 Requires: 960 961 - `fn`: 962 - Type: function | method 963 - What: The function or method to map over the list or dictionary 964 - Note: This function should have an arity of 1 965 - `data`: 966 - Type: list | dict 967 - What: The list or dict of items to map the function over 968 969 Examples: 970 971 ``` 972 data=[1,2,3] 973 pamda.map( 974 fn=pamda.inc, 975 data=data 976 ) 977 #=> [2,3,4] 978 ``` 979 980 ``` 981 data={'a':1,'b':2,'c':3} 982 pamda.map( 983 fn=pamda.inc, 984 data=data 985 ) 986 #=> {'a':2,'b':3,'c':4} 987 ``` 988 989 """ 990 # TODO: Check for efficiency gains 991 fn = self.curry(fn) 992 if fn.__arity__ != 1: 993 raise Exception("`map` `fn` must be unary (take one input)") 994 if not len(data) > 0: 995 raise Exception( 996 "`map` `data` has a length of 0 or is an empty dictionary, however it must have at least one element in it" 997 ) 998 if isinstance(data, dict): 999 return {key: fn(value) for key, value in data.items()} 1000 else: 1001 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}
1003 def mean(self, data: list): 1004 """ 1005 Function: 1006 1007 - Calculates the mean of a given list 1008 1009 Requires: 1010 1011 - `data`: 1012 - Type: list of (floats | ints) 1013 - What: The list with wich to calculate the mean 1014 - Note: If the length of this list is 0, returns None 1015 1016 Example: 1017 1018 ``` 1019 data=[1,2,3] 1020 pamda.mean(data=data) 1021 #=> 2 1022 ``` 1023 1024 ``` 1025 data=[] 1026 pamda.mean(data=data) 1027 #=> None 1028 ``` 1029 """ 1030 if len(data) == 0: 1031 return None 1032 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
1034 def median(self, data: list): 1035 """ 1036 Function: 1037 1038 - Calculates the median of a given list 1039 - If the length of the list is even, calculates the mean of the two central values 1040 1041 Requires: 1042 1043 - `data`: 1044 - Type: list of (floats | ints) 1045 - What: The list with wich to calculate the mean 1046 - Note: If the length of this list is 0, returns None 1047 1048 Examples: 1049 1050 ``` 1051 data=[7,2,8,9] 1052 pamda.median(data=data) 1053 #=> 7.5 1054 ``` 1055 1056 ``` 1057 data=[7,8,9] 1058 pamda.median(data=data) 1059 #=> 8 1060 ``` 1061 1062 ``` 1063 data=[] 1064 pamda.median(data=data) 1065 #=> None 1066 ``` 1067 """ 1068 if not isinstance(data, (list)): 1069 raise Exception("`median` `data` must be a list") 1070 length = len(data) 1071 if length == 0: 1072 return None 1073 data = sorted(data) 1074 if length % 2 == 0: 1075 return (data[int(length / 2)] + data[int(length / 2) - 1]) / 2 1076 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
1078 def mergeDeep(self, update_data, data): 1079 """ 1080 Function: 1081 1082 - Recursively merges two nested dictionaries keeping all keys at each layer 1083 - Values from `update_data` are used when keys are present in both dictionaries 1084 1085 Requires: 1086 1087 - `update_data`: 1088 - Type: any 1089 - What: The new data that will take precedence during merging 1090 - `data`: 1091 - Type: any 1092 - What: The original data that will be merged into 1093 1094 Example: 1095 1096 ``` 1097 data={'a':{'b':{'c':'d'},'e':'f'}} 1098 update_data={'a':{'b':{'h':'i'},'e':'g'}} 1099 pamda.mergeDeep( 1100 update_data=update_data, 1101 data=data 1102 ) #=> {'a':{'b':{'c':'d','h':'i'},'e':'g'}} 1103 ``` 1104 """ 1105 return __mergeDeep__(update_data, data)
Function:
- Recursively merges two nested dictionaries keeping all keys at each layer
- Values from
update_dataare 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'}}
1107 def nest(self, path_keys: list, value_key: str, data: list): 1108 """ 1109 Function: 1110 1111 - Nests a list of dictionaries into a nested dictionary 1112 - Similar items are appended to a list in the end of the nested dictionary 1113 1114 Requires: 1115 1116 - `path_keys`: 1117 - Type: list of strs 1118 - What: The variables to pull from each item in data 1119 - Note: Used to build out the nested dicitonary 1120 - Note: Order matters as the nesting occurs in order of variable 1121 - `value_key`: 1122 - Type: str 1123 - What: The variable to add to the list at the end of the nested dictionary path 1124 - `data`: 1125 - Type: list of dicts 1126 - What: A list of dictionaries to use for nesting purposes 1127 1128 Example: 1129 1130 ``` 1131 data=[ 1132 {'x_1':'a','x_2':'b', 'output':'c'}, 1133 {'x_1':'a','x_2':'b', 'output':'d'}, 1134 {'x_1':'a','x_2':'e', 'output':'f'} 1135 ] 1136 pamda.nest( 1137 path_keys=['x_1','x_2'], 1138 value_key='output', 1139 data=data 1140 ) #=> {'a':{'b':['c','d'], 'e':['f']}} 1141 ``` 1142 """ 1143 if not isinstance(data, list): 1144 raise Exception("Attempting to `nest` an object that is not a list") 1145 if len(data) == 0: 1146 raise Exception("Attempting to `nest` from an empty list") 1147 nested_output = {} 1148 for item in self.groupKeys(keys=path_keys, data=data): 1149 nested_output = __assocPath__( 1150 path=__getKeyValues__(path_keys, item[0]), 1151 value=[i.get(value_key) for i in item], 1152 data=nested_output, 1153 ) 1154 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']}}
1156 def nestItem(self, path_keys: list, data: list): 1157 """ 1158 Function: 1159 1160 - Nests a list of dictionaries into a nested dictionary 1161 - Similar items are appended to a list in the end of the nested dictionary 1162 - Similar to `nest`, except no values are plucked for the aggregated list 1163 1164 Requires: 1165 1166 - `path_keys`: 1167 - Type: list of strs 1168 - What: The variables to pull from each item in data 1169 - Note: Used to build out the nested dicitonary 1170 - Note: Order matters as the nesting occurs in order of variable 1171 - `data`: 1172 - Type: list of dicts 1173 - What: A list of dictionaries to use for nesting purposes 1174 1175 Example: 1176 1177 ``` 1178 data=[ 1179 {'x_1':'a','x_2':'b'}, 1180 {'x_1':'a','x_2':'b'}, 1181 {'x_1':'a','x_2':'e'} 1182 ] 1183 pamda.nestItem 1184 path_keys=['x_1','x_2'], 1185 data=data 1186 ) 1187 #=> {'a': {'b': [{'x_1': 'a', 'x_2': 'b'}, {'x_1': 'a', 'x_2': 'b'}], 'e': [{'x_1': 'a', 'x_2': 'e'}]}} 1188 1189 ``` 1190 """ 1191 if not isinstance(data, list): 1192 raise Exception("Attempting to `nest` an object that is not a list") 1193 if len(data) == 0: 1194 raise Exception("Attempting to `nest` from an empty list") 1195 nested_output = {} 1196 for item in self.groupKeys(keys=path_keys, data=data): 1197 nested_output = __assocPath__( 1198 path=__getKeyValues__(path_keys, item[0]), 1199 value=item, 1200 data=nested_output, 1201 ) 1202 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'}]}}
1204 def path(self, path: list | str, data: dict): 1205 """ 1206 Function: 1207 1208 - Returns the value of a path within a nested dictionary or None if the path does not exist 1209 1210 Requires: 1211 1212 - `path`: 1213 - Type: list of strs | str 1214 - What: The path to pull given the data 1215 - Note: If a string is passed, assumes a single item path list with that string 1216 - `data`: 1217 - Type: dict 1218 - What: A dictionary to get the path from 1219 1220 Example: 1221 1222 ``` 1223 data={'a':{'b':1}} 1224 pamda.path(path=['a','b'], data=data) #=> 1 1225 ``` 1226 """ 1227 if isinstance(path, str): 1228 path = [path] 1229 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
1231 def pathOr(self, default, path: list | str, data: dict): 1232 """ 1233 Function: 1234 1235 - Returns the value of a path within a nested dictionary or a default value if that path does not exist 1236 1237 Requires: 1238 1239 - `default`: 1240 - Type: any 1241 - What: The object to return if the path does not exist 1242 - `path`: 1243 - Type: list of strs | str 1244 - What: The path to pull given the data 1245 - Note: If a string is passed, assumes a single item path list with that string 1246 - `data`: 1247 - Type: dict 1248 - What: A dictionary to get the path from 1249 1250 Example: 1251 1252 ``` 1253 data={'a':{'b':1}} 1254 pamda.path(default=2, path=['a','c'], data=data) #=> 2 1255 ``` 1256 """ 1257 if isinstance(path, str): 1258 path = [path] 1259 try: 1260 return reduce(lambda x, y: x[y], path, data) 1261 except (KeyError, IndexError, TypeError): 1262 return default
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
1264 def pipe(self, fns: list, args: tuple, kwargs: dict): 1265 """ 1266 Function: 1267 1268 - Pipes data through n functions in order (left to right composition) and returns the output 1269 1270 Requires: 1271 1272 - `fns`: 1273 - Type: list of (functions | methods) 1274 - What: The list of functions and methods to pipe the data through 1275 - Notes: The first function in the list can be any arity (accepting any number of inputs) 1276 - Notes: Any further function in the list can only be unary (single input) 1277 - Notes: A function can be curried, but is not required to be 1278 - Notes: You may opt to curry functions and add inputs to make them unary 1279 - `args`: 1280 - Type: tuple 1281 - What: a tuple of positional arguments to pass to the first function in `fns` 1282 - `kwargs`: 1283 - Type: dict 1284 - What: a dictionary of keyword arguments to pass to the first function in `fns` 1285 1286 Examples: 1287 1288 ``` 1289 data=['abc','def'] 1290 pamda.pipe(fns=[pamda.head, pamda.tail], args=(data), kwargs={}) #=> 'c' 1291 pamda.pipe(fns=[pamda.head, pamda.tail], args=(), kwargs={'data':data}) #=> 'c' 1292 ``` 1293 1294 ``` 1295 data={'a':{'b':'c'}} 1296 curriedPath=pamda.curry(pamda.path) 1297 pamda.pipe(fns=[curriedPath('a'), curriedPath('b')], args=(), kwargs={'data':data}) #=> 'c' 1298 ``` 1299 """ 1300 if len(fns) == 0: 1301 raise Exception("`fns` must be a list with at least one function") 1302 if self.getArity(fns[0]) == 0: 1303 raise Exception( 1304 "The first function in `fns` can have n arity (accepting n args), but this must be greater than 0." 1305 ) 1306 if not all([(self.getArity(fn) == 1) for fn in fns[1:]]): 1307 raise Exception( 1308 "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)." 1309 ) 1310 out = fns[0](*args, **kwargs) 1311 for fn in fns[1:]: 1312 out = fn(out) 1313 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'
1315 def pivot(self, data: list[dict] | dict[Any, list]): 1316 """ 1317 Function: 1318 1319 - Pivots a list of dictionaries into a dictionary of lists 1320 - Pivots a dictionary of lists into a list of dictionaries 1321 1322 Requires: 1323 1324 - `data`: 1325 - Type: list of dicts | dict of lists 1326 - What: The data to pivot 1327 - Note: If a list of dictionaries is passed, all dictionaries must have the same keys 1328 - Note: If a dictionary of lists is passed, all lists must have the same length 1329 1330 Example: 1331 1332 ``` 1333 data=[ 1334 {'a':1,'b':2}, 1335 {'a':3,'b':4} 1336 ] 1337 pamda.pivot(data=data) #=> {'a':[1,3],'b':[2,4]} 1338 1339 data={'a':[1,3],'b':[2,4]} 1340 pamda.pivot(data=data) 1341 #=> [ 1342 #=> {'a':1,'b':2}, 1343 #=> {'a':3,'b':4} 1344 #=> ] 1345 ``` 1346 """ 1347 if isinstance(data, list): 1348 return { 1349 key: [record[key] for record in data] for key in data[0].keys() 1350 } 1351 else: 1352 return [ 1353 {key: data[key][i] for key in data.keys()} 1354 for i in range(len(data[list(data.keys())[0]])) 1355 ]
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}
#=> ]
1357 def pluck(self, path: list | str, data: list): 1358 """ 1359 Function: 1360 1361 - Returns the values of a path within a list of nested dictionaries 1362 1363 Requires: 1364 1365 - `path`: 1366 - Type: list of strs 1367 - What: The path to pull given the data 1368 - Note: If a string is passed, assumes a single item path list with that string 1369 - `data`: 1370 - Type: list of dicts 1371 - What: A list of dictionaries to get the path from 1372 1373 Example: 1374 1375 ``` 1376 data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}] 1377 pamda.pluck(path=['a','b'], data=data) #=> [1,2] 1378 ``` 1379 """ 1380 if len(data) == 0: 1381 raise Exception("Attempting to pluck from an empty list") 1382 if isinstance(path, str): 1383 path = [path] 1384 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]
1386 def pluckIf(self, fn, path: list | str, data: list): 1387 """ 1388 Function: 1389 1390 - Returns the values of a path within a list of nested dictionaries if a path in those same dictionaries matches a value 1391 1392 Requires: 1393 1394 - `fn`: 1395 - Type: function 1396 - What: A function to take in each item in data and return a boolean 1397 - Note: Only items that return true are plucked 1398 - Note: Should be a unary function (take one input) 1399 - `path`: 1400 - Type: list of strs 1401 - What: The path to pull given the data 1402 - Note: If a string is passed, assumes a single item path list with that string 1403 - `data`: 1404 - Type: list of dicts 1405 - What: A list of dictionary to get the path from 1406 1407 Example: 1408 1409 ``` 1410 1411 data=[{'a':{'b':1, 'c':'d'}},{'a':{'b':2, 'c':'e'}}] 1412 pamda.pluck(fn:lambda x: x['a']['b']==1, path=['a','c'], data=data) #=> ['d'] 1413 ``` 1414 """ 1415 if len(data) == 0: 1416 raise Exception("Attempting to pluck from an empty list") 1417 curried_fn = self.curry(fn) 1418 if curried_fn.__arity__ != 1: 1419 raise Exception( 1420 "`pluckIf` `fn` must have an arity of 1 (take one input)" 1421 ) 1422 if isinstance(path, str): 1423 path = [path] 1424 return [ 1425 __pathOr__(default=None, path=path, data=i) for i in data if fn(i) 1426 ]
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']
1428 def project(self, keys: list[str], data: list[dict]): 1429 """ 1430 Function: 1431 1432 - Returns a list of dictionaries with only the keys provided 1433 - Analogous to SQL's `SELECT` statement 1434 1435 Requires: 1436 1437 - `keys`: 1438 - Type: list of strs 1439 - What: The keys to select from each dictionary in the data list 1440 - `data`: 1441 - Type: list of dicts 1442 - What: The list of dictionaries to select from 1443 1444 Example: 1445 1446 ``` 1447 data=[ 1448 {'a':1,'b':2,'c':3}, 1449 {'a':4,'b':5,'c':6} 1450 ] 1451 pamda.project(keys=['a','c'], data=data) 1452 #=> [ 1453 #=> {'a':1,'c':3}, 1454 #=> {'a':4,'c':6} 1455 #=> ] 1456 ``` 1457 """ 1458 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
SELECTstatement
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}
#=> ]
1460 def props(self, keys: list[str], data: dict): 1461 """ 1462 Function: 1463 1464 - Returns the values of a list of keys within a dictionary 1465 1466 Requires: 1467 1468 - `keys`: 1469 - Type: list of strs 1470 - What: The keys to pull given the data 1471 - `data`: 1472 - Type: dict 1473 - What: A dictionary to get the keys from 1474 1475 Example: 1476 ``` 1477 data={'a':1,'b':2,'c':3} 1478 pamda.props(keys=['a','c'], data=data) 1479 #=> [1,3] 1480 ``` 1481 """ 1482 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]
1484 def reduce(self, fn, initial_accumulator, data: list): 1485 """ 1486 Function: 1487 1488 - Returns a single item by iterating a function starting with an accumulator over a list 1489 1490 Requires: 1491 1492 - `fn`: 1493 - Type: function | method 1494 - What: The function or method to reduce 1495 - Note: This function should have an arity of 2 (take two inputs) 1496 - Note: The first input should take the accumulator value 1497 - Note: The second input should take the data value 1498 -`initial_accumulator`: 1499 - Type: any 1500 - What: The initial item to pass into the function when starting the accumulation process 1501 - `data`: 1502 - Type: list 1503 - What: The list of items to iterate over 1504 1505 Example: 1506 1507 ``` 1508 data=[1,2,3,4] 1509 pamda.reduce( 1510 fn=pamda.add, 1511 initial_accumulator=0, 1512 data=data 1513 ) 1514 #=> 10 1515 1516 ``` 1517 """ 1518 fn = self.curry(fn) 1519 if fn.__arity__ != 2: 1520 raise Exception( 1521 "`reduce` `fn` must have an arity of 2 (take two inputs)" 1522 ) 1523 if not isinstance(data, (list)): 1524 raise Exception("`reduce` `data` must be a list") 1525 if not len(data) > 0: 1526 raise Exception( 1527 "`reduce` `data` has a length of 0, however it must have a length of at least 1" 1528 ) 1529 acc = initial_accumulator 1530 for i in data: 1531 acc = fn(acc, i) 1532 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
1534 def safeDivide(self, denominator: int | float, a: int | float): 1535 """ 1536 Function: 1537 1538 - Forces division to work by enforcing a denominator of 1 if the provided denominator is zero 1539 1540 Requires: 1541 1542 - `denominator`: 1543 - Type: int | float 1544 - What: The denominator 1545 1546 - `a`: 1547 - Type: int | float 1548 - What: The numerator 1549 1550 Example: 1551 1552 ``` 1553 pamda.safeDivide(2,10) #=> 5 1554 pamda.safeDivide(0,10) #=> 10 1555 ``` 1556 """ 1557 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
1559 def safeDivideDefault( 1560 self, 1561 default_denominator: int | float, 1562 denominator: int | float, 1563 a: int | float, 1564 ): 1565 """ 1566 Function: 1567 1568 - Forces division to work by enforcing a non zero default denominator if the provided denominator is zero 1569 1570 Requires: 1571 1572 - `default_denominator`: 1573 - Type: int | float 1574 - What: A non zero denominator to use if denominator is zero 1575 - Default: 1 1576 - `denominator`: 1577 - Type: int | float 1578 - What: The denominator 1579 - `a`: 1580 - Type: int | float 1581 - What: The numerator 1582 1583 Example: 1584 1585 ``` 1586 pamda.safeDivideDefault(2,5,10) #=> 2 1587 pamda.safeDivideDefault(2,0,10) #=> 5 1588 ``` 1589 """ 1590 if default_denominator == 0: 1591 raise Exception( 1592 "`safeDivideDefault` `default_denominator` can not be 0" 1593 ) 1594 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
1596 def symmetricDifference(self, a: list, b: list): 1597 """ 1598 Function: 1599 1600 - Combines two lists into a list of no duplicates items present in one list but not the other 1601 1602 Requires: 1603 1604 - `a`: 1605 - Type: list 1606 - What: List of items in which to look for a difference 1607 - `b`: 1608 - Type: list 1609 - What: List of items in which to look for a difference 1610 1611 Example: 1612 1613 ``` 1614 a=['a','b'] 1615 b=['b','c'] 1616 pamda.symmetricDifference(a=a, b=b) #=> ['a','c'] 1617 ``` 1618 """ 1619 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']
1621 def tail(self, data: list | str): 1622 """ 1623 Function: 1624 1625 - Picks the last item out of a list or string 1626 1627 Requires: 1628 1629 - `data`: 1630 - Type: list | str 1631 - What: A list or string 1632 1633 Example: 1634 1635 ``` 1636 data=['fe','fi','fo','fum'] 1637 pamda.tail( 1638 data=data 1639 ) #=> fum 1640 ``` 1641 """ 1642 if not len(data) > 0: 1643 raise Exception("Attempting to call `tail` on an empty list or str") 1644 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
1646 def thunkify(self, fn): 1647 """ 1648 Function: 1649 1650 - Creates a curried thunk out of a function 1651 - Evaluation of the thunk lazy and is delayed until called 1652 1653 Requires: 1654 1655 - `fn`: 1656 - Type: function | method 1657 - What: The function or method to thunkify 1658 - Note: Thunkified functions are automatically curried 1659 - Note: Class methods auto apply self during thunkify 1660 1661 Notes: 1662 1663 - Input functions are not thunkified in place 1664 - The returned function is a thunkified version of the input function 1665 - A curried function can be thunkified in place by calling fn.thunkify() 1666 1667 Examples: 1668 1669 ``` 1670 def add(a,b): 1671 return a+b 1672 1673 addThunk=pamda.thunkify(add) 1674 1675 add(1,2) #=> 3 1676 addThunk(1,2) 1677 addThunk(1,2)() #=> 3 1678 1679 x=addThunk(1,2) 1680 x() #=> 3 1681 ``` 1682 1683 ``` 1684 @pamda.curry 1685 def add(a,b): 1686 return a+b 1687 1688 add(1,2) #=> 3 1689 1690 add.thunkify() 1691 1692 add(1,2) 1693 add(1,2)() #=> 3 1694 ``` 1695 """ 1696 fn = self.curry(fn) 1697 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
1699 def unnest(self, data: list): 1700 """ 1701 Function: 1702 1703 - Removes one level of depth for all items in a list 1704 1705 Requires: 1706 1707 - `data`: 1708 - Type: list 1709 - What: A list of items to unnest by one level 1710 1711 Examples: 1712 1713 ``` 1714 data=['fe','fi',['fo',['fum']]] 1715 pamda.unnest( 1716 data=data 1717 ) #=> ['fe','fi','fo',['fum']] 1718 ``` 1719 """ 1720 if not len(data) > 0: 1721 raise Exception("Attempting to call `unnest` on an empty list") 1722 output = [] 1723 for i in data: 1724 if isinstance(i, list): 1725 output += i 1726 else: 1727 output.append(i) 1728 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']]
1730 def zip(self, a: list, b: list): 1731 """ 1732 Function: 1733 1734 - Creates a new list out of the two supplied by pairing up equally-positioned items from both lists 1735 1736 Requires: 1737 1738 - `a`: 1739 - Type: list 1740 - What: List of items to appear in new list first 1741 - `b`: 1742 - Type: list 1743 - What: List of items to appear in new list second 1744 1745 Example: 1746 1747 ``` 1748 a=['a','b'] 1749 b=[1,2] 1750 pamda.zip(a=a, b=b) #=> [['a',1],['b',2]] 1751 ``` 1752 """ 1753 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]]
1755 def zipObj(self, a: list, b: list): 1756 """ 1757 Function: 1758 1759 - Creates a new dict out of two supplied lists by pairing up equally-positioned items from both lists 1760 - The first list represents keys and the second values 1761 1762 Requires: 1763 1764 - `a`: 1765 - Type: list 1766 - What: List of items to appear in new list first 1767 - `b`: 1768 - Type: list 1769 - What: List of items to appear in new list second 1770 1771 Example: 1772 1773 ``` 1774 a=['a','b'] 1775 b=[1,2] 1776 pamda.zipObj(a=a, b=b) #=> {'a':1, 'b':2} 1777 ``` 1778 """ 1779 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}