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