zakat
xxx
_____ _ _ _ _ _
|__ /__ _| | ____ _| |_ | | (_) |__ _ __ __ _ _ __ _ _
/ // _| |/ / _
| __| | | | | '_ \| '__/ _` | '__| | | |
/ /| (_| | < (_| | |_ | |___| | |_) | | | (_| | | | |_| |
/______,_|_|___,_|__| |_____|_|_.__/|_| __,_|_| __, |
|___/
"رَبَّنَا افْتَحْ بَيْنَنَا وَبَيْنَ قَوْمِنَا بِالْحَقِّ وَأَنتَ خَيْرُ الْفَاتِحِينَ (89)" -- سورة الأعراف ... Never Trust, Always Verify ...
This file provides the ZakatLibrary classes, functions for tracking and calculating Zakat.
1""" 2 _____ _ _ _ _ _ 3|__ /__ _| | ____ _| |_ | | (_) |__ _ __ __ _ _ __ _ _ 4 / // _` | |/ / _` | __| | | | | '_ \| '__/ _` | '__| | | | 5 / /| (_| | < (_| | |_ | |___| | |_) | | | (_| | | | |_| | 6/____\__,_|_|\_\__,_|\__| |_____|_|_.__/|_| \__,_|_| \__, | 7 |___/ 8 9"رَبَّنَا افْتَحْ بَيْنَنَا وَبَيْنَ قَوْمِنَا بِالْحَقِّ وَأَنتَ خَيْرُ الْفَاتِحِينَ (89)" -- سورة الأعراف 10... Never Trust, Always Verify ... 11 12This file provides the ZakatLibrary classes, functions for tracking and calculating Zakat. 13""" 14# Importing necessary classes and functions from the main module 15from .zakat_tracker import ( 16 ZakatTracker, 17 Action, 18 JSONEncoder, 19 MathOperation, 20) 21 22# Version information for the module 23__version__ = ZakatTracker.Version() 24__all__ = [ 25 "ZakatTracker", 26 "Action", 27 "JSONEncoder", 28 "MathOperation", 29]
91class ZakatTracker: 92 """ 93 A class for tracking and calculating Zakat. 94 95 This class provides functionalities for recording transactions, calculating Zakat due, 96 and managing account balances. It also offers features like importing transactions from 97 CSV files, exporting data to JSON format, and saving/loading the tracker state. 98 99 The `ZakatTracker` class is designed to handle both positive and negative transactions, 100 allowing for flexible tracking of financial activities related to Zakat. It also supports 101 the concept of a "nisab" (minimum threshold for Zakat) and a "haul" (complete one year for Transaction) can calculate Zakat due 102 based on the current silver price. 103 104 The class uses a pickle file as its database to persist the tracker state, 105 ensuring data integrity across sessions. It also provides options for enabling or 106 disabling history tracking, allowing users to choose their preferred level of detail. 107 108 In addition, the `ZakatTracker` class includes various helper methods like 109 `time`, `time_to_datetime`, `lock`, `free`, `recall`, `export_json`, 110 and more. These methods provide additional functionalities and flexibility 111 for interacting with and managing the Zakat tracker. 112 113 Attributes: 114 ZakatCut (function): A function to calculate the Zakat percentage. 115 TimeCycle (function): A function to determine the time cycle for Zakat. 116 Nisab (function): A function to calculate the Nisab based on the silver price. 117 Version (str): The version of the ZakatTracker class. 118 119 Data Structure: 120 The ZakatTracker class utilizes a nested dictionary structure called "_vault" to store and manage data. 121 122 _vault (dict): 123 - account (dict): 124 - {account_number} (dict): 125 - balance (int): The current balance of the account. 126 - box (dict): A dictionary storing transaction details. 127 - {timestamp} (dict): 128 - capital (int): The initial amount of the transaction. 129 - count (int): The number of times Zakat has been calculated for this transaction. 130 - last (int): The timestamp of the last Zakat calculation. 131 - rest (int): The remaining amount after Zakat deductions and withdrawal. 132 - total (int): The total Zakat deducted from this transaction. 133 - count (int): The total number of transactions for the account. 134 - log (dict): A dictionary storing transaction logs. 135 - {timestamp} (dict): 136 - value (int): The transaction amount (positive or negative). 137 - desc (str): The description of the transaction. 138 - file (dict): A dictionary storing file references associated with the transaction. 139 - zakatable (bool): Indicates whether the account is subject to Zakat. 140 - history (dict): 141 - {timestamp} (list): A list of dictionaries storing the history of actions performed. 142 - {action_dict} (dict): 143 - action (Action): The type of action (CREATE, TRACK, LOG, SUB, ADD_FILE, REMOVE_FILE, BOX_TRANSFER, EXCHANGE, REPORT, ZAKAT). 144 - account (str): The account number associated with the action. 145 - ref (int): The reference number of the transaction. 146 - file (int): The reference number of the file (if applicable). 147 - key (str): The key associated with the action (e.g., 'rest', 'total'). 148 - value (int): The value associated with the action. 149 - math (MathOperation): The mathematical operation performed (if applicable). 150 - lock (int or None): The timestamp indicating the current lock status (None if not locked). 151 - report (dict): 152 - {timestamp} (tuple): A tuple storing Zakat report details. 153 154 """ 155 156 # Hybrid Constants 157 ZakatCut = lambda x: 0.025*x # Zakat Cut in one Lunar Year 158 TimeCycle = lambda days = 355: int(60*60*24*days*1e9) # Lunar Year in nanoseconds 159 Nisab = lambda x: 595*x # Silver Price in Local currency value 160 Version = lambda : '0.2.3' 161 162 def __init__(self, db_path: str = "zakat.pickle", history_mode: bool = True): 163 """ 164 Initialize ZakatTracker with database path and history mode. 165 166 Parameters: 167 db_path (str): The path to the database file. Default is "zakat.pickle". 168 history_mode (bool): The mode for tracking history. Default is True. 169 170 Returns: 171 None 172 """ 173 self.reset() 174 self._history(history_mode) 175 self.path(db_path) 176 self.load() 177 178 def path(self, _path: str = None) -> str: 179 """ 180 Set or get the database path. 181 182 Parameters: 183 _path (str): The path to the database file. If not provided, it returns the current path. 184 185 Returns: 186 str: The current database path. 187 """ 188 if _path is not None: 189 self._vault_path = _path 190 return self._vault_path 191 192 def _history(self, status: bool = None) -> bool: 193 """ 194 Enable or disable history tracking. 195 196 Parameters: 197 status (bool): The status of history tracking. Default is True. 198 199 Returns: 200 None 201 """ 202 if status is not None: 203 self._history_mode = status 204 return self._history_mode 205 206 def reset(self) -> None: 207 """ 208 Reset the internal data structure to its initial state. 209 210 Parameters: 211 None 212 213 Returns: 214 None 215 """ 216 self._vault = {} 217 self._vault['account'] = {} 218 self._vault['exchange'] = {} 219 self._vault['history'] = {} 220 self._vault['lock'] = None 221 self._vault['report'] = {} 222 223 @staticmethod 224 def time(now: datetime = None) -> int: 225 """ 226 Generates a timestamp based on the provided datetime object or the current datetime. 227 228 Parameters: 229 now (datetime, optional): The datetime object to generate the timestamp from. If not provided, the current datetime is used. 230 231 Returns: 232 int: The timestamp in positive nanoseconds since the Unix epoch (January 1, 1970), before 1970 will return in negative until 1000AD. 233 """ 234 if now is None: 235 now = datetime.datetime.now() 236 ordinal_day = now.toordinal() 237 ns_in_day = (now - now.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() * 10**9 238 return int((ordinal_day - 719_163) * 86_400_000_000_000 + ns_in_day) 239 240 @staticmethod 241 def time_to_datetime(ordinal_ns: int) -> datetime: 242 """ 243 Converts an ordinal number (number of days since 1000-01-01) to a datetime object. 244 245 Parameters: 246 ordinal_ns (int): The ordinal number of days since 1000-01-01. 247 248 Returns: 249 datetime: The corresponding datetime object. 250 """ 251 ordinal_day = ordinal_ns // 86_400_000_000_000 + 719_163 252 ns_in_day = ordinal_ns % 86_400_000_000_000 253 d = datetime.datetime.fromordinal(ordinal_day) 254 t = datetime.timedelta(seconds=ns_in_day // 10**9) 255 return datetime.datetime.combine(d, datetime.time()) + t 256 257 def _step(self, action: Action = None, account = None, ref: int = None, file: int = None, value: int = None, key: str = None, math_operation: MathOperation = None) -> int: 258 """ 259 This method is responsible for recording the actions performed on the ZakatTracker. 260 261 Parameters: 262 - action (Action): The type of action performed. 263 - account (str): The account number on which the action was performed. 264 - ref (int): The reference number of the action. 265 - file (int): The file reference number of the action. 266 - value (int): The value associated with the action. 267 - key (str): The key associated with the action. 268 - math_operation (MathOperation): The mathematical operation performed during the action. 269 270 Returns: 271 - int: The lock time of the recorded action. If no lock was performed, it returns 0. 272 """ 273 if not self._history(): 274 return 0 275 _lock = self._vault['lock'] 276 if self.nolock(): 277 _lock = self._vault['lock'] = self.time() 278 self._vault['history'][_lock] = [] 279 if action is None: 280 return _lock 281 self._vault['history'][_lock].append({ 282 'action': action, 283 'account': account, 284 'ref': ref, 285 'file': file, 286 'key': key, 287 'value': value, 288 'math': math_operation, 289 }) 290 291 def nolock(self) -> bool: 292 """ 293 Check if the vault lock is currently not set. 294 295 :return: True if the vault lock is not set, False otherwise. 296 """ 297 return self._vault['lock'] is None 298 299 def lock(self) -> int: 300 """ 301 Acquires a lock on the ZakatTracker instance. 302 303 Returns: 304 int: The lock ID. This ID can be used to release the lock later. 305 """ 306 return self._step() 307 308 def box(self) -> dict: 309 """ 310 Returns a copy of the internal vault dictionary. 311 312 This method is used to retrieve the current state of the ZakatTracker object. 313 It provides a snapshot of the internal data structure, allowing for further 314 processing or analysis. 315 316 :return: A copy of the internal vault dictionary. 317 """ 318 return self._vault.copy() 319 320 def steps(self) -> dict: 321 """ 322 Returns a copy of the history of steps taken in the ZakatTracker. 323 324 The history is a dictionary where each key is a unique identifier for a step, 325 and the corresponding value is a dictionary containing information about the step. 326 327 :return: A copy of the history of steps taken in the ZakatTracker. 328 """ 329 return self._vault['history'].copy() 330 331 def free(self, _lock: int, auto_save: bool = True) -> bool: 332 """ 333 Releases the lock on the database. 334 335 Parameters: 336 _lock (int): The lock ID to be released. 337 auto_save (bool): Whether to automatically save the database after releasing the lock. 338 339 Returns: 340 bool: True if the lock is successfully released and (optionally) saved, False otherwise. 341 """ 342 if _lock == self._vault['lock']: 343 self._vault['lock'] = None 344 if auto_save: 345 return self.save(self.path()) 346 return True 347 return False 348 349 def account_exists(self, account) -> bool: 350 """ 351 Check if the given account exists in the vault. 352 353 Parameters: 354 account (str): The account number to check. 355 356 Returns: 357 bool: True if the account exists, False otherwise. 358 """ 359 return account in self._vault['account'] 360 361 def box_size(self, account) -> int: 362 """ 363 Calculate the size of the box for a specific account. 364 365 Parameters: 366 account (str): The account number for which the box size needs to be calculated. 367 368 Returns: 369 int: The size of the box for the given account. If the account does not exist, -1 is returned. 370 """ 371 if self.account_exists(account): 372 return len(self._vault['account'][account]['box']) 373 return -1 374 375 def log_size(self, account) -> int: 376 """ 377 Get the size of the log for a specific account. 378 379 Parameters: 380 account (str): The account number for which the log size needs to be calculated. 381 382 Returns: 383 int: The size of the log for the given account. If the account does not exist, -1 is returned. 384 """ 385 if self.account_exists(account): 386 return len(self._vault['account'][account]['log']) 387 return -1 388 389 def recall(self, dry = True, debug = False) -> bool: 390 """ 391 Revert the last operation. 392 393 Parameters: 394 dry (bool): If True, the function will not modify the data, but will simulate the operation. Default is True. 395 debug (bool): If True, the function will print debug information. Default is False. 396 397 Returns: 398 bool: True if the operation was successful, False otherwise. 399 """ 400 if not self.nolock() or len(self._vault['history']) == 0: 401 return False 402 if len(self._vault['history']) <= 0: 403 return 404 ref = sorted(self._vault['history'].keys())[-1] 405 if debug: 406 print('recall', ref) 407 memory = self._vault['history'][ref] 408 if debug: 409 print(type(memory), 'memory', memory) 410 411 limit = len(memory) + 1 412 sub_positive_log_negative = 0 413 for i in range(-1, -limit, -1): 414 x = memory[i] 415 if debug: 416 print(type(x), x) 417 match x['action']: 418 case Action.CREATE: 419 if x['account'] is not None: 420 if self.account_exists(x['account']): 421 if debug: 422 print('account', self._vault['account'][x['account']]) 423 assert len(self._vault['account'][x['account']]['box']) == 0 424 assert self._vault['account'][x['account']]['balance'] == 0 425 assert self._vault['account'][x['account']]['count'] == 0 426 if dry: 427 continue 428 del self._vault['account'][x['account']] 429 430 case Action.TRACK: 431 if x['account'] is not None: 432 if self.account_exists(x['account']): 433 if dry: 434 continue 435 self._vault['account'][x['account']]['balance'] -= x['value'] 436 self._vault['account'][x['account']]['count'] -= 1 437 del self._vault['account'][x['account']]['box'][x['ref']] 438 439 case Action.LOG: 440 if x['account'] is not None: 441 if self.account_exists(x['account']): 442 if x['ref'] in self._vault['account'][x['account']]['log']: 443 if dry: 444 continue 445 if sub_positive_log_negative == -x['value']: 446 self._vault['account'][x['account']]['count'] -= 1 447 sub_positive_log_negative = 0 448 del self._vault['account'][x['account']]['log'][x['ref']] 449 450 case Action.SUB: 451 if x['account'] is not None: 452 if self.account_exists(x['account']): 453 if x['ref'] in self._vault['account'][x['account']]['box']: 454 if dry: 455 continue 456 self._vault['account'][x['account']]['box'][x['ref']]['rest'] += x['value'] 457 self._vault['account'][x['account']]['balance'] += x['value'] 458 sub_positive_log_negative = x['value'] 459 460 case Action.ADD_FILE: 461 if x['account'] is not None: 462 if self.account_exists(x['account']): 463 if x['ref'] in self._vault['account'][x['account']]['log']: 464 if x['file'] in self._vault['account'][x['account']]['log'][x['ref']]['file']: 465 if dry: 466 continue 467 del self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']] 468 469 case Action.REMOVE_FILE: 470 if x['account'] is not None: 471 if self.account_exists(x['account']): 472 if x['ref'] in self._vault['account'][x['account']]['log']: 473 if dry: 474 continue 475 self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']] = x['value'] 476 477 case Action.BOX_TRANSFER: 478 if x['account'] is not None: 479 if self.account_exists(x['account']): 480 if x['ref'] in self._vault['account'][x['account']]['box']: 481 if dry: 482 continue 483 self._vault['account'][x['account']]['box'][x['ref']]['rest'] -= x['value'] 484 485 case Action.EXCHANGE: 486 if x['account'] is not None: 487 if x['account'] in self._vault['exchange']: 488 if x['ref'] in self._vault['exchange'][x['account']]: 489 if dry: 490 continue 491 del self._vault['exchange'][x['account']][x['ref']] 492 493 case Action.REPORT: 494 if x['ref'] in self._vault['report']: 495 if dry: 496 continue 497 del self._vault['report'][x['ref']] 498 499 case Action.ZAKAT: 500 if x['account'] is not None: 501 if self.account_exists(x['account']): 502 if x['ref'] in self._vault['account'][x['account']]['box']: 503 if x['key'] in self._vault['account'][x['account']]['box'][x['ref']]: 504 if dry: 505 continue 506 match x['math']: 507 case MathOperation.ADDITION: 508 self._vault['account'][x['account']]['box'][x['ref']][x['key']] -= x['value'] 509 case MathOperation.EQUAL: 510 self._vault['account'][x['account']]['box'][x['ref']][x['key']] = x['value'] 511 case MathOperation.SUBTRACTION: 512 self._vault['account'][x['account']]['box'][x['ref']][x['key']] += x['value'] 513 514 if not dry: 515 del self._vault['history'][ref] 516 return True 517 518 def ref_exists(self, account: str, ref_type: str, ref: int) -> bool: 519 """ 520 Check if a specific reference (transaction) exists in the vault for a given account and reference type. 521 522 Parameters: 523 account (str): The account number for which to check the existence of the reference. 524 ref_type (str): The type of reference (e.g., 'box', 'log', etc.). 525 ref (int): The reference (transaction) number to check for existence. 526 527 Returns: 528 bool: True if the reference exists for the given account and reference type, False otherwise. 529 """ 530 if account in self._vault['account']: 531 return ref in self._vault['account'][account][ref_type] 532 return False 533 534 def box_exists(self, account: str, ref: int) -> bool: 535 """ 536 Check if a specific box (transaction) exists in the vault for a given account and reference. 537 538 Parameters: 539 - account (str): The account number for which to check the existence of the box. 540 - ref (int): The reference (transaction) number to check for existence. 541 542 Returns: 543 - bool: True if the box exists for the given account and reference, False otherwise. 544 """ 545 return self.ref_exists(account, 'box', ref) 546 547 def track(self, value: int = 0, desc: str = '', account: str = 1, logging: bool = True, created: int = None, debug: bool = False) -> int: 548 """ 549 This function tracks a transaction for a specific account. 550 551 Parameters: 552 value (int): The value of the transaction. Default is 0. 553 desc (str): The description of the transaction. Default is an empty string. 554 account (str): The account for which the transaction is being tracked. Default is '1'. 555 logging (bool): Whether to log the transaction. Default is True. 556 created (int): The timestamp of the transaction. If not provided, it will be generated. Default is None. 557 debug (bool): Whether to print debug information. Default is False. 558 559 Returns: 560 int: The timestamp of the transaction. 561 562 This function creates a new account if it doesn't exist, logs the transaction if logging is True, and updates the account's balance and box. 563 564 Raises: 565 ValueError: If the box transction happend again in the same nanosecond time. 566 """ 567 if created is None: 568 created = self.time() 569 _nolock = self.nolock(); self.lock() 570 if not self.account_exists(account): 571 if debug: 572 print(f"account {account} created") 573 self._vault['account'][account] = { 574 'balance': 0, 575 'box': {}, 576 'count': 0, 577 'log': {}, 578 'zakatable': True, 579 } 580 self._step(Action.CREATE, account) 581 if value == 0: 582 if _nolock: self.free(self.lock()) 583 return 0 584 if logging: 585 self._log(value, desc, account, created, debug) 586 if debug: 587 print('create-box', created) 588 if self.box_exists(account, created): 589 raise ValueError(f"The box transction happend again in the same nanosecond time({created}).") 590 if debug: 591 print('created-box', created) 592 self._vault['account'][account]['box'][created] = { 593 'capital': value, 594 'count': 0, 595 'last': 0, 596 'rest': value, 597 'total': 0, 598 } 599 self._step(Action.TRACK, account, ref=created, value=value) 600 if _nolock: self.free(self.lock()) 601 return created 602 603 def log_exists(self, account: str, ref: int) -> bool: 604 """ 605 Checks if a specific transaction log entry exists for a given account. 606 607 Parameters: 608 account (str): The account number associated with the transaction log. 609 ref (int): The reference to the transaction log entry. 610 611 Returns: 612 bool: True if the transaction log entry exists, False otherwise. 613 """ 614 return self.ref_exists(account, 'log', ref) 615 616 def _log(self, value: int, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> int: 617 """ 618 Log a transaction into the account's log. 619 620 Parameters: 621 value (int): The value of the transaction. 622 desc (str): The description of the transaction. 623 account (str): The account to log the transaction into. Default is '1'. 624 created (int): The timestamp of the transaction. If not provided, it will be generated. 625 626 Returns: 627 int: The timestamp of the logged transaction. 628 629 This method updates the account's balance, count, and log with the transaction details. 630 It also creates a step in the history of the transaction. 631 632 Raises: 633 ValueError: If the log transction happend again in the same nanosecond time. 634 """ 635 if created is None: 636 created = self.time() 637 self._vault['account'][account]['balance'] += value 638 self._vault['account'][account]['count'] += 1 639 if debug: 640 print('create-log', created) 641 if self.log_exists(account, created): 642 raise ValueError(f"The log transction happend again in the same nanosecond time({created}).") 643 if debug: 644 print('created-log', created) 645 self._vault['account'][account]['log'][created] = { 646 'value': value, 647 'desc': desc, 648 'file': {}, 649 } 650 self._step(Action.LOG, account, ref=created, value=value) 651 return created 652 653 def exchange(self, account, created: int = None, rate: float = None, description: str = None, debug: bool = False) -> dict: 654 """ 655 This method is used to record or retrieve exchange rates for a specific account. 656 657 Parameters: 658 - account (str): The account number for which the exchange rate is being recorded or retrieved. 659 - created (int): The timestamp of the exchange rate. If not provided, the current timestamp will be used. 660 - rate (float): The exchange rate to be recorded. If not provided, the method will retrieve the latest exchange rate. 661 - description (str): A description of the exchange rate. 662 663 Returns: 664 - dict: A dictionary containing the latest exchange rate and its description. If no exchange rate is found, 665 it returns a dictionary with default values for the rate and description. 666 """ 667 if created is None: 668 created = self.time() 669 if rate is not None: 670 if rate <= 1: 671 return None 672 if account not in self._vault['exchange']: 673 self._vault['exchange'][account] = {} 674 self._vault['exchange'][account][created] = {"rate": rate, "description": description} 675 self._step(Action.EXCHANGE, account, ref=created, value=rate) 676 if debug: 677 print("exchange-created-1", f'account: {account}, created: {created}, rate:{rate}, description:{description}') 678 679 if account in self._vault['exchange']: 680 valid_rates = [(ts, r) for ts, r in self._vault['exchange'][account].items() if ts <= created] 681 if valid_rates: 682 latest_rate = max(valid_rates, key=lambda x: x[0]) 683 if debug: 684 print("exchange-read-1", f'account: {account}, created: {created}, rate:{rate}, description:{description}', 'latest_rate', latest_rate) 685 return latest_rate[1] # إرجاع قاموس يحتوي على المعدل والوصف 686 if debug: 687 print("exchange-read-0", f'account: {account}, created: {created}, rate:{rate}, description:{description}') 688 return {"rate": 1, "description": None} # إرجاع القيمة الافتراضية مع وصف فارغ 689 690 def exchanges(self) -> dict: 691 """ 692 Retrieve the recorded exchange rates for all accounts. 693 694 Parameters: 695 None 696 697 Returns: 698 dict: A dictionary containing all recorded exchange rates. 699 The keys are account names or numbers, and the values are dictionaries containing the exchange rates. 700 Each exchange rate dictionary has timestamps as keys and exchange rate details as values. 701 """ 702 return self._vault['exchange'].copy() 703 704 def accounts(self) -> dict: 705 """ 706 Returns a dictionary containing account numbers as keys and their respective balances as values. 707 708 Parameters: 709 None 710 711 Returns: 712 dict: A dictionary where keys are account numbers and values are their respective balances. 713 """ 714 result = {} 715 for i in self._vault['account']: 716 result[i] = self._vault['account'][i]['balance'] 717 return result 718 719 def boxes(self, account) -> dict: 720 """ 721 Retrieve the boxes (transactions) associated with a specific account. 722 723 Parameters: 724 account (str): The account number for which to retrieve the boxes. 725 726 Returns: 727 dict: A dictionary containing the boxes associated with the given account. 728 If the account does not exist, an empty dictionary is returned. 729 """ 730 if self.account_exists(account): 731 return self._vault['account'][account]['box'] 732 return {} 733 734 def logs(self, account) -> dict: 735 """ 736 Retrieve the logs (transactions) associated with a specific account. 737 738 Parameters: 739 account (str): The account number for which to retrieve the logs. 740 741 Returns: 742 dict: A dictionary containing the logs associated with the given account. 743 If the account does not exist, an empty dictionary is returned. 744 """ 745 if self.account_exists(account): 746 return self._vault['account'][account]['log'] 747 return {} 748 749 def add_file(self, account: str, ref: int, path: str) -> int: 750 """ 751 Adds a file reference to a specific transaction log entry in the vault. 752 753 Parameters: 754 account (str): The account number associated with the transaction log. 755 ref (int): The reference to the transaction log entry. 756 path (str): The path of the file to be added. 757 758 Returns: 759 int: The reference of the added file. If the account or transaction log entry does not exist, returns 0. 760 """ 761 if self.account_exists(account): 762 if ref in self._vault['account'][account]['log']: 763 file_ref = self.time() 764 self._vault['account'][account]['log'][ref]['file'][file_ref] = path 765 _nolock = self.nolock(); self.lock() 766 self._step(Action.ADD_FILE, account, ref=ref, file=file_ref) 767 if _nolock: self.free(self.lock()) 768 return file_ref 769 return 0 770 771 def remove_file(self, account: str, ref: int, file_ref: int) -> bool: 772 """ 773 Removes a file reference from a specific transaction log entry in the vault. 774 775 Parameters: 776 account (str): The account number associated with the transaction log. 777 ref (int): The reference to the transaction log entry. 778 file_ref (int): The reference of the file to be removed. 779 780 Returns: 781 bool: True if the file reference is successfully removed, False otherwise. 782 """ 783 if self.account_exists(account): 784 if ref in self._vault['account'][account]['log']: 785 if file_ref in self._vault['account'][account]['log'][ref]['file']: 786 x = self._vault['account'][account]['log'][ref]['file'][file_ref] 787 del self._vault['account'][account]['log'][ref]['file'][file_ref] 788 _nolock = self.nolock(); self.lock() 789 self._step(Action.REMOVE_FILE, account, ref=ref, file=file_ref, value=x) 790 if _nolock: self.free(self.lock()) 791 return True 792 return False 793 794 def balance(self, account: str = 1, cached: bool = True) -> int: 795 """ 796 Calculate and return the balance of a specific account. 797 798 Parameters: 799 account (str): The account number. Default is '1'. 800 cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True. 801 802 Returns: 803 int: The balance of the account. 804 805 Note: 806 If cached is True, the function returns the cached balance. 807 If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items. 808 """ 809 if cached: 810 return self._vault['account'][account]['balance'] 811 x = 0 812 return [x := x + y['rest'] for y in self._vault['account'][account]['box'].values()][-1] 813 814 def zakatable(self, account, status: bool = None) -> bool: 815 """ 816 Check or set the zakatable status of a specific account. 817 818 Parameters: 819 account (str): The account number. 820 status (bool, optional): The new zakatable status. If not provided, the function will return the current status. 821 822 Returns: 823 bool: The current or updated zakatable status of the account. 824 825 Raises: 826 None 827 828 Example: 829 >>> tracker = ZakatTracker() 830 >>> tracker.zakatable('account1', True) # Set the zakatable status of 'account1' to True 831 True 832 >>> tracker.zakatable('account1') # Get the zakatable status of 'account1' 833 True 834 """ 835 if self.account_exists(account): 836 if status is None: 837 return self._vault['account'][account]['zakatable'] 838 self._vault['account'][account]['zakatable'] = status 839 return status 840 return False 841 842 def sub(self, x: int, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> tuple: 843 """ 844 Subtracts a specified value from an account's balance. 845 846 Parameters: 847 x (int): The amount to be subtracted. 848 desc (str): A description for the transaction. Defaults to an empty string. 849 account (str): The account from which the value will be subtracted. Defaults to '1'. 850 created (int): The timestamp of the transaction. If not provided, the current timestamp will be used. 851 debug (bool): A flag indicating whether to print debug information. Defaults to False. 852 853 Returns: 854 tuple: A tuple containing the timestamp of the transaction and a list of tuples representing the age of each transaction. 855 856 If the amount to subtract is greater than the account's balance, 857 the remaining amount will be transferred to a new transaction with a negative value. 858 859 Raises: 860 ValueError: If the transction happend again in the same nanosecond time. 861 """ 862 if x < 0: 863 return 864 if x == 0: 865 return self.track(x, '', account) 866 if created is None: 867 created = self.time() 868 _nolock = self.nolock(); self.lock() 869 self.track(0, '', account) 870 self._log(-x, desc, account, created) 871 ids = sorted(self._vault['account'][account]['box'].keys()) 872 limit = len(ids) + 1 873 target = x 874 if debug: 875 print('ids', ids) 876 ages = [] 877 for i in range(-1, -limit, -1): 878 if target == 0: 879 break 880 j = ids[i] 881 if debug: 882 print('i', i, 'j', j) 883 if self._vault['account'][account]['box'][j]['rest'] >= target: 884 self._vault['account'][account]['box'][j]['rest'] -= target 885 self._step(Action.SUB, account, ref=j, value=target) 886 ages.append((j, target)) 887 target = 0 888 break 889 elif self._vault['account'][account]['box'][j]['rest'] < target and self._vault['account'][account]['box'][j]['rest'] > 0: 890 chunk = self._vault['account'][account]['box'][j]['rest'] 891 target -= chunk 892 self._step(Action.SUB, account, ref=j, value=chunk) 893 ages.append((j, chunk)) 894 self._vault['account'][account]['box'][j]['rest'] = 0 895 if target > 0: 896 self.track(-target, desc, account, False, created) 897 ages.append((created, target)) 898 if _nolock: self.free(self.lock()) 899 return (created, ages) 900 901 def transfer(self, amount: int, from_account: str, to_account: str, desc: str = '', created: int = None, debug: bool = False) -> list[int]: 902 """ 903 Transfers a specified value from one account to another. 904 905 Parameters: 906 amount (int): The amount to be transferred. 907 from_account (str): The account from which the value will be transferred. 908 to_account (str): The account to which the value will be transferred. 909 desc (str, optional): A description for the transaction. Defaults to an empty string. 910 created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used. 911 debug (bool): A flag indicating whether to print debug information. Defaults to False. 912 913 Returns: 914 list[int]: A list of timestamps corresponding to the transactions made during the transfer. 915 916 Raises: 917 ValueError: If the transction happend again in the same nanosecond time. 918 """ 919 if amount <= 0: 920 return None 921 if created is None: 922 created = self.time() 923 (_, ages) = self.sub(amount, desc, from_account, created, debug=debug) 924 times = [] 925 source_exchange = self.exchange(from_account, created) 926 target_exchange = self.exchange(to_account, created) 927 928 if debug: 929 print('ages', ages) 930 931 for age, value in ages: 932 # Convert source amount to the base currency 933 source_amount_base = value * source_exchange['rate'] 934 # Convert base amount to the target currency 935 target_amount = source_amount_base / target_exchange['rate'] 936 # Perform the transfer 937 if self.box_exists(to_account, age): 938 if debug: 939 print('box_exists', age) 940 capital = self._vault['account'][to_account]['box'][age]['capital'] 941 rest = self._vault['account'][to_account]['box'][age]['rest'] 942 if debug: 943 print(f"Transfer {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).") 944 selected_age = age 945 if rest + target_amount > capital: 946 self._vault['account'][to_account]['box'][age]['capital'] += target_amount 947 selected_age = ZakatTracker.time() 948 self._vault['account'][to_account]['box'][age]['rest'] += target_amount 949 self._step(Action.BOX_TRANSFER, to_account, ref=selected_age, value=target_amount) 950 y = self._log(target_amount, desc=f'TRANSFER {from_account} -> {to_account}', account=to_account, debug=debug) 951 times.append((age, y)) 952 continue 953 y = self.track(target_amount, desc, to_account, logging=True, created=age, debug=debug) 954 if debug: 955 print(f"Transferred {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).") 956 times.append(y) 957 return times 958 959 def check(self, silver_gram_price: float, nisab: float = None, debug: bool = False, now: int = None, cycle: float = None) -> tuple: 960 """ 961 Check the eligibility for Zakat based on the given parameters. 962 963 Parameters: 964 silver_gram_price (float): The price of a gram of silver. 965 nisab (float): The minimum amount of wealth required for Zakat. If not provided, it will be calculated based on the silver_gram_price. 966 debug (bool): Flag to enable debug mode. 967 now (int): The current timestamp. If not provided, it will be calculated using ZakatTracker.time(). 968 cycle (float): The time cycle for Zakat. If not provided, it will be calculated using ZakatTracker.TimeCycle(). 969 970 Returns: 971 tuple: A tuple containing a boolean indicating the eligibility for Zakat, a list of brief statistics, and a dictionary containing the Zakat plan. 972 """ 973 if now is None: 974 now = self.time() 975 if cycle is None: 976 cycle = ZakatTracker.TimeCycle() 977 if nisab is None: 978 nisab = ZakatTracker.Nisab(silver_gram_price) 979 plan = {} 980 below_nisab = 0 981 brief = [0, 0, 0] 982 valid = False 983 for x in self._vault['account']: 984 if not self._vault['account'][x]['zakatable']: 985 continue 986 _box = self._vault['account'][x]['box'] 987 limit = len(_box) + 1 988 ids = sorted(self._vault['account'][x]['box'].keys()) 989 for i in range(-1, -limit, -1): 990 j = ids[i] 991 if _box[j]['rest'] <= 0: 992 continue 993 brief[0] += _box[j]['rest'] 994 index = limit + i - 1 995 epoch = (now - j) / cycle 996 if debug: 997 print(f"Epoch: {epoch}", _box[j]) 998 if _box[j]['last'] > 0: 999 epoch = (now - _box[j]['last']) / cycle 1000 if debug: 1001 print(f"Epoch: {epoch}") 1002 epoch = floor(epoch) 1003 if debug: 1004 print(f"Epoch: {epoch}", type(epoch), epoch == 0, 1-epoch, epoch) 1005 if epoch == 0: 1006 continue 1007 if debug: 1008 print("Epoch - PASSED") 1009 brief[1] += _box[j]['rest'] 1010 if _box[j]['rest'] >= nisab: 1011 total = 0 1012 for _ in range(epoch): 1013 total += ZakatTracker.ZakatCut(_box[j]['rest'] - total) 1014 if total > 0: 1015 if x not in plan: 1016 plan[x] = {} 1017 valid = True 1018 brief[2] += total 1019 plan[x][index] = {'total': total, 'count': epoch} 1020 else: 1021 chunk = ZakatTracker.ZakatCut(_box[j]['rest']) 1022 if chunk > 0: 1023 if x not in plan: 1024 plan[x] = {} 1025 if j not in plan[x].keys(): 1026 plan[x][index] = {} 1027 below_nisab += _box[j]['rest'] 1028 brief[2] += chunk 1029 plan[x][index]['below_nisab'] = chunk 1030 valid = valid or below_nisab >= nisab 1031 if debug: 1032 print(f"below_nisab({below_nisab}) >= nisab({nisab})") 1033 return (valid, brief, plan) 1034 1035 def build_payment_parts(self, demand: float, positive_only: bool = True) -> dict: 1036 """ 1037 Build payment parts for the zakat distribution. 1038 1039 Parameters: 1040 demand (float): The total demand for payment. 1041 positive_only (bool): If True, only consider accounts with positive balance. Default is True. 1042 1043 Returns: 1044 dict: A dictionary containing the payment parts for each account. The dictionary has the following structure: 1045 { 1046 'account': { 1047 'account_id': {'balance': float, 'part': float}, 1048 ... 1049 }, 1050 'exceed': bool, 1051 'demand': float, 1052 'total': float, 1053 } 1054 """ 1055 total = 0 1056 parts = { 1057 'account': {}, 1058 'exceed': False, 1059 'demand': demand, 1060 } 1061 for x, y in self.accounts().items(): 1062 if positive_only and y <= 0: 1063 continue 1064 total += y 1065 parts['account'][x] = {'balance': y, 'part': 0} 1066 parts['total'] = total 1067 return parts 1068 1069 def check_payment_parts(self, parts: dict) -> int: 1070 """ 1071 Checks the validity of payment parts. 1072 1073 Parameters: 1074 parts (dict): A dictionary containing payment parts information. 1075 1076 Returns: 1077 int: Returns 0 if the payment parts are valid, otherwise returns the error code. 1078 1079 Error Codes: 1080 1: 'demand', 'account', 'total', or 'exceed' key is missing in parts. 1081 2: 'balance' or 'part' key is missing in parts['account'][x]. 1082 3: 'part' value in parts['account'][x] is less than or equal to 0. 1083 4: If 'exceed' is False, 'balance' value in parts['account'][x] is less than or equal to 0. 1084 5: 'part' value in parts['account'][x] is less than 0. 1085 6: If 'exceed' is False, 'part' value in parts['account'][x] is greater than 'balance' value. 1086 7: The sum of 'part' values in parts['account'] does not match with 'demand' value. 1087 """ 1088 for i in ['demand', 'account', 'total', 'exceed']: 1089 if not i in parts: 1090 return 1 1091 exceed = parts['exceed'] 1092 for x in parts['account']: 1093 for j in ['balance', 'part']: 1094 if not j in parts['account'][x]: 1095 return 2 1096 if parts['account'][x]['part'] <= 0: 1097 return 3 1098 if not exceed and parts['account'][x]['balance'] <= 0: 1099 return 4 1100 demand = parts['demand'] 1101 z = 0 1102 for _, y in parts['account'].items(): 1103 if y['part'] < 0: 1104 return 5 1105 if not exceed and y['part'] > y['balance']: 1106 return 6 1107 z += y['part'] 1108 if z != demand: 1109 return 7 1110 return 0 1111 1112 def zakat(self, report: tuple, parts: dict = None, debug: bool = False) -> bool: 1113 """ 1114 Perform Zakat calculation based on the given report and optional parts. 1115 1116 Parameters: 1117 report (tuple): A tuple containing the validity of the report, the report data, and the zakat plan. 1118 parts (dict): A dictionary containing the payment parts for the zakat. 1119 debug (bool): A flag indicating whether to print debug information. 1120 1121 Returns: 1122 bool: True if the zakat calculation is successful, False otherwise. 1123 """ 1124 (valid, _, plan) = report 1125 if not valid: 1126 return valid 1127 parts_exist = parts is not None 1128 if parts_exist: 1129 for part in parts: 1130 if self.check_payment_parts(part) != 0: 1131 return False 1132 if debug: 1133 print('######### zakat #######') 1134 print('parts_exist', parts_exist) 1135 _nolock = self.nolock(); self.lock() 1136 report_time = self.time() 1137 self._vault['report'][report_time] = report 1138 self._step(Action.REPORT, ref=report_time) 1139 created = self.time() 1140 for x in plan: 1141 if debug: 1142 print(plan[x]) 1143 print('-------------') 1144 print(self._vault['account'][x]['box']) 1145 ids = sorted(self._vault['account'][x]['box'].keys()) 1146 if debug: 1147 print('plan[x]', plan[x]) 1148 for i in plan[x].keys(): 1149 j = ids[i] 1150 if debug: 1151 print('i', i, 'j', j) 1152 self._step(Action.ZAKAT, x, j, value=self._vault['account'][x]['box'][j]['last'], key='last', math_operation=MathOperation.EQUAL) 1153 self._vault['account'][x]['box'][j]['last'] = created 1154 self._vault['account'][x]['box'][j]['total'] += plan[x][i]['total'] 1155 self._step(Action.ZAKAT, x, j, value=plan[x][i]['total'], key='total', math_operation=MathOperation.ADDITION) 1156 self._vault['account'][x]['box'][j]['count'] += plan[x][i]['count'] 1157 self._step(Action.ZAKAT, x, j, value=plan[x][i]['count'], key='count', math_operation=MathOperation.ADDITION) 1158 if not parts_exist: 1159 self._vault['account'][x]['box'][j]['rest'] -= plan[x][i]['total'] 1160 self._step(Action.ZAKAT, x, j, value=plan[x][i]['total'], key='rest', math_operation=MathOperation.SUBTRACTION) 1161 if parts_exist: 1162 for transaction in parts: 1163 for account, part in transaction['account'].items(): 1164 if debug: 1165 print('zakat-part', account, part['part']) 1166 self.sub(part['part'], 'zakat-part', account, debug=debug) 1167 if _nolock: self.free(self.lock()) 1168 return True 1169 1170 def export_json(self, path: str = "data.json") -> bool: 1171 """ 1172 Exports the current state of the ZakatTracker object to a JSON file. 1173 1174 Parameters: 1175 path (str): The path where the JSON file will be saved. Default is "data.json". 1176 1177 Returns: 1178 bool: True if the export is successful, False otherwise. 1179 1180 Raises: 1181 No specific exceptions are raised by this method. 1182 """ 1183 with open(path, "w") as file: 1184 json.dump(self._vault, file, indent=4, cls=JSONEncoder) 1185 return True 1186 return False 1187 1188 def save(self, path: str = None) -> bool: 1189 """ 1190 Save the current state of the ZakatTracker object to a pickle file. 1191 1192 Parameters: 1193 path (str): The path where the pickle file will be saved. If not provided, it will use the default path. 1194 1195 Returns: 1196 bool: True if the save operation is successful, False otherwise. 1197 """ 1198 if path is None: 1199 path = self.path() 1200 with open(path, "wb") as f: 1201 pickle.dump(self._vault, f) 1202 return True 1203 return False 1204 1205 def load(self, path: str = None) -> bool: 1206 """ 1207 Load the current state of the ZakatTracker object from a pickle file. 1208 1209 Parameters: 1210 path (str): The path where the pickle file is located. If not provided, it will use the default path. 1211 1212 Returns: 1213 bool: True if the load operation is successful, False otherwise. 1214 """ 1215 if path is None: 1216 path = self.path() 1217 if os.path.exists(path): 1218 with open(path, "rb") as f: 1219 self._vault = pickle.load(f) 1220 return True 1221 return False 1222 1223 def import_csv(self, path: str = 'file.csv') -> tuple: 1224 """ 1225 Import transactions from a CSV file. 1226 1227 Parameters: 1228 path (str): The path to the CSV file. Default is 'file.csv'. 1229 1230 Returns: 1231 tuple: A tuple containing the number of transactions created, the number of transactions found in the cache, and a dictionary of bad transactions. 1232 1233 The CSV file should have the following format: 1234 account, desc, value, date 1235 For example: 1236 safe-45, "Some text", 34872, 1988-06-30 00:00:00 1237 1238 The function reads the CSV file, checks for duplicate transactions, and creates the transactions in the system. 1239 """ 1240 cache = [] 1241 tmp = "tmp" 1242 try: 1243 with open(tmp, "rb") as f: 1244 cache = pickle.load(f) 1245 except: 1246 pass 1247 date_formats = [ 1248 "%Y-%m-%d %H:%M:%S", 1249 "%Y-%m-%dT%H:%M:%S", 1250 "%Y-%m-%dT%H%M%S", 1251 "%Y-%m-%d", 1252 ] 1253 created, found, bad = 0, 0, {} 1254 with open(path, newline='', encoding="utf-8") as f: 1255 i = 0 1256 for row in csv.reader(f, delimiter=','): 1257 i += 1 1258 hashed = hash(tuple(row)) 1259 if hashed in cache: 1260 found += 1 1261 continue 1262 account = row[0] 1263 desc = row[1] 1264 value = float(row[2]) 1265 date = 0 1266 for time_format in date_formats: 1267 try: 1268 date = self.time(datetime.datetime.strptime(row[3], time_format)) 1269 break 1270 except: 1271 pass 1272 # TODO: not allowed for negative dates 1273 if date == 0 or value == 0: 1274 bad[i] = row 1275 continue 1276 if value > 0: 1277 self.track(value, desc, account, True, date) 1278 elif value < 0: 1279 self.sub(-value, desc, account, date) 1280 created += 1 1281 cache.append(hashed) 1282 with open(tmp, "wb") as f: 1283 pickle.dump(cache, f) 1284 return (created, found, bad) 1285 1286 ######## 1287 # TESTS # 1288 ####### 1289 1290 @staticmethod 1291 def DurationFromNanoSeconds(ns: int) -> tuple: 1292 """REF https://github.com/JayRizzo/Random_Scripts/blob/master/time_measure.py#L106 1293 Convert NanoSeconds to Human Readable Time Format. 1294 A NanoSeconds is a unit of time in the International System of Units (SI) equal to one millionth (0.000001 or 10−6 or 1⁄1,000,000) of a second. 1295 Its symbol is μs, sometimes simplified to us when Unicode is not available. 1296 A microsecond is equal to 1000 nanoseconds or 1⁄1,000 of a millisecond. 1297 1298 INPUT : ms (AKA: MilliSeconds) 1299 OUTPUT: tuple(string TIMELAPSED, string SPOKENTIME) like format. 1300 OUTPUT Variables: TIMELAPSED, SPOKENTIME 1301 1302 Example Input: DurationFromNanoSeconds(ns) 1303 **"Millennium:Century:Years:Days:Hours:Minutes:Seconds:MilliSeconds:MicroSeconds:NanoSeconds"** 1304 Example Output: ('039:0001:047:325:05:02:03:456:789:012', ' 39 Millennia, 1 Century, 47 Years, 325 Days, 5 Hours, 2 Minutes, 3 Seconds, 456 MilliSeconds, 789 MicroSeconds, 12 NanoSeconds') 1305 DurationFromNanoSeconds(1234567890123456789012) 1306 """ 1307 μs, ns = divmod(ns, 1000) 1308 ms, μs = divmod(μs, 1000) 1309 s, ms = divmod(ms, 1000) 1310 m, s = divmod(s, 60) 1311 h, m = divmod(m, 60) 1312 d, h = divmod(h, 24) 1313 y, d = divmod(d, 365) 1314 c, y = divmod(y, 100) 1315 n, c = divmod(c, 10) 1316 TIMELAPSED = f"{n:03.0f}:{c:04.0f}:{y:03.0f}:{d:03.0f}:{h:02.0f}:{m:02.0f}:{s:02.0f}::{ms:03.0f}::{μs:03.0f}::{ns:03.0f}" 1317 SPOKENTIME = f"{n: 3d} Millennia, {c: 4d} Century, {y: 3d} Years, {d: 4d} Days, {h: 2d} Hours, {m: 2d} Minutes, {s: 2d} Seconds, {ms: 3d} MilliSeconds, {μs: 3d} MicroSeconds, {ns: 3d} NanoSeconds" 1318 return TIMELAPSED, SPOKENTIME 1319 1320 @staticmethod 1321 def day_to_time(day: int, month: int = 6, year: int = 2024) -> int: # افتراض أن الشهر هو يونيو والسنة 2024 1322 """ 1323 Convert a specific day, month, and year into a timestamp. 1324 1325 Parameters: 1326 day (int): The day of the month. 1327 month (int): The month of the year. Default is 6 (June). 1328 year (int): The year. Default is 2024. 1329 1330 Returns: 1331 int: The timestamp representing the given day, month, and year. 1332 1333 Note: 1334 This method assumes the default month and year if not provided. 1335 """ 1336 return ZakatTracker.time(datetime.datetime(year, month, day)) 1337 1338 @staticmethod 1339 def generate_random_date(start_date: datetime.datetime, end_date: datetime.datetime) -> datetime.datetime: 1340 """ 1341 Generate a random date between two given dates. 1342 1343 Parameters: 1344 start_date (datetime.datetime): The start date from which to generate a random date. 1345 end_date (datetime.datetime): The end date until which to generate a random date. 1346 1347 Returns: 1348 datetime.datetime: A random date between the start_date and end_date. 1349 """ 1350 time_between_dates = end_date - start_date 1351 days_between_dates = time_between_dates.days 1352 random_number_of_days = random.randrange(days_between_dates) 1353 return start_date + datetime.timedelta(days=random_number_of_days) 1354 1355 @staticmethod 1356 def generate_random_csv_file(path: str = "data.csv", count: int = 1000) -> None: 1357 """ 1358 Generate a random CSV file with specified parameters. 1359 1360 Parameters: 1361 path (str): The path where the CSV file will be saved. Default is "data.csv". 1362 count (int): The number of rows to generate in the CSV file. Default is 1000. 1363 1364 Returns: 1365 None. The function generates a CSV file at the specified path with the given count of rows. 1366 Each row contains a randomly generated account, description, value, and date. 1367 The value is randomly generated between 1000 and 100000, and the date is randomly generated between 1950-01-01 and 2023-12-31. 1368 If the row number is not divisible by 13, the value is multiplied by -1. 1369 """ 1370 with open(path, "w", newline="") as csvfile: 1371 writer = csv.writer(csvfile) 1372 for i in range(count): 1373 account = f"acc-{random.randint(1, 1000)}" 1374 desc = f"Some text {random.randint(1, 1000)}" 1375 value = random.randint(1000, 100000) 1376 date = ZakatTracker.generate_random_date(datetime.datetime(1950, 1, 1), datetime.datetime(2023, 12, 31)).strftime("%Y-%m-%d %H:%M:%S") 1377 if not i % 13 == 0: 1378 value *= -1 1379 writer.writerow([account, desc, value, date]) 1380 1381 @staticmethod 1382 def create_random_list(max_sum, min_value=0, max_value=10): 1383 """ 1384 Creates a list of random integers whose sum does not exceed the specified maximum. 1385 1386 Args: 1387 max_sum: The maximum allowed sum of the list elements. 1388 min_value: The minimum possible value for an element (inclusive). 1389 max_value: The maximum possible value for an element (inclusive). 1390 1391 Returns: 1392 A list of random integers. 1393 """ 1394 result = [] 1395 current_sum = 0 1396 1397 while current_sum < max_sum: 1398 # Calculate the remaining space for the next element 1399 remaining_sum = max_sum - current_sum 1400 # Determine the maximum possible value for the next element 1401 next_max_value = min(remaining_sum, max_value) 1402 # Generate a random element within the allowed range 1403 next_element = random.randint(min_value, next_max_value) 1404 result.append(next_element) 1405 current_sum += next_element 1406 1407 return result 1408 1409 def _test_core(self, restore = False, debug = False): 1410 1411 random.seed(1234567890) 1412 1413 # sanity check - random forward time 1414 1415 xlist = [] 1416 limit = 1000 1417 for _ in range(limit): 1418 y = ZakatTracker.time() 1419 z = '-' 1420 if not y in xlist: 1421 xlist.append(y) 1422 else: 1423 z = 'x' 1424 if debug: 1425 print(z, y) 1426 xx = len(xlist) 1427 if debug: 1428 print('count', xx, ' - unique: ', (xx/limit)*100, '%') 1429 assert limit == xx 1430 1431 # sanity check - convert date since 1000AD 1432 1433 for year in range(1000, 9000): 1434 ns = ZakatTracker.time(datetime.datetime.strptime(f"{year}-12-30 18:30:45","%Y-%m-%d %H:%M:%S")) 1435 date = ZakatTracker.time_to_datetime(ns) 1436 if debug: 1437 print(date) 1438 assert date.year == year 1439 assert date.month == 12 1440 assert date.day == 30 1441 assert date.hour == 18 1442 assert date.minute == 30 1443 assert date.second in [44, 45] 1444 assert self.nolock() 1445 1446 assert self._history() is True 1447 1448 table = { 1449 1: [ 1450 (0, 10, 10, 10, 10, 1, 1), 1451 (0, 20, 30, 30, 30, 2, 2), 1452 (0, 30, 60, 60, 60, 3, 3), 1453 (1, 15, 45, 45, 45, 3, 4), 1454 (1, 50, -5, -5, -5, 4, 5), 1455 (1, 100, -105, -105, -105, 5, 6), 1456 ], 1457 'wallet': [ 1458 (1, 90, -90, -90, -90, 1, 1), 1459 (0, 100, 10, 10, 10, 2, 2), 1460 (1, 190, -180, -180, -180, 3, 3), 1461 (0, 1000, 820, 820, 820, 4, 4), 1462 ], 1463 } 1464 for x in table: 1465 for y in table[x]: 1466 self.lock() 1467 ref = 0 1468 if y[0] == 0: 1469 ref = self.track(y[1], 'test-add', x, True, ZakatTracker.time(), debug) 1470 else: 1471 (ref, z) = self.sub(y[1], 'test-sub', x, ZakatTracker.time()) 1472 if debug: 1473 print('_sub', z, ZakatTracker.time()) 1474 assert ref != 0 1475 assert len(self._vault['account'][x]['log'][ref]['file']) == 0 1476 for i in range(3): 1477 file_ref = self.add_file(x, ref, 'file_' + str(i)) 1478 sleep(0.0000001) 1479 assert file_ref != 0 1480 if debug: 1481 print('ref', ref, 'file', file_ref) 1482 assert len(self._vault['account'][x]['log'][ref]['file']) == i+1 1483 file_ref = self.add_file(x, ref, 'file_' + str(3)) 1484 assert self.remove_file(x, ref, file_ref) 1485 assert self.balance(x) == y[2] 1486 z = self.balance(x, False) 1487 if debug: 1488 print("debug-1", z, y[3]) 1489 assert z == y[3] 1490 l = self._vault['account'][x]['log'] 1491 z = 0 1492 for i in l: 1493 z += l[i]['value'] 1494 if debug: 1495 print("debug-2", z, type(z)) 1496 print("debug-2", y[4], type(y[4])) 1497 assert z == y[4] 1498 if debug: 1499 print('debug-2 - PASSED') 1500 assert self.box_size(x) == y[5] 1501 assert self.log_size(x) == y[6] 1502 assert not self.nolock() 1503 self.free(self.lock()) 1504 assert self.nolock() 1505 assert self.boxes(x) != {} 1506 assert self.logs(x) != {} 1507 1508 assert self.zakatable(x) 1509 assert self.zakatable(x, False) is False 1510 assert self.zakatable(x) is False 1511 assert self.zakatable(x, True) 1512 assert self.zakatable(x) 1513 1514 if restore is True: 1515 count = len(self._vault['history']) 1516 if debug: 1517 print('history-count', count) 1518 assert count == 10 1519 # try mode 1520 for _ in range(count): 1521 assert self.recall(True, debug) 1522 count = len(self._vault['history']) 1523 if debug: 1524 print('history-count', count) 1525 assert count == 10 1526 _accounts = list(table.keys()) 1527 accounts_limit = len(_accounts) + 1 1528 for i in range(-1, -accounts_limit, -1): 1529 account = _accounts[i] 1530 if debug: 1531 print(account, len(table[account])) 1532 transaction_limit = len(table[account]) + 1 1533 for j in range(-1, -transaction_limit, -1): 1534 row = table[account][j] 1535 if debug: 1536 print(row, self.balance(account), self.balance(account, False)) 1537 assert self.balance(account) == self.balance(account, False) 1538 assert self.balance(account) == row[2] 1539 assert self.recall(False, debug) 1540 assert self.recall(False, debug) is False 1541 count = len(self._vault['history']) 1542 if debug: 1543 print('history-count', count) 1544 assert count == 0 1545 self.reset() 1546 1547 def test(self, debug: bool = False): 1548 1549 try: 1550 1551 assert self._history() 1552 1553 # Not allowed for duplicate transactions in the same account and time 1554 1555 created = ZakatTracker.time() 1556 self.track(100, 'test-1', 'same', True, created) 1557 failed = False 1558 try: 1559 self.track(50, 'test-1', 'same', True, created) 1560 except: 1561 failed = True 1562 assert failed is True 1563 1564 self.reset() 1565 1566 # Always preserve box age during transfer 1567 1568 created = ZakatTracker.time() 1569 series = [ 1570 (30, 4), 1571 (60, 3), 1572 (90, 2), 1573 ] 1574 case = { 1575 30: { 1576 'series': series, 1577 'rest' : 150, 1578 }, 1579 60: { 1580 'series': series, 1581 'rest' : 120, 1582 }, 1583 90: { 1584 'series': series, 1585 'rest' : 90, 1586 }, 1587 180: { 1588 'series': series, 1589 'rest' : 0, 1590 }, 1591 270: { 1592 'series': series, 1593 'rest' : -90, 1594 }, 1595 360: { 1596 'series': series, 1597 'rest' : -180, 1598 }, 1599 } 1600 1601 selected_time = ZakatTracker.time() - ZakatTracker.TimeCycle() 1602 1603 for total in case: 1604 for x in case[total]['series']: 1605 self.track(x[0], f"test-{x} ages", 'ages', True, selected_time * x[1]) 1606 1607 refs = self.transfer(total, 'ages', 'future', 'Zakat Movement', debug=debug) 1608 1609 if debug: 1610 print('refs', refs) 1611 1612 ages_cache_balance = self.balance('ages') 1613 ages_fresh_balance = self.balance('ages', False) 1614 rest = case[total]['rest'] 1615 if debug: 1616 print('source', ages_cache_balance, ages_fresh_balance, rest) 1617 assert ages_cache_balance == rest 1618 assert ages_fresh_balance == rest 1619 1620 future_cache_balance = self.balance('future') 1621 future_fresh_balance = self.balance('future', False) 1622 if debug: 1623 print('target', future_cache_balance, future_fresh_balance, total) 1624 print('refs', refs) 1625 assert future_cache_balance == total 1626 assert future_fresh_balance == total 1627 1628 for ref in self._vault['account']['ages']['box']: 1629 ages_capital = self._vault['account']['ages']['box'][ref]['capital'] 1630 ages_rest = self._vault['account']['ages']['box'][ref]['rest'] 1631 future_capital = 0 1632 if ref in self._vault['account']['future']['box']: 1633 future_capital = self._vault['account']['future']['box'][ref]['capital'] 1634 future_rest = 0 1635 if ref in self._vault['account']['future']['box']: 1636 future_rest = self._vault['account']['future']['box'][ref]['rest'] 1637 if ages_capital != 0 and future_capital != 0 and future_rest != 0: 1638 if debug: 1639 print('================================================================') 1640 print('ages', ages_capital, ages_rest) 1641 print('future', future_capital, future_rest) 1642 if ages_rest == 0: 1643 assert ages_capital == future_capital 1644 elif ages_rest < 0: 1645 assert -ages_capital == future_capital 1646 elif ages_rest > 0: 1647 assert ages_capital == ages_rest + future_capital 1648 self.reset() 1649 assert len(self._vault['history']) == 0 1650 1651 assert self._history() 1652 assert self._history(False) is False 1653 assert self._history() is False 1654 assert self._history(True) 1655 assert self._history() 1656 1657 self._test_core(True, debug) 1658 self._test_core(False, debug) 1659 1660 transaction = [ 1661 ( 1662 20, 'wallet', 1, 800, 800, 800, 4, 5, 1663 -85, -85, -85, 6, 7, 1664 ), 1665 ( 1666 750, 'wallet', 'safe', 50, 50, 50, 4, 6, 1667 750, 750, 750, 1, 1, 1668 ), 1669 ( 1670 600, 'safe', 'bank', 150, 150, 150, 1, 2, 1671 600, 600, 600, 1, 1, 1672 ), 1673 ] 1674 for z in transaction: 1675 self.lock() 1676 x = z[1] 1677 y = z[2] 1678 self.transfer(z[0], x, y, 'test-transfer', debug=debug) 1679 assert self.balance(x) == z[3] 1680 xx = self.accounts()[x] 1681 assert xx == z[3] 1682 assert self.balance(x, False) == z[4] 1683 assert xx == z[4] 1684 1685 l = self._vault['account'][x]['log'] 1686 s = 0 1687 for i in l: 1688 s += l[i]['value'] 1689 if debug: 1690 print('s', s, 'z[5]', z[5]) 1691 assert s == z[5] 1692 1693 assert self.box_size(x) == z[6] 1694 assert self.log_size(x) == z[7] 1695 1696 yy = self.accounts()[y] 1697 assert self.balance(y) == z[8] 1698 assert yy == z[8] 1699 assert self.balance(y, False) == z[9] 1700 assert yy == z[9] 1701 1702 l = self._vault['account'][y]['log'] 1703 s = 0 1704 for i in l: 1705 s += l[i]['value'] 1706 assert s == z[10] 1707 1708 assert self.box_size(y) == z[11] 1709 assert self.log_size(y) == z[12] 1710 1711 if debug: 1712 pp().pprint(self.check(2.17)) 1713 1714 assert not self.nolock() 1715 history_count = len(self._vault['history']) 1716 if debug: 1717 print('history-count', history_count) 1718 assert history_count == 11 1719 assert not self.free(ZakatTracker.time()) 1720 assert self.free(self.lock()) 1721 assert self.nolock() 1722 assert len(self._vault['history']) == 11 1723 1724 # storage 1725 1726 _path = self.path('test.pickle') 1727 if os.path.exists(_path): 1728 os.remove(_path) 1729 self.save() 1730 assert os.path.getsize(_path) > 0 1731 self.reset() 1732 assert self.recall(False, debug) is False 1733 self.load() 1734 assert self._vault['account'] is not None 1735 1736 # recall 1737 1738 assert self.nolock() 1739 assert len(self._vault['history']) == 11 1740 assert self.recall(False, debug) is True 1741 assert len(self._vault['history']) == 10 1742 assert self.recall(False, debug) is True 1743 assert len(self._vault['history']) == 9 1744 1745 # csv 1746 1747 _path = "test.csv" 1748 count = 1000 1749 if os.path.exists(_path): 1750 os.remove(_path) 1751 self.generate_random_csv_file(_path, count) 1752 assert os.path.getsize(_path) > 0 1753 tmp = "tmp" 1754 if os.path.exists(tmp): 1755 os.remove(tmp) 1756 (created, found, bad) = self.import_csv(_path) 1757 bad_count = len(bad) 1758 if debug: 1759 print(f"csv-imported: ({created}, {found}, {bad_count})") 1760 tmp_size = os.path.getsize(tmp) 1761 assert tmp_size > 0 1762 assert created + found + bad_count == count 1763 assert created == count 1764 assert bad_count == 0 1765 (created_2, found_2, bad_2) = self.import_csv(_path) 1766 bad_2_count = len(bad_2) 1767 if debug: 1768 print(f"csv-imported: ({created_2}, {found_2}, {bad_2_count})") 1769 print(bad) 1770 assert tmp_size == os.path.getsize(tmp) 1771 assert created_2 + found_2 + bad_2_count == count 1772 assert created == found_2 1773 assert bad_count == bad_2_count 1774 assert found_2 == count 1775 assert bad_2_count == 0 1776 assert created_2 == 0 1777 1778 # payment parts 1779 1780 positive_parts = self.build_payment_parts(100, positive_only=True) 1781 assert self.check_payment_parts(positive_parts) != 0 1782 assert self.check_payment_parts(positive_parts) != 0 1783 all_parts = self.build_payment_parts(300, positive_only= False) 1784 assert self.check_payment_parts(all_parts) != 0 1785 assert self.check_payment_parts(all_parts) != 0 1786 if debug: 1787 pp().pprint(positive_parts) 1788 pp().pprint(all_parts) 1789 # dynamic discount 1790 suite = [] 1791 count = 3 1792 for exceed in [False, True]: 1793 case = [] 1794 for parts in [positive_parts, all_parts]: 1795 part = parts.copy() 1796 demand = part['demand'] 1797 if debug: 1798 print(demand, part['total']) 1799 i = 0 1800 z = demand / count 1801 cp = { 1802 'account': {}, 1803 'demand': demand, 1804 'exceed': exceed, 1805 'total': part['total'], 1806 } 1807 for x, y in part['account'].items(): 1808 if exceed and z <= demand: 1809 i += 1 1810 y['part'] = z 1811 if debug: 1812 print(exceed, y) 1813 cp['account'][x] = y 1814 case.append(y) 1815 elif not exceed and y['balance'] >= z: 1816 i += 1 1817 y['part'] = z 1818 if debug: 1819 print(exceed, y) 1820 cp['account'][x] = y 1821 case.append(y) 1822 if i >= count: 1823 break 1824 if len(cp['account'][x]) > 0: 1825 suite.append(cp) 1826 if debug: 1827 print('suite', len(suite)) 1828 for case in suite: 1829 if debug: 1830 print(case) 1831 result = self.check_payment_parts(case) 1832 if debug: 1833 print('check_payment_parts', result) 1834 assert result == 0 1835 1836 report = self.check(2.17, None, debug) 1837 (valid, brief, plan) = report 1838 if debug: 1839 print('valid', valid) 1840 assert self.zakat(report, parts=suite, debug=debug) 1841 1842 # exchange 1843 1844 self.exchange("cash", 25, 3.75, "2024-06-25") 1845 self.exchange("cash", 22, 3.73, "2024-06-22") 1846 self.exchange("cash", 15, 3.69, "2024-06-15") 1847 self.exchange("cash", 10, 3.66) 1848 1849 for i in range(1, 30): 1850 rate, description = self.exchange("cash", i).values() 1851 if debug: 1852 print(i, rate, description) 1853 if i < 10: 1854 assert rate == 1 1855 assert description is None 1856 elif i == 10: 1857 assert rate == 3.66 1858 assert description is None 1859 elif i < 15: 1860 assert rate == 3.66 1861 assert description is None 1862 elif i == 15: 1863 assert rate == 3.69 1864 assert description is not None 1865 elif i < 22: 1866 assert rate == 3.69 1867 assert description is not None 1868 elif i == 22: 1869 assert rate == 3.73 1870 assert description is not None 1871 elif i >= 25: 1872 assert rate == 3.75 1873 assert description is not None 1874 rate, description = self.exchange("bank", i).values() 1875 if debug: 1876 print(i, rate, description) 1877 assert rate == 1 1878 assert description is None 1879 1880 assert len(self._vault['exchange']) > 0 1881 assert len(self.exchanges()) > 0 1882 self._vault['exchange'].clear() 1883 assert len(self._vault['exchange']) == 0 1884 assert len(self.exchanges()) == 0 1885 1886 # حفظ أسعار الصرف باستخدام التواريخ بالنانو ثانية 1887 self.exchange("cash", ZakatTracker.day_to_time(25), 3.75, "2024-06-25") 1888 self.exchange("cash", ZakatTracker.day_to_time(22), 3.73, "2024-06-22") 1889 self.exchange("cash", ZakatTracker.day_to_time(15), 3.69, "2024-06-15") 1890 self.exchange("cash", ZakatTracker.day_to_time(10), 3.66) 1891 1892 for i in [x * 0.12 for x in range(-15, 21)]: 1893 if i <= 1: 1894 assert self.exchange("test", ZakatTracker.time(), i, f"range({i})") is None 1895 else: 1896 assert self.exchange("test", ZakatTracker.time(), i, f"range({i})") is not None 1897 1898 # اختبار النتائج باستخدام التواريخ بالنانو ثانية 1899 for i in range(1, 31): 1900 timestamp_ns = ZakatTracker.day_to_time(i) 1901 rate, description = self.exchange("cash", timestamp_ns).values() 1902 print(i, rate, description) 1903 if i < 10: 1904 assert rate == 1 1905 assert description is None 1906 elif i == 10: 1907 assert rate == 3.66 1908 assert description is None 1909 elif i < 15: 1910 assert rate == 3.66 1911 assert description is None 1912 elif i == 15: 1913 assert rate == 3.69 1914 assert description is not None 1915 elif i < 22: 1916 assert rate == 3.69 1917 assert description is not None 1918 elif i == 22: 1919 assert rate == 3.73 1920 assert description is not None 1921 elif i >= 25: 1922 assert rate == 3.75 1923 assert description is not None 1924 rate, description = self.exchange("bank", i).values() 1925 print(i, rate, description) 1926 assert rate == 1 1927 assert description is None 1928 1929 assert self.export_json("1000-transactions-test.json") 1930 assert self.save("1000-transactions-test.pickle") 1931 1932 self.reset() 1933 1934 # test transfer between accounts with different exchange rate 1935 1936 debug = True 1937 a_SAR = "Bank (SAR)" 1938 b_USD = "Bank (USD)" 1939 c_SAR = "Safe (SAR)" 1940 # 0: track, 1: check-exchange, 2: do-exchange, 3: transfer 1941 for case in[ 1942 (0, a_SAR, "SAR Gift", 1000, 1000), 1943 (1, a_SAR, 1), 1944 (0, b_USD, "USD Gift", 500, 500), 1945 (1, b_USD, 1), 1946 (2, b_USD, 3.75), 1947 (1, b_USD, 3.75), 1948 (3, 100, b_USD, a_SAR, "100 USD -> SAR", 400, 1375), 1949 (0, c_SAR, "Salary", 750, 750), 1950 (3, 375, c_SAR, b_USD, "375 SAR -> USD", 375, 500), 1951 (3, 3.75, a_SAR, b_USD, "3.75 SAR -> USD", 1371.25, 501), 1952 ]: 1953 match(case[0]): 1954 case 0: # track 1955 _, account, desc, x, balance = case 1956 self.track(value=x, desc=desc, account=account, debug=debug) 1957 1958 cached_value = self.balance(account, cached=True) 1959 fresh_value = self.balance(account, cached=False) 1960 if debug: 1961 print('account', account, 'cached_value', cached_value, 'fresh_value', fresh_value) 1962 assert cached_value == balance 1963 assert fresh_value == balance 1964 case 1: # check-exchange 1965 _, account, expected_rate = case 1966 t_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug) 1967 if debug: 1968 print('t-exchange', t_exchange) 1969 assert t_exchange['rate'] == expected_rate 1970 case 2: # do-exchange 1971 _, account, rate = case 1972 self.exchange(account, rate=rate, debug=debug) 1973 b_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug) 1974 if debug: 1975 print('b-exchange', b_exchange) 1976 assert b_exchange['rate'] == rate 1977 case 3: # transfer 1978 _, x, a, b, desc, a_balance, b_balance = case 1979 self.transfer(x, a, b, desc, debug=debug) 1980 1981 cached_value = self.balance(a, cached=True) 1982 fresh_value = self.balance(a, cached=False) 1983 if debug: 1984 print('account', a, 'cached_value', cached_value, 'fresh_value', fresh_value) 1985 assert cached_value == a_balance 1986 assert fresh_value == a_balance 1987 1988 cached_value = self.balance(b, cached=True) 1989 fresh_value = self.balance(b, cached=False) 1990 if debug: 1991 print('account', b, 'cached_value', cached_value, 'fresh_value', fresh_value) 1992 assert cached_value == b_balance 1993 assert fresh_value == b_balance 1994 1995 # Transfer all in many chunks randomly from B to A 1996 a_SAR_balance = 1371.25 1997 b_USD_balance = 501 1998 b_USD_exchange = self.exchange(b_USD) 1999 amounts = ZakatTracker.create_random_list(b_USD_balance) 2000 if debug: 2001 print('amounts', amounts) 2002 i = 0 2003 for x in amounts: 2004 if debug: 2005 print(f'{i} - transfer-with-exchnage({x})') 2006 self.transfer(x, b_USD, a_SAR, f"{x} USD -> SAR", debug=debug) 2007 2008 b_USD_balance -= x 2009 cached_value = self.balance(b_USD, cached=True) 2010 fresh_value = self.balance(b_USD, cached=False) 2011 if debug: 2012 print('account', b_USD, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted', b_USD_balance) 2013 assert cached_value == b_USD_balance 2014 assert fresh_value == b_USD_balance 2015 2016 a_SAR_balance += x * b_USD_exchange['rate'] 2017 cached_value = self.balance(a_SAR, cached=True) 2018 fresh_value = self.balance(a_SAR, cached=False) 2019 if debug: 2020 print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected', a_SAR_balance, 'rate', b_USD_exchange['rate']) 2021 assert cached_value == a_SAR_balance 2022 assert fresh_value == a_SAR_balance 2023 i += 1 2024 2025 # Transfer all in many chunks randomly from C to A 2026 c_SAR_balance = 375 2027 amounts = ZakatTracker.create_random_list(c_SAR_balance) 2028 if debug: 2029 print('amounts', amounts) 2030 i = 0 2031 for x in amounts: 2032 if debug: 2033 print(f'{i} - transfer-with-exchnage({x})') 2034 self.transfer(x, c_SAR, a_SAR, f"{x} SAR -> a_SAR", debug=debug) 2035 2036 c_SAR_balance -= x 2037 cached_value = self.balance(c_SAR, cached=True) 2038 fresh_value = self.balance(c_SAR, cached=False) 2039 if debug: 2040 print('account', c_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted', c_SAR_balance) 2041 assert cached_value == c_SAR_balance 2042 assert fresh_value == c_SAR_balance 2043 2044 a_SAR_balance += x 2045 cached_value = self.balance(a_SAR, cached=True) 2046 fresh_value = self.balance(a_SAR, cached=False) 2047 if debug: 2048 print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected', a_SAR_balance) 2049 assert cached_value == a_SAR_balance 2050 assert fresh_value == a_SAR_balance 2051 i += 1 2052 2053 assert self.export_json("accounts-transfer-with-exchange-rates.json") 2054 assert self.save("accounts-transfer-with-exchange-rates.pickle") 2055 2056 # check & zakat 2057 2058 cases = [ 2059 (1000, 'safe', ZakatTracker.time()-ZakatTracker.TimeCycle(), [ 2060 {'safe': {0: {'below_nisab': 25}}}, 2061 ], False), 2062 (2000, 'safe', ZakatTracker.time()-ZakatTracker.TimeCycle(), [ 2063 {'safe': {0: {'count': 1, 'total': 50}}}, 2064 ], True), 2065 (10000, 'cave', ZakatTracker.time()-(ZakatTracker.TimeCycle()*3), [ 2066 {'cave': {0: {'count': 3, 'total': 731.40625}}}, 2067 ], True), 2068 ] 2069 for case in cases: 2070 if debug: 2071 print("############# check #############") 2072 self.reset() 2073 self.track(case[0], 'test-check', case[1], True, case[2]) 2074 2075 assert self.nolock() 2076 assert len(self._vault['history']) == 1 2077 assert self.lock() 2078 assert not self.nolock() 2079 report = self.check(2.17, None, debug) 2080 (valid, brief, plan) = report 2081 assert valid == case[4] 2082 if debug: 2083 print(brief) 2084 assert case[0] == brief[0] 2085 assert case[0] == brief[1] 2086 2087 if debug: 2088 pp().pprint(plan) 2089 2090 for x in plan: 2091 assert case[1] == x 2092 if 'total' in case[3][0][x][0].keys(): 2093 assert case[3][0][x][0]['total'] == brief[2] 2094 assert plan[x][0]['total'] == case[3][0][x][0]['total'] 2095 assert plan[x][0]['count'] == case[3][0][x][0]['count'] 2096 else: 2097 assert plan[x][0]['below_nisab'] == case[3][0][x][0]['below_nisab'] 2098 if debug: 2099 pp().pprint(report) 2100 result = self.zakat(report, debug=debug) 2101 if debug: 2102 print('zakat-result', result, case[4]) 2103 assert result == case[4] 2104 report = self.check(2.17, None, debug) 2105 (valid, brief, plan) = report 2106 assert valid is False 2107 2108 assert len(self._vault['history']) == 2 2109 assert not self.nolock() 2110 assert self.recall(False, debug) is False 2111 self.free(self.lock()) 2112 assert self.nolock() 2113 assert len(self._vault['history']) == 2 2114 assert self.recall(False, debug) is True 2115 assert len(self._vault['history']) == 1 2116 2117 assert self.nolock() 2118 assert len(self._vault['history']) == 1 2119 2120 assert self.recall(False, debug) is True 2121 assert len(self._vault['history']) == 0 2122 2123 assert len(self._vault['account']) == 0 2124 assert len(self._vault['history']) == 0 2125 assert len(self._vault['report']) == 0 2126 assert self.nolock() 2127 except: 2128 # pp().pprint(self._vault) 2129 assert self.export_json("test-snapshot.json") 2130 assert self.save("test-snapshot.pickle") 2131 raise
A class for tracking and calculating Zakat.
This class provides functionalities for recording transactions, calculating Zakat due, and managing account balances. It also offers features like importing transactions from CSV files, exporting data to JSON format, and saving/loading the tracker state.
The ZakatTracker
class is designed to handle both positive and negative transactions,
allowing for flexible tracking of financial activities related to Zakat. It also supports
the concept of a "nisab" (minimum threshold for Zakat) and a "haul" (complete one year for Transaction) can calculate Zakat due
based on the current silver price.
The class uses a pickle file as its database to persist the tracker state, ensuring data integrity across sessions. It also provides options for enabling or disabling history tracking, allowing users to choose their preferred level of detail.
In addition, the ZakatTracker
class includes various helper methods like
time
, time_to_datetime
, lock
, free
, recall
, export_json
,
and more. These methods provide additional functionalities and flexibility
for interacting with and managing the Zakat tracker.
Attributes: ZakatCut (function): A function to calculate the Zakat percentage. TimeCycle (function): A function to determine the time cycle for Zakat. Nisab (function): A function to calculate the Nisab based on the silver price. Version (str): The version of the ZakatTracker class.
Data Structure:
The ZakatTracker class utilizes a nested dictionary structure called "_vault" to store and manage data.
_vault (dict):
- account (dict):
- {account_number} (dict):
- balance (int): The current balance of the account.
- box (dict): A dictionary storing transaction details.
- {timestamp} (dict):
- capital (int): The initial amount of the transaction.
- count (int): The number of times Zakat has been calculated for this transaction.
- last (int): The timestamp of the last Zakat calculation.
- rest (int): The remaining amount after Zakat deductions and withdrawal.
- total (int): The total Zakat deducted from this transaction.
- count (int): The total number of transactions for the account.
- log (dict): A dictionary storing transaction logs.
- {timestamp} (dict):
- value (int): The transaction amount (positive or negative).
- desc (str): The description of the transaction.
- file (dict): A dictionary storing file references associated with the transaction.
- zakatable (bool): Indicates whether the account is subject to Zakat.
- history (dict):
- {timestamp} (list): A list of dictionaries storing the history of actions performed.
- {action_dict} (dict):
- action (Action): The type of action (CREATE, TRACK, LOG, SUB, ADD_FILE, REMOVE_FILE, BOX_TRANSFER, EXCHANGE, REPORT, ZAKAT).
- account (str): The account number associated with the action.
- ref (int): The reference number of the transaction.
- file (int): The reference number of the file (if applicable).
- key (str): The key associated with the action (e.g., 'rest', 'total').
- value (int): The value associated with the action.
- math (MathOperation): The mathematical operation performed (if applicable).
- lock (int or None): The timestamp indicating the current lock status (None if not locked).
- report (dict):
- {timestamp} (tuple): A tuple storing Zakat report details.
162 def __init__(self, db_path: str = "zakat.pickle", history_mode: bool = True): 163 """ 164 Initialize ZakatTracker with database path and history mode. 165 166 Parameters: 167 db_path (str): The path to the database file. Default is "zakat.pickle". 168 history_mode (bool): The mode for tracking history. Default is True. 169 170 Returns: 171 None 172 """ 173 self.reset() 174 self._history(history_mode) 175 self.path(db_path) 176 self.load()
Initialize ZakatTracker with database path and history mode.
Parameters: db_path (str): The path to the database file. Default is "zakat.pickle". history_mode (bool): The mode for tracking history. Default is True.
Returns: None
158 TimeCycle = lambda days = 355: int(60*60*24*days*1e9) # Lunar Year in nanoseconds
178 def path(self, _path: str = None) -> str: 179 """ 180 Set or get the database path. 181 182 Parameters: 183 _path (str): The path to the database file. If not provided, it returns the current path. 184 185 Returns: 186 str: The current database path. 187 """ 188 if _path is not None: 189 self._vault_path = _path 190 return self._vault_path
Set or get the database path.
Parameters: _path (str): The path to the database file. If not provided, it returns the current path.
Returns: str: The current database path.
206 def reset(self) -> None: 207 """ 208 Reset the internal data structure to its initial state. 209 210 Parameters: 211 None 212 213 Returns: 214 None 215 """ 216 self._vault = {} 217 self._vault['account'] = {} 218 self._vault['exchange'] = {} 219 self._vault['history'] = {} 220 self._vault['lock'] = None 221 self._vault['report'] = {}
Reset the internal data structure to its initial state.
Parameters: None
Returns: None
223 @staticmethod 224 def time(now: datetime = None) -> int: 225 """ 226 Generates a timestamp based on the provided datetime object or the current datetime. 227 228 Parameters: 229 now (datetime, optional): The datetime object to generate the timestamp from. If not provided, the current datetime is used. 230 231 Returns: 232 int: The timestamp in positive nanoseconds since the Unix epoch (January 1, 1970), before 1970 will return in negative until 1000AD. 233 """ 234 if now is None: 235 now = datetime.datetime.now() 236 ordinal_day = now.toordinal() 237 ns_in_day = (now - now.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() * 10**9 238 return int((ordinal_day - 719_163) * 86_400_000_000_000 + ns_in_day)
Generates a timestamp based on the provided datetime object or the current datetime.
Parameters: now (datetime, optional): The datetime object to generate the timestamp from. If not provided, the current datetime is used.
Returns: int: The timestamp in positive nanoseconds since the Unix epoch (January 1, 1970), before 1970 will return in negative until 1000AD.
240 @staticmethod 241 def time_to_datetime(ordinal_ns: int) -> datetime: 242 """ 243 Converts an ordinal number (number of days since 1000-01-01) to a datetime object. 244 245 Parameters: 246 ordinal_ns (int): The ordinal number of days since 1000-01-01. 247 248 Returns: 249 datetime: The corresponding datetime object. 250 """ 251 ordinal_day = ordinal_ns // 86_400_000_000_000 + 719_163 252 ns_in_day = ordinal_ns % 86_400_000_000_000 253 d = datetime.datetime.fromordinal(ordinal_day) 254 t = datetime.timedelta(seconds=ns_in_day // 10**9) 255 return datetime.datetime.combine(d, datetime.time()) + t
Converts an ordinal number (number of days since 1000-01-01) to a datetime object.
Parameters: ordinal_ns (int): The ordinal number of days since 1000-01-01.
Returns: datetime: The corresponding datetime object.
291 def nolock(self) -> bool: 292 """ 293 Check if the vault lock is currently not set. 294 295 :return: True if the vault lock is not set, False otherwise. 296 """ 297 return self._vault['lock'] is None
Check if the vault lock is currently not set.
Returns
True if the vault lock is not set, False otherwise.
299 def lock(self) -> int: 300 """ 301 Acquires a lock on the ZakatTracker instance. 302 303 Returns: 304 int: The lock ID. This ID can be used to release the lock later. 305 """ 306 return self._step()
Acquires a lock on the ZakatTracker instance.
Returns: int: The lock ID. This ID can be used to release the lock later.
308 def box(self) -> dict: 309 """ 310 Returns a copy of the internal vault dictionary. 311 312 This method is used to retrieve the current state of the ZakatTracker object. 313 It provides a snapshot of the internal data structure, allowing for further 314 processing or analysis. 315 316 :return: A copy of the internal vault dictionary. 317 """ 318 return self._vault.copy()
Returns a copy of the internal vault dictionary.
This method is used to retrieve the current state of the ZakatTracker object. It provides a snapshot of the internal data structure, allowing for further processing or analysis.
Returns
A copy of the internal vault dictionary.
320 def steps(self) -> dict: 321 """ 322 Returns a copy of the history of steps taken in the ZakatTracker. 323 324 The history is a dictionary where each key is a unique identifier for a step, 325 and the corresponding value is a dictionary containing information about the step. 326 327 :return: A copy of the history of steps taken in the ZakatTracker. 328 """ 329 return self._vault['history'].copy()
Returns a copy of the history of steps taken in the ZakatTracker.
The history is a dictionary where each key is a unique identifier for a step, and the corresponding value is a dictionary containing information about the step.
Returns
A copy of the history of steps taken in the ZakatTracker.
331 def free(self, _lock: int, auto_save: bool = True) -> bool: 332 """ 333 Releases the lock on the database. 334 335 Parameters: 336 _lock (int): The lock ID to be released. 337 auto_save (bool): Whether to automatically save the database after releasing the lock. 338 339 Returns: 340 bool: True if the lock is successfully released and (optionally) saved, False otherwise. 341 """ 342 if _lock == self._vault['lock']: 343 self._vault['lock'] = None 344 if auto_save: 345 return self.save(self.path()) 346 return True 347 return False
Releases the lock on the database.
Parameters: _lock (int): The lock ID to be released. auto_save (bool): Whether to automatically save the database after releasing the lock.
Returns: bool: True if the lock is successfully released and (optionally) saved, False otherwise.
349 def account_exists(self, account) -> bool: 350 """ 351 Check if the given account exists in the vault. 352 353 Parameters: 354 account (str): The account number to check. 355 356 Returns: 357 bool: True if the account exists, False otherwise. 358 """ 359 return account in self._vault['account']
Check if the given account exists in the vault.
Parameters: account (str): The account number to check.
Returns: bool: True if the account exists, False otherwise.
361 def box_size(self, account) -> int: 362 """ 363 Calculate the size of the box for a specific account. 364 365 Parameters: 366 account (str): The account number for which the box size needs to be calculated. 367 368 Returns: 369 int: The size of the box for the given account. If the account does not exist, -1 is returned. 370 """ 371 if self.account_exists(account): 372 return len(self._vault['account'][account]['box']) 373 return -1
Calculate the size of the box for a specific account.
Parameters: account (str): The account number for which the box size needs to be calculated.
Returns: int: The size of the box for the given account. If the account does not exist, -1 is returned.
375 def log_size(self, account) -> int: 376 """ 377 Get the size of the log for a specific account. 378 379 Parameters: 380 account (str): The account number for which the log size needs to be calculated. 381 382 Returns: 383 int: The size of the log for the given account. If the account does not exist, -1 is returned. 384 """ 385 if self.account_exists(account): 386 return len(self._vault['account'][account]['log']) 387 return -1
Get the size of the log for a specific account.
Parameters: account (str): The account number for which the log size needs to be calculated.
Returns: int: The size of the log for the given account. If the account does not exist, -1 is returned.
389 def recall(self, dry = True, debug = False) -> bool: 390 """ 391 Revert the last operation. 392 393 Parameters: 394 dry (bool): If True, the function will not modify the data, but will simulate the operation. Default is True. 395 debug (bool): If True, the function will print debug information. Default is False. 396 397 Returns: 398 bool: True if the operation was successful, False otherwise. 399 """ 400 if not self.nolock() or len(self._vault['history']) == 0: 401 return False 402 if len(self._vault['history']) <= 0: 403 return 404 ref = sorted(self._vault['history'].keys())[-1] 405 if debug: 406 print('recall', ref) 407 memory = self._vault['history'][ref] 408 if debug: 409 print(type(memory), 'memory', memory) 410 411 limit = len(memory) + 1 412 sub_positive_log_negative = 0 413 for i in range(-1, -limit, -1): 414 x = memory[i] 415 if debug: 416 print(type(x), x) 417 match x['action']: 418 case Action.CREATE: 419 if x['account'] is not None: 420 if self.account_exists(x['account']): 421 if debug: 422 print('account', self._vault['account'][x['account']]) 423 assert len(self._vault['account'][x['account']]['box']) == 0 424 assert self._vault['account'][x['account']]['balance'] == 0 425 assert self._vault['account'][x['account']]['count'] == 0 426 if dry: 427 continue 428 del self._vault['account'][x['account']] 429 430 case Action.TRACK: 431 if x['account'] is not None: 432 if self.account_exists(x['account']): 433 if dry: 434 continue 435 self._vault['account'][x['account']]['balance'] -= x['value'] 436 self._vault['account'][x['account']]['count'] -= 1 437 del self._vault['account'][x['account']]['box'][x['ref']] 438 439 case Action.LOG: 440 if x['account'] is not None: 441 if self.account_exists(x['account']): 442 if x['ref'] in self._vault['account'][x['account']]['log']: 443 if dry: 444 continue 445 if sub_positive_log_negative == -x['value']: 446 self._vault['account'][x['account']]['count'] -= 1 447 sub_positive_log_negative = 0 448 del self._vault['account'][x['account']]['log'][x['ref']] 449 450 case Action.SUB: 451 if x['account'] is not None: 452 if self.account_exists(x['account']): 453 if x['ref'] in self._vault['account'][x['account']]['box']: 454 if dry: 455 continue 456 self._vault['account'][x['account']]['box'][x['ref']]['rest'] += x['value'] 457 self._vault['account'][x['account']]['balance'] += x['value'] 458 sub_positive_log_negative = x['value'] 459 460 case Action.ADD_FILE: 461 if x['account'] is not None: 462 if self.account_exists(x['account']): 463 if x['ref'] in self._vault['account'][x['account']]['log']: 464 if x['file'] in self._vault['account'][x['account']]['log'][x['ref']]['file']: 465 if dry: 466 continue 467 del self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']] 468 469 case Action.REMOVE_FILE: 470 if x['account'] is not None: 471 if self.account_exists(x['account']): 472 if x['ref'] in self._vault['account'][x['account']]['log']: 473 if dry: 474 continue 475 self._vault['account'][x['account']]['log'][x['ref']]['file'][x['file']] = x['value'] 476 477 case Action.BOX_TRANSFER: 478 if x['account'] is not None: 479 if self.account_exists(x['account']): 480 if x['ref'] in self._vault['account'][x['account']]['box']: 481 if dry: 482 continue 483 self._vault['account'][x['account']]['box'][x['ref']]['rest'] -= x['value'] 484 485 case Action.EXCHANGE: 486 if x['account'] is not None: 487 if x['account'] in self._vault['exchange']: 488 if x['ref'] in self._vault['exchange'][x['account']]: 489 if dry: 490 continue 491 del self._vault['exchange'][x['account']][x['ref']] 492 493 case Action.REPORT: 494 if x['ref'] in self._vault['report']: 495 if dry: 496 continue 497 del self._vault['report'][x['ref']] 498 499 case Action.ZAKAT: 500 if x['account'] is not None: 501 if self.account_exists(x['account']): 502 if x['ref'] in self._vault['account'][x['account']]['box']: 503 if x['key'] in self._vault['account'][x['account']]['box'][x['ref']]: 504 if dry: 505 continue 506 match x['math']: 507 case MathOperation.ADDITION: 508 self._vault['account'][x['account']]['box'][x['ref']][x['key']] -= x['value'] 509 case MathOperation.EQUAL: 510 self._vault['account'][x['account']]['box'][x['ref']][x['key']] = x['value'] 511 case MathOperation.SUBTRACTION: 512 self._vault['account'][x['account']]['box'][x['ref']][x['key']] += x['value'] 513 514 if not dry: 515 del self._vault['history'][ref] 516 return True
Revert the last operation.
Parameters: dry (bool): If True, the function will not modify the data, but will simulate the operation. Default is True. debug (bool): If True, the function will print debug information. Default is False.
Returns: bool: True if the operation was successful, False otherwise.
518 def ref_exists(self, account: str, ref_type: str, ref: int) -> bool: 519 """ 520 Check if a specific reference (transaction) exists in the vault for a given account and reference type. 521 522 Parameters: 523 account (str): The account number for which to check the existence of the reference. 524 ref_type (str): The type of reference (e.g., 'box', 'log', etc.). 525 ref (int): The reference (transaction) number to check for existence. 526 527 Returns: 528 bool: True if the reference exists for the given account and reference type, False otherwise. 529 """ 530 if account in self._vault['account']: 531 return ref in self._vault['account'][account][ref_type] 532 return False
Check if a specific reference (transaction) exists in the vault for a given account and reference type.
Parameters: account (str): The account number for which to check the existence of the reference. ref_type (str): The type of reference (e.g., 'box', 'log', etc.). ref (int): The reference (transaction) number to check for existence.
Returns: bool: True if the reference exists for the given account and reference type, False otherwise.
534 def box_exists(self, account: str, ref: int) -> bool: 535 """ 536 Check if a specific box (transaction) exists in the vault for a given account and reference. 537 538 Parameters: 539 - account (str): The account number for which to check the existence of the box. 540 - ref (int): The reference (transaction) number to check for existence. 541 542 Returns: 543 - bool: True if the box exists for the given account and reference, False otherwise. 544 """ 545 return self.ref_exists(account, 'box', ref)
Check if a specific box (transaction) exists in the vault for a given account and reference.
Parameters:
- account (str): The account number for which to check the existence of the box.
- ref (int): The reference (transaction) number to check for existence.
Returns:
- bool: True if the box exists for the given account and reference, False otherwise.
547 def track(self, value: int = 0, desc: str = '', account: str = 1, logging: bool = True, created: int = None, debug: bool = False) -> int: 548 """ 549 This function tracks a transaction for a specific account. 550 551 Parameters: 552 value (int): The value of the transaction. Default is 0. 553 desc (str): The description of the transaction. Default is an empty string. 554 account (str): The account for which the transaction is being tracked. Default is '1'. 555 logging (bool): Whether to log the transaction. Default is True. 556 created (int): The timestamp of the transaction. If not provided, it will be generated. Default is None. 557 debug (bool): Whether to print debug information. Default is False. 558 559 Returns: 560 int: The timestamp of the transaction. 561 562 This function creates a new account if it doesn't exist, logs the transaction if logging is True, and updates the account's balance and box. 563 564 Raises: 565 ValueError: If the box transction happend again in the same nanosecond time. 566 """ 567 if created is None: 568 created = self.time() 569 _nolock = self.nolock(); self.lock() 570 if not self.account_exists(account): 571 if debug: 572 print(f"account {account} created") 573 self._vault['account'][account] = { 574 'balance': 0, 575 'box': {}, 576 'count': 0, 577 'log': {}, 578 'zakatable': True, 579 } 580 self._step(Action.CREATE, account) 581 if value == 0: 582 if _nolock: self.free(self.lock()) 583 return 0 584 if logging: 585 self._log(value, desc, account, created, debug) 586 if debug: 587 print('create-box', created) 588 if self.box_exists(account, created): 589 raise ValueError(f"The box transction happend again in the same nanosecond time({created}).") 590 if debug: 591 print('created-box', created) 592 self._vault['account'][account]['box'][created] = { 593 'capital': value, 594 'count': 0, 595 'last': 0, 596 'rest': value, 597 'total': 0, 598 } 599 self._step(Action.TRACK, account, ref=created, value=value) 600 if _nolock: self.free(self.lock()) 601 return created
This function tracks a transaction for a specific account.
Parameters: value (int): The value of the transaction. Default is 0. desc (str): The description of the transaction. Default is an empty string. account (str): The account for which the transaction is being tracked. Default is '1'. logging (bool): Whether to log the transaction. Default is True. created (int): The timestamp of the transaction. If not provided, it will be generated. Default is None. debug (bool): Whether to print debug information. Default is False.
Returns: int: The timestamp of the transaction.
This function creates a new account if it doesn't exist, logs the transaction if logging is True, and updates the account's balance and box.
Raises:
ValueError: If the box transction happend again in the same nanosecond time.
603 def log_exists(self, account: str, ref: int) -> bool: 604 """ 605 Checks if a specific transaction log entry exists for a given account. 606 607 Parameters: 608 account (str): The account number associated with the transaction log. 609 ref (int): The reference to the transaction log entry. 610 611 Returns: 612 bool: True if the transaction log entry exists, False otherwise. 613 """ 614 return self.ref_exists(account, 'log', ref)
Checks if a specific transaction log entry exists for a given account.
Parameters: account (str): The account number associated with the transaction log. ref (int): The reference to the transaction log entry.
Returns: bool: True if the transaction log entry exists, False otherwise.
653 def exchange(self, account, created: int = None, rate: float = None, description: str = None, debug: bool = False) -> dict: 654 """ 655 This method is used to record or retrieve exchange rates for a specific account. 656 657 Parameters: 658 - account (str): The account number for which the exchange rate is being recorded or retrieved. 659 - created (int): The timestamp of the exchange rate. If not provided, the current timestamp will be used. 660 - rate (float): The exchange rate to be recorded. If not provided, the method will retrieve the latest exchange rate. 661 - description (str): A description of the exchange rate. 662 663 Returns: 664 - dict: A dictionary containing the latest exchange rate and its description. If no exchange rate is found, 665 it returns a dictionary with default values for the rate and description. 666 """ 667 if created is None: 668 created = self.time() 669 if rate is not None: 670 if rate <= 1: 671 return None 672 if account not in self._vault['exchange']: 673 self._vault['exchange'][account] = {} 674 self._vault['exchange'][account][created] = {"rate": rate, "description": description} 675 self._step(Action.EXCHANGE, account, ref=created, value=rate) 676 if debug: 677 print("exchange-created-1", f'account: {account}, created: {created}, rate:{rate}, description:{description}') 678 679 if account in self._vault['exchange']: 680 valid_rates = [(ts, r) for ts, r in self._vault['exchange'][account].items() if ts <= created] 681 if valid_rates: 682 latest_rate = max(valid_rates, key=lambda x: x[0]) 683 if debug: 684 print("exchange-read-1", f'account: {account}, created: {created}, rate:{rate}, description:{description}', 'latest_rate', latest_rate) 685 return latest_rate[1] # إرجاع قاموس يحتوي على المعدل والوصف 686 if debug: 687 print("exchange-read-0", f'account: {account}, created: {created}, rate:{rate}, description:{description}') 688 return {"rate": 1, "description": None} # إرجاع القيمة الافتراضية مع وصف فارغ
This method is used to record or retrieve exchange rates for a specific account.
Parameters:
- account (str): The account number for which the exchange rate is being recorded or retrieved.
- created (int): The timestamp of the exchange rate. If not provided, the current timestamp will be used.
- rate (float): The exchange rate to be recorded. If not provided, the method will retrieve the latest exchange rate.
- description (str): A description of the exchange rate.
Returns:
- dict: A dictionary containing the latest exchange rate and its description. If no exchange rate is found, it returns a dictionary with default values for the rate and description.
690 def exchanges(self) -> dict: 691 """ 692 Retrieve the recorded exchange rates for all accounts. 693 694 Parameters: 695 None 696 697 Returns: 698 dict: A dictionary containing all recorded exchange rates. 699 The keys are account names or numbers, and the values are dictionaries containing the exchange rates. 700 Each exchange rate dictionary has timestamps as keys and exchange rate details as values. 701 """ 702 return self._vault['exchange'].copy()
Retrieve the recorded exchange rates for all accounts.
Parameters: None
Returns: dict: A dictionary containing all recorded exchange rates. The keys are account names or numbers, and the values are dictionaries containing the exchange rates. Each exchange rate dictionary has timestamps as keys and exchange rate details as values.
704 def accounts(self) -> dict: 705 """ 706 Returns a dictionary containing account numbers as keys and their respective balances as values. 707 708 Parameters: 709 None 710 711 Returns: 712 dict: A dictionary where keys are account numbers and values are their respective balances. 713 """ 714 result = {} 715 for i in self._vault['account']: 716 result[i] = self._vault['account'][i]['balance'] 717 return result
Returns a dictionary containing account numbers as keys and their respective balances as values.
Parameters: None
Returns: dict: A dictionary where keys are account numbers and values are their respective balances.
719 def boxes(self, account) -> dict: 720 """ 721 Retrieve the boxes (transactions) associated with a specific account. 722 723 Parameters: 724 account (str): The account number for which to retrieve the boxes. 725 726 Returns: 727 dict: A dictionary containing the boxes associated with the given account. 728 If the account does not exist, an empty dictionary is returned. 729 """ 730 if self.account_exists(account): 731 return self._vault['account'][account]['box'] 732 return {}
Retrieve the boxes (transactions) associated with a specific account.
Parameters: account (str): The account number for which to retrieve the boxes.
Returns: dict: A dictionary containing the boxes associated with the given account. If the account does not exist, an empty dictionary is returned.
734 def logs(self, account) -> dict: 735 """ 736 Retrieve the logs (transactions) associated with a specific account. 737 738 Parameters: 739 account (str): The account number for which to retrieve the logs. 740 741 Returns: 742 dict: A dictionary containing the logs associated with the given account. 743 If the account does not exist, an empty dictionary is returned. 744 """ 745 if self.account_exists(account): 746 return self._vault['account'][account]['log'] 747 return {}
Retrieve the logs (transactions) associated with a specific account.
Parameters: account (str): The account number for which to retrieve the logs.
Returns: dict: A dictionary containing the logs associated with the given account. If the account does not exist, an empty dictionary is returned.
749 def add_file(self, account: str, ref: int, path: str) -> int: 750 """ 751 Adds a file reference to a specific transaction log entry in the vault. 752 753 Parameters: 754 account (str): The account number associated with the transaction log. 755 ref (int): The reference to the transaction log entry. 756 path (str): The path of the file to be added. 757 758 Returns: 759 int: The reference of the added file. If the account or transaction log entry does not exist, returns 0. 760 """ 761 if self.account_exists(account): 762 if ref in self._vault['account'][account]['log']: 763 file_ref = self.time() 764 self._vault['account'][account]['log'][ref]['file'][file_ref] = path 765 _nolock = self.nolock(); self.lock() 766 self._step(Action.ADD_FILE, account, ref=ref, file=file_ref) 767 if _nolock: self.free(self.lock()) 768 return file_ref 769 return 0
Adds a file reference to a specific transaction log entry in the vault.
Parameters: account (str): The account number associated with the transaction log. ref (int): The reference to the transaction log entry. path (str): The path of the file to be added.
Returns: int: The reference of the added file. If the account or transaction log entry does not exist, returns 0.
771 def remove_file(self, account: str, ref: int, file_ref: int) -> bool: 772 """ 773 Removes a file reference from a specific transaction log entry in the vault. 774 775 Parameters: 776 account (str): The account number associated with the transaction log. 777 ref (int): The reference to the transaction log entry. 778 file_ref (int): The reference of the file to be removed. 779 780 Returns: 781 bool: True if the file reference is successfully removed, False otherwise. 782 """ 783 if self.account_exists(account): 784 if ref in self._vault['account'][account]['log']: 785 if file_ref in self._vault['account'][account]['log'][ref]['file']: 786 x = self._vault['account'][account]['log'][ref]['file'][file_ref] 787 del self._vault['account'][account]['log'][ref]['file'][file_ref] 788 _nolock = self.nolock(); self.lock() 789 self._step(Action.REMOVE_FILE, account, ref=ref, file=file_ref, value=x) 790 if _nolock: self.free(self.lock()) 791 return True 792 return False
Removes a file reference from a specific transaction log entry in the vault.
Parameters: account (str): The account number associated with the transaction log. ref (int): The reference to the transaction log entry. file_ref (int): The reference of the file to be removed.
Returns: bool: True if the file reference is successfully removed, False otherwise.
794 def balance(self, account: str = 1, cached: bool = True) -> int: 795 """ 796 Calculate and return the balance of a specific account. 797 798 Parameters: 799 account (str): The account number. Default is '1'. 800 cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True. 801 802 Returns: 803 int: The balance of the account. 804 805 Note: 806 If cached is True, the function returns the cached balance. 807 If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items. 808 """ 809 if cached: 810 return self._vault['account'][account]['balance'] 811 x = 0 812 return [x := x + y['rest'] for y in self._vault['account'][account]['box'].values()][-1]
Calculate and return the balance of a specific account.
Parameters: account (str): The account number. Default is '1'. cached (bool): If True, use the cached balance. If False, calculate the balance from the box. Default is True.
Returns: int: The balance of the account.
Note: If cached is True, the function returns the cached balance. If cached is False, the function calculates the balance from the box by summing up the 'rest' values of all box items.
814 def zakatable(self, account, status: bool = None) -> bool: 815 """ 816 Check or set the zakatable status of a specific account. 817 818 Parameters: 819 account (str): The account number. 820 status (bool, optional): The new zakatable status. If not provided, the function will return the current status. 821 822 Returns: 823 bool: The current or updated zakatable status of the account. 824 825 Raises: 826 None 827 828 Example: 829 >>> tracker = ZakatTracker() 830 >>> tracker.zakatable('account1', True) # Set the zakatable status of 'account1' to True 831 True 832 >>> tracker.zakatable('account1') # Get the zakatable status of 'account1' 833 True 834 """ 835 if self.account_exists(account): 836 if status is None: 837 return self._vault['account'][account]['zakatable'] 838 self._vault['account'][account]['zakatable'] = status 839 return status 840 return False
Check or set the zakatable status of a specific account.
Parameters: account (str): The account number. status (bool, optional): The new zakatable status. If not provided, the function will return the current status.
Returns: bool: The current or updated zakatable status of the account.
Raises: None
Example:
>>> tracker = ZakatTracker()
>>> tracker.zakatable('account1', True) # Set the zakatable status of 'account1' to True
True
>>> tracker.zakatable('account1') # Get the zakatable status of 'account1'
True
842 def sub(self, x: int, desc: str = '', account: str = 1, created: int = None, debug: bool = False) -> tuple: 843 """ 844 Subtracts a specified value from an account's balance. 845 846 Parameters: 847 x (int): The amount to be subtracted. 848 desc (str): A description for the transaction. Defaults to an empty string. 849 account (str): The account from which the value will be subtracted. Defaults to '1'. 850 created (int): The timestamp of the transaction. If not provided, the current timestamp will be used. 851 debug (bool): A flag indicating whether to print debug information. Defaults to False. 852 853 Returns: 854 tuple: A tuple containing the timestamp of the transaction and a list of tuples representing the age of each transaction. 855 856 If the amount to subtract is greater than the account's balance, 857 the remaining amount will be transferred to a new transaction with a negative value. 858 859 Raises: 860 ValueError: If the transction happend again in the same nanosecond time. 861 """ 862 if x < 0: 863 return 864 if x == 0: 865 return self.track(x, '', account) 866 if created is None: 867 created = self.time() 868 _nolock = self.nolock(); self.lock() 869 self.track(0, '', account) 870 self._log(-x, desc, account, created) 871 ids = sorted(self._vault['account'][account]['box'].keys()) 872 limit = len(ids) + 1 873 target = x 874 if debug: 875 print('ids', ids) 876 ages = [] 877 for i in range(-1, -limit, -1): 878 if target == 0: 879 break 880 j = ids[i] 881 if debug: 882 print('i', i, 'j', j) 883 if self._vault['account'][account]['box'][j]['rest'] >= target: 884 self._vault['account'][account]['box'][j]['rest'] -= target 885 self._step(Action.SUB, account, ref=j, value=target) 886 ages.append((j, target)) 887 target = 0 888 break 889 elif self._vault['account'][account]['box'][j]['rest'] < target and self._vault['account'][account]['box'][j]['rest'] > 0: 890 chunk = self._vault['account'][account]['box'][j]['rest'] 891 target -= chunk 892 self._step(Action.SUB, account, ref=j, value=chunk) 893 ages.append((j, chunk)) 894 self._vault['account'][account]['box'][j]['rest'] = 0 895 if target > 0: 896 self.track(-target, desc, account, False, created) 897 ages.append((created, target)) 898 if _nolock: self.free(self.lock()) 899 return (created, ages)
Subtracts a specified value from an account's balance.
Parameters: x (int): The amount to be subtracted. desc (str): A description for the transaction. Defaults to an empty string. account (str): The account from which the value will be subtracted. Defaults to '1'. created (int): The timestamp of the transaction. If not provided, the current timestamp will be used. debug (bool): A flag indicating whether to print debug information. Defaults to False.
Returns: tuple: A tuple containing the timestamp of the transaction and a list of tuples representing the age of each transaction.
If the amount to subtract is greater than the account's balance, the remaining amount will be transferred to a new transaction with a negative value.
Raises: ValueError: If the transction happend again in the same nanosecond time.
901 def transfer(self, amount: int, from_account: str, to_account: str, desc: str = '', created: int = None, debug: bool = False) -> list[int]: 902 """ 903 Transfers a specified value from one account to another. 904 905 Parameters: 906 amount (int): The amount to be transferred. 907 from_account (str): The account from which the value will be transferred. 908 to_account (str): The account to which the value will be transferred. 909 desc (str, optional): A description for the transaction. Defaults to an empty string. 910 created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used. 911 debug (bool): A flag indicating whether to print debug information. Defaults to False. 912 913 Returns: 914 list[int]: A list of timestamps corresponding to the transactions made during the transfer. 915 916 Raises: 917 ValueError: If the transction happend again in the same nanosecond time. 918 """ 919 if amount <= 0: 920 return None 921 if created is None: 922 created = self.time() 923 (_, ages) = self.sub(amount, desc, from_account, created, debug=debug) 924 times = [] 925 source_exchange = self.exchange(from_account, created) 926 target_exchange = self.exchange(to_account, created) 927 928 if debug: 929 print('ages', ages) 930 931 for age, value in ages: 932 # Convert source amount to the base currency 933 source_amount_base = value * source_exchange['rate'] 934 # Convert base amount to the target currency 935 target_amount = source_amount_base / target_exchange['rate'] 936 # Perform the transfer 937 if self.box_exists(to_account, age): 938 if debug: 939 print('box_exists', age) 940 capital = self._vault['account'][to_account]['box'][age]['capital'] 941 rest = self._vault['account'][to_account]['box'][age]['rest'] 942 if debug: 943 print(f"Transfer {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).") 944 selected_age = age 945 if rest + target_amount > capital: 946 self._vault['account'][to_account]['box'][age]['capital'] += target_amount 947 selected_age = ZakatTracker.time() 948 self._vault['account'][to_account]['box'][age]['rest'] += target_amount 949 self._step(Action.BOX_TRANSFER, to_account, ref=selected_age, value=target_amount) 950 y = self._log(target_amount, desc=f'TRANSFER {from_account} -> {to_account}', account=to_account, debug=debug) 951 times.append((age, y)) 952 continue 953 y = self.track(target_amount, desc, to_account, logging=True, created=age, debug=debug) 954 if debug: 955 print(f"Transferred {value} from `{from_account}` to `{to_account}` (equivalent to {target_amount} `{to_account}`).") 956 times.append(y) 957 return times
Transfers a specified value from one account to another.
Parameters: amount (int): The amount to be transferred. from_account (str): The account from which the value will be transferred. to_account (str): The account to which the value will be transferred. desc (str, optional): A description for the transaction. Defaults to an empty string. created (int, optional): The timestamp of the transaction. If not provided, the current timestamp will be used. debug (bool): A flag indicating whether to print debug information. Defaults to False.
Returns: list[int]: A list of timestamps corresponding to the transactions made during the transfer.
Raises: ValueError: If the transction happend again in the same nanosecond time.
959 def check(self, silver_gram_price: float, nisab: float = None, debug: bool = False, now: int = None, cycle: float = None) -> tuple: 960 """ 961 Check the eligibility for Zakat based on the given parameters. 962 963 Parameters: 964 silver_gram_price (float): The price of a gram of silver. 965 nisab (float): The minimum amount of wealth required for Zakat. If not provided, it will be calculated based on the silver_gram_price. 966 debug (bool): Flag to enable debug mode. 967 now (int): The current timestamp. If not provided, it will be calculated using ZakatTracker.time(). 968 cycle (float): The time cycle for Zakat. If not provided, it will be calculated using ZakatTracker.TimeCycle(). 969 970 Returns: 971 tuple: A tuple containing a boolean indicating the eligibility for Zakat, a list of brief statistics, and a dictionary containing the Zakat plan. 972 """ 973 if now is None: 974 now = self.time() 975 if cycle is None: 976 cycle = ZakatTracker.TimeCycle() 977 if nisab is None: 978 nisab = ZakatTracker.Nisab(silver_gram_price) 979 plan = {} 980 below_nisab = 0 981 brief = [0, 0, 0] 982 valid = False 983 for x in self._vault['account']: 984 if not self._vault['account'][x]['zakatable']: 985 continue 986 _box = self._vault['account'][x]['box'] 987 limit = len(_box) + 1 988 ids = sorted(self._vault['account'][x]['box'].keys()) 989 for i in range(-1, -limit, -1): 990 j = ids[i] 991 if _box[j]['rest'] <= 0: 992 continue 993 brief[0] += _box[j]['rest'] 994 index = limit + i - 1 995 epoch = (now - j) / cycle 996 if debug: 997 print(f"Epoch: {epoch}", _box[j]) 998 if _box[j]['last'] > 0: 999 epoch = (now - _box[j]['last']) / cycle 1000 if debug: 1001 print(f"Epoch: {epoch}") 1002 epoch = floor(epoch) 1003 if debug: 1004 print(f"Epoch: {epoch}", type(epoch), epoch == 0, 1-epoch, epoch) 1005 if epoch == 0: 1006 continue 1007 if debug: 1008 print("Epoch - PASSED") 1009 brief[1] += _box[j]['rest'] 1010 if _box[j]['rest'] >= nisab: 1011 total = 0 1012 for _ in range(epoch): 1013 total += ZakatTracker.ZakatCut(_box[j]['rest'] - total) 1014 if total > 0: 1015 if x not in plan: 1016 plan[x] = {} 1017 valid = True 1018 brief[2] += total 1019 plan[x][index] = {'total': total, 'count': epoch} 1020 else: 1021 chunk = ZakatTracker.ZakatCut(_box[j]['rest']) 1022 if chunk > 0: 1023 if x not in plan: 1024 plan[x] = {} 1025 if j not in plan[x].keys(): 1026 plan[x][index] = {} 1027 below_nisab += _box[j]['rest'] 1028 brief[2] += chunk 1029 plan[x][index]['below_nisab'] = chunk 1030 valid = valid or below_nisab >= nisab 1031 if debug: 1032 print(f"below_nisab({below_nisab}) >= nisab({nisab})") 1033 return (valid, brief, plan)
Check the eligibility for Zakat based on the given parameters.
Parameters: silver_gram_price (float): The price of a gram of silver. nisab (float): The minimum amount of wealth required for Zakat. If not provided, it will be calculated based on the silver_gram_price. debug (bool): Flag to enable debug mode. now (int): The current timestamp. If not provided, it will be calculated using ZakatTracker.time(). cycle (float): The time cycle for Zakat. If not provided, it will be calculated using ZakatTracker.TimeCycle().
Returns: tuple: A tuple containing a boolean indicating the eligibility for Zakat, a list of brief statistics, and a dictionary containing the Zakat plan.
1035 def build_payment_parts(self, demand: float, positive_only: bool = True) -> dict: 1036 """ 1037 Build payment parts for the zakat distribution. 1038 1039 Parameters: 1040 demand (float): The total demand for payment. 1041 positive_only (bool): If True, only consider accounts with positive balance. Default is True. 1042 1043 Returns: 1044 dict: A dictionary containing the payment parts for each account. The dictionary has the following structure: 1045 { 1046 'account': { 1047 'account_id': {'balance': float, 'part': float}, 1048 ... 1049 }, 1050 'exceed': bool, 1051 'demand': float, 1052 'total': float, 1053 } 1054 """ 1055 total = 0 1056 parts = { 1057 'account': {}, 1058 'exceed': False, 1059 'demand': demand, 1060 } 1061 for x, y in self.accounts().items(): 1062 if positive_only and y <= 0: 1063 continue 1064 total += y 1065 parts['account'][x] = {'balance': y, 'part': 0} 1066 parts['total'] = total 1067 return parts
Build payment parts for the zakat distribution.
Parameters: demand (float): The total demand for payment. positive_only (bool): If True, only consider accounts with positive balance. Default is True.
Returns: dict: A dictionary containing the payment parts for each account. The dictionary has the following structure: { 'account': { 'account_id': {'balance': float, 'part': float}, ... }, 'exceed': bool, 'demand': float, 'total': float, }
1069 def check_payment_parts(self, parts: dict) -> int: 1070 """ 1071 Checks the validity of payment parts. 1072 1073 Parameters: 1074 parts (dict): A dictionary containing payment parts information. 1075 1076 Returns: 1077 int: Returns 0 if the payment parts are valid, otherwise returns the error code. 1078 1079 Error Codes: 1080 1: 'demand', 'account', 'total', or 'exceed' key is missing in parts. 1081 2: 'balance' or 'part' key is missing in parts['account'][x]. 1082 3: 'part' value in parts['account'][x] is less than or equal to 0. 1083 4: If 'exceed' is False, 'balance' value in parts['account'][x] is less than or equal to 0. 1084 5: 'part' value in parts['account'][x] is less than 0. 1085 6: If 'exceed' is False, 'part' value in parts['account'][x] is greater than 'balance' value. 1086 7: The sum of 'part' values in parts['account'] does not match with 'demand' value. 1087 """ 1088 for i in ['demand', 'account', 'total', 'exceed']: 1089 if not i in parts: 1090 return 1 1091 exceed = parts['exceed'] 1092 for x in parts['account']: 1093 for j in ['balance', 'part']: 1094 if not j in parts['account'][x]: 1095 return 2 1096 if parts['account'][x]['part'] <= 0: 1097 return 3 1098 if not exceed and parts['account'][x]['balance'] <= 0: 1099 return 4 1100 demand = parts['demand'] 1101 z = 0 1102 for _, y in parts['account'].items(): 1103 if y['part'] < 0: 1104 return 5 1105 if not exceed and y['part'] > y['balance']: 1106 return 6 1107 z += y['part'] 1108 if z != demand: 1109 return 7 1110 return 0
Checks the validity of payment parts.
Parameters: parts (dict): A dictionary containing payment parts information.
Returns: int: Returns 0 if the payment parts are valid, otherwise returns the error code.
Error Codes: 1: 'demand', 'account', 'total', or 'exceed' key is missing in parts. 2: 'balance' or 'part' key is missing in parts['account'][x]. 3: 'part' value in parts['account'][x] is less than or equal to 0. 4: If 'exceed' is False, 'balance' value in parts['account'][x] is less than or equal to 0. 5: 'part' value in parts['account'][x] is less than 0. 6: If 'exceed' is False, 'part' value in parts['account'][x] is greater than 'balance' value. 7: The sum of 'part' values in parts['account'] does not match with 'demand' value.
1112 def zakat(self, report: tuple, parts: dict = None, debug: bool = False) -> bool: 1113 """ 1114 Perform Zakat calculation based on the given report and optional parts. 1115 1116 Parameters: 1117 report (tuple): A tuple containing the validity of the report, the report data, and the zakat plan. 1118 parts (dict): A dictionary containing the payment parts for the zakat. 1119 debug (bool): A flag indicating whether to print debug information. 1120 1121 Returns: 1122 bool: True if the zakat calculation is successful, False otherwise. 1123 """ 1124 (valid, _, plan) = report 1125 if not valid: 1126 return valid 1127 parts_exist = parts is not None 1128 if parts_exist: 1129 for part in parts: 1130 if self.check_payment_parts(part) != 0: 1131 return False 1132 if debug: 1133 print('######### zakat #######') 1134 print('parts_exist', parts_exist) 1135 _nolock = self.nolock(); self.lock() 1136 report_time = self.time() 1137 self._vault['report'][report_time] = report 1138 self._step(Action.REPORT, ref=report_time) 1139 created = self.time() 1140 for x in plan: 1141 if debug: 1142 print(plan[x]) 1143 print('-------------') 1144 print(self._vault['account'][x]['box']) 1145 ids = sorted(self._vault['account'][x]['box'].keys()) 1146 if debug: 1147 print('plan[x]', plan[x]) 1148 for i in plan[x].keys(): 1149 j = ids[i] 1150 if debug: 1151 print('i', i, 'j', j) 1152 self._step(Action.ZAKAT, x, j, value=self._vault['account'][x]['box'][j]['last'], key='last', math_operation=MathOperation.EQUAL) 1153 self._vault['account'][x]['box'][j]['last'] = created 1154 self._vault['account'][x]['box'][j]['total'] += plan[x][i]['total'] 1155 self._step(Action.ZAKAT, x, j, value=plan[x][i]['total'], key='total', math_operation=MathOperation.ADDITION) 1156 self._vault['account'][x]['box'][j]['count'] += plan[x][i]['count'] 1157 self._step(Action.ZAKAT, x, j, value=plan[x][i]['count'], key='count', math_operation=MathOperation.ADDITION) 1158 if not parts_exist: 1159 self._vault['account'][x]['box'][j]['rest'] -= plan[x][i]['total'] 1160 self._step(Action.ZAKAT, x, j, value=plan[x][i]['total'], key='rest', math_operation=MathOperation.SUBTRACTION) 1161 if parts_exist: 1162 for transaction in parts: 1163 for account, part in transaction['account'].items(): 1164 if debug: 1165 print('zakat-part', account, part['part']) 1166 self.sub(part['part'], 'zakat-part', account, debug=debug) 1167 if _nolock: self.free(self.lock()) 1168 return True
Perform Zakat calculation based on the given report and optional parts.
Parameters: report (tuple): A tuple containing the validity of the report, the report data, and the zakat plan. parts (dict): A dictionary containing the payment parts for the zakat. debug (bool): A flag indicating whether to print debug information.
Returns: bool: True if the zakat calculation is successful, False otherwise.
1170 def export_json(self, path: str = "data.json") -> bool: 1171 """ 1172 Exports the current state of the ZakatTracker object to a JSON file. 1173 1174 Parameters: 1175 path (str): The path where the JSON file will be saved. Default is "data.json". 1176 1177 Returns: 1178 bool: True if the export is successful, False otherwise. 1179 1180 Raises: 1181 No specific exceptions are raised by this method. 1182 """ 1183 with open(path, "w") as file: 1184 json.dump(self._vault, file, indent=4, cls=JSONEncoder) 1185 return True 1186 return False
Exports the current state of the ZakatTracker object to a JSON file.
Parameters: path (str): The path where the JSON file will be saved. Default is "data.json".
Returns: bool: True if the export is successful, False otherwise.
Raises: No specific exceptions are raised by this method.
1188 def save(self, path: str = None) -> bool: 1189 """ 1190 Save the current state of the ZakatTracker object to a pickle file. 1191 1192 Parameters: 1193 path (str): The path where the pickle file will be saved. If not provided, it will use the default path. 1194 1195 Returns: 1196 bool: True if the save operation is successful, False otherwise. 1197 """ 1198 if path is None: 1199 path = self.path() 1200 with open(path, "wb") as f: 1201 pickle.dump(self._vault, f) 1202 return True 1203 return False
Save the current state of the ZakatTracker object to a pickle file.
Parameters: path (str): The path where the pickle file will be saved. If not provided, it will use the default path.
Returns: bool: True if the save operation is successful, False otherwise.
1205 def load(self, path: str = None) -> bool: 1206 """ 1207 Load the current state of the ZakatTracker object from a pickle file. 1208 1209 Parameters: 1210 path (str): The path where the pickle file is located. If not provided, it will use the default path. 1211 1212 Returns: 1213 bool: True if the load operation is successful, False otherwise. 1214 """ 1215 if path is None: 1216 path = self.path() 1217 if os.path.exists(path): 1218 with open(path, "rb") as f: 1219 self._vault = pickle.load(f) 1220 return True 1221 return False
Load the current state of the ZakatTracker object from a pickle file.
Parameters: path (str): The path where the pickle file is located. If not provided, it will use the default path.
Returns: bool: True if the load operation is successful, False otherwise.
1223 def import_csv(self, path: str = 'file.csv') -> tuple: 1224 """ 1225 Import transactions from a CSV file. 1226 1227 Parameters: 1228 path (str): The path to the CSV file. Default is 'file.csv'. 1229 1230 Returns: 1231 tuple: A tuple containing the number of transactions created, the number of transactions found in the cache, and a dictionary of bad transactions. 1232 1233 The CSV file should have the following format: 1234 account, desc, value, date 1235 For example: 1236 safe-45, "Some text", 34872, 1988-06-30 00:00:00 1237 1238 The function reads the CSV file, checks for duplicate transactions, and creates the transactions in the system. 1239 """ 1240 cache = [] 1241 tmp = "tmp" 1242 try: 1243 with open(tmp, "rb") as f: 1244 cache = pickle.load(f) 1245 except: 1246 pass 1247 date_formats = [ 1248 "%Y-%m-%d %H:%M:%S", 1249 "%Y-%m-%dT%H:%M:%S", 1250 "%Y-%m-%dT%H%M%S", 1251 "%Y-%m-%d", 1252 ] 1253 created, found, bad = 0, 0, {} 1254 with open(path, newline='', encoding="utf-8") as f: 1255 i = 0 1256 for row in csv.reader(f, delimiter=','): 1257 i += 1 1258 hashed = hash(tuple(row)) 1259 if hashed in cache: 1260 found += 1 1261 continue 1262 account = row[0] 1263 desc = row[1] 1264 value = float(row[2]) 1265 date = 0 1266 for time_format in date_formats: 1267 try: 1268 date = self.time(datetime.datetime.strptime(row[3], time_format)) 1269 break 1270 except: 1271 pass 1272 # TODO: not allowed for negative dates 1273 if date == 0 or value == 0: 1274 bad[i] = row 1275 continue 1276 if value > 0: 1277 self.track(value, desc, account, True, date) 1278 elif value < 0: 1279 self.sub(-value, desc, account, date) 1280 created += 1 1281 cache.append(hashed) 1282 with open(tmp, "wb") as f: 1283 pickle.dump(cache, f) 1284 return (created, found, bad)
Import transactions from a CSV file.
Parameters: path (str): The path to the CSV file. Default is 'file.csv'.
Returns: tuple: A tuple containing the number of transactions created, the number of transactions found in the cache, and a dictionary of bad transactions.
The CSV file should have the following format: account, desc, value, date For example: safe-45, "Some text", 34872, 1988-06-30 00:00:00
The function reads the CSV file, checks for duplicate transactions, and creates the transactions in the system.
1290 @staticmethod 1291 def DurationFromNanoSeconds(ns: int) -> tuple: 1292 """REF https://github.com/JayRizzo/Random_Scripts/blob/master/time_measure.py#L106 1293 Convert NanoSeconds to Human Readable Time Format. 1294 A NanoSeconds is a unit of time in the International System of Units (SI) equal to one millionth (0.000001 or 10−6 or 1⁄1,000,000) of a second. 1295 Its symbol is μs, sometimes simplified to us when Unicode is not available. 1296 A microsecond is equal to 1000 nanoseconds or 1⁄1,000 of a millisecond. 1297 1298 INPUT : ms (AKA: MilliSeconds) 1299 OUTPUT: tuple(string TIMELAPSED, string SPOKENTIME) like format. 1300 OUTPUT Variables: TIMELAPSED, SPOKENTIME 1301 1302 Example Input: DurationFromNanoSeconds(ns) 1303 **"Millennium:Century:Years:Days:Hours:Minutes:Seconds:MilliSeconds:MicroSeconds:NanoSeconds"** 1304 Example Output: ('039:0001:047:325:05:02:03:456:789:012', ' 39 Millennia, 1 Century, 47 Years, 325 Days, 5 Hours, 2 Minutes, 3 Seconds, 456 MilliSeconds, 789 MicroSeconds, 12 NanoSeconds') 1305 DurationFromNanoSeconds(1234567890123456789012) 1306 """ 1307 μs, ns = divmod(ns, 1000) 1308 ms, μs = divmod(μs, 1000) 1309 s, ms = divmod(ms, 1000) 1310 m, s = divmod(s, 60) 1311 h, m = divmod(m, 60) 1312 d, h = divmod(h, 24) 1313 y, d = divmod(d, 365) 1314 c, y = divmod(y, 100) 1315 n, c = divmod(c, 10) 1316 TIMELAPSED = f"{n:03.0f}:{c:04.0f}:{y:03.0f}:{d:03.0f}:{h:02.0f}:{m:02.0f}:{s:02.0f}::{ms:03.0f}::{μs:03.0f}::{ns:03.0f}" 1317 SPOKENTIME = f"{n: 3d} Millennia, {c: 4d} Century, {y: 3d} Years, {d: 4d} Days, {h: 2d} Hours, {m: 2d} Minutes, {s: 2d} Seconds, {ms: 3d} MilliSeconds, {μs: 3d} MicroSeconds, {ns: 3d} NanoSeconds" 1318 return TIMELAPSED, SPOKENTIME
REF https://github.com/JayRizzo/Random_Scripts/blob/master/time_measure.py#L106 Convert NanoSeconds to Human Readable Time Format. A NanoSeconds is a unit of time in the International System of Units (SI) equal to one millionth (0.000001 or 10−6 or 1⁄1,000,000) of a second. Its symbol is μs, sometimes simplified to us when Unicode is not available. A microsecond is equal to 1000 nanoseconds or 1⁄1,000 of a millisecond.
INPUT : ms (AKA: MilliSeconds) OUTPUT: tuple(string TIMELAPSED, string SPOKENTIME) like format. OUTPUT Variables: TIMELAPSED, SPOKENTIME
Example Input: DurationFromNanoSeconds(ns) "Millennium:Century:Years:Days:Hours:Minutes:Seconds:MilliSeconds:MicroSeconds:NanoSeconds" Example Output: ('039:0001:047:325:05:02:03:456:789:012', ' 39 Millennia, 1 Century, 47 Years, 325 Days, 5 Hours, 2 Minutes, 3 Seconds, 456 MilliSeconds, 789 MicroSeconds, 12 NanoSeconds') DurationFromNanoSeconds(1234567890123456789012)
1320 @staticmethod 1321 def day_to_time(day: int, month: int = 6, year: int = 2024) -> int: # افتراض أن الشهر هو يونيو والسنة 2024 1322 """ 1323 Convert a specific day, month, and year into a timestamp. 1324 1325 Parameters: 1326 day (int): The day of the month. 1327 month (int): The month of the year. Default is 6 (June). 1328 year (int): The year. Default is 2024. 1329 1330 Returns: 1331 int: The timestamp representing the given day, month, and year. 1332 1333 Note: 1334 This method assumes the default month and year if not provided. 1335 """ 1336 return ZakatTracker.time(datetime.datetime(year, month, day))
Convert a specific day, month, and year into a timestamp.
Parameters: day (int): The day of the month. month (int): The month of the year. Default is 6 (June). year (int): The year. Default is 2024.
Returns: int: The timestamp representing the given day, month, and year.
Note: This method assumes the default month and year if not provided.
1338 @staticmethod 1339 def generate_random_date(start_date: datetime.datetime, end_date: datetime.datetime) -> datetime.datetime: 1340 """ 1341 Generate a random date between two given dates. 1342 1343 Parameters: 1344 start_date (datetime.datetime): The start date from which to generate a random date. 1345 end_date (datetime.datetime): The end date until which to generate a random date. 1346 1347 Returns: 1348 datetime.datetime: A random date between the start_date and end_date. 1349 """ 1350 time_between_dates = end_date - start_date 1351 days_between_dates = time_between_dates.days 1352 random_number_of_days = random.randrange(days_between_dates) 1353 return start_date + datetime.timedelta(days=random_number_of_days)
Generate a random date between two given dates.
Parameters: start_date (datetime.datetime): The start date from which to generate a random date. end_date (datetime.datetime): The end date until which to generate a random date.
Returns: datetime.datetime: A random date between the start_date and end_date.
1355 @staticmethod 1356 def generate_random_csv_file(path: str = "data.csv", count: int = 1000) -> None: 1357 """ 1358 Generate a random CSV file with specified parameters. 1359 1360 Parameters: 1361 path (str): The path where the CSV file will be saved. Default is "data.csv". 1362 count (int): The number of rows to generate in the CSV file. Default is 1000. 1363 1364 Returns: 1365 None. The function generates a CSV file at the specified path with the given count of rows. 1366 Each row contains a randomly generated account, description, value, and date. 1367 The value is randomly generated between 1000 and 100000, and the date is randomly generated between 1950-01-01 and 2023-12-31. 1368 If the row number is not divisible by 13, the value is multiplied by -1. 1369 """ 1370 with open(path, "w", newline="") as csvfile: 1371 writer = csv.writer(csvfile) 1372 for i in range(count): 1373 account = f"acc-{random.randint(1, 1000)}" 1374 desc = f"Some text {random.randint(1, 1000)}" 1375 value = random.randint(1000, 100000) 1376 date = ZakatTracker.generate_random_date(datetime.datetime(1950, 1, 1), datetime.datetime(2023, 12, 31)).strftime("%Y-%m-%d %H:%M:%S") 1377 if not i % 13 == 0: 1378 value *= -1 1379 writer.writerow([account, desc, value, date])
Generate a random CSV file with specified parameters.
Parameters: path (str): The path where the CSV file will be saved. Default is "data.csv". count (int): The number of rows to generate in the CSV file. Default is 1000.
Returns: None. The function generates a CSV file at the specified path with the given count of rows. Each row contains a randomly generated account, description, value, and date. The value is randomly generated between 1000 and 100000, and the date is randomly generated between 1950-01-01 and 2023-12-31. If the row number is not divisible by 13, the value is multiplied by -1.
1381 @staticmethod 1382 def create_random_list(max_sum, min_value=0, max_value=10): 1383 """ 1384 Creates a list of random integers whose sum does not exceed the specified maximum. 1385 1386 Args: 1387 max_sum: The maximum allowed sum of the list elements. 1388 min_value: The minimum possible value for an element (inclusive). 1389 max_value: The maximum possible value for an element (inclusive). 1390 1391 Returns: 1392 A list of random integers. 1393 """ 1394 result = [] 1395 current_sum = 0 1396 1397 while current_sum < max_sum: 1398 # Calculate the remaining space for the next element 1399 remaining_sum = max_sum - current_sum 1400 # Determine the maximum possible value for the next element 1401 next_max_value = min(remaining_sum, max_value) 1402 # Generate a random element within the allowed range 1403 next_element = random.randint(min_value, next_max_value) 1404 result.append(next_element) 1405 current_sum += next_element 1406 1407 return result
Creates a list of random integers whose sum does not exceed the specified maximum.
Args: max_sum: The maximum allowed sum of the list elements. min_value: The minimum possible value for an element (inclusive). max_value: The maximum possible value for an element (inclusive).
Returns: A list of random integers.
1547 def test(self, debug: bool = False): 1548 1549 try: 1550 1551 assert self._history() 1552 1553 # Not allowed for duplicate transactions in the same account and time 1554 1555 created = ZakatTracker.time() 1556 self.track(100, 'test-1', 'same', True, created) 1557 failed = False 1558 try: 1559 self.track(50, 'test-1', 'same', True, created) 1560 except: 1561 failed = True 1562 assert failed is True 1563 1564 self.reset() 1565 1566 # Always preserve box age during transfer 1567 1568 created = ZakatTracker.time() 1569 series = [ 1570 (30, 4), 1571 (60, 3), 1572 (90, 2), 1573 ] 1574 case = { 1575 30: { 1576 'series': series, 1577 'rest' : 150, 1578 }, 1579 60: { 1580 'series': series, 1581 'rest' : 120, 1582 }, 1583 90: { 1584 'series': series, 1585 'rest' : 90, 1586 }, 1587 180: { 1588 'series': series, 1589 'rest' : 0, 1590 }, 1591 270: { 1592 'series': series, 1593 'rest' : -90, 1594 }, 1595 360: { 1596 'series': series, 1597 'rest' : -180, 1598 }, 1599 } 1600 1601 selected_time = ZakatTracker.time() - ZakatTracker.TimeCycle() 1602 1603 for total in case: 1604 for x in case[total]['series']: 1605 self.track(x[0], f"test-{x} ages", 'ages', True, selected_time * x[1]) 1606 1607 refs = self.transfer(total, 'ages', 'future', 'Zakat Movement', debug=debug) 1608 1609 if debug: 1610 print('refs', refs) 1611 1612 ages_cache_balance = self.balance('ages') 1613 ages_fresh_balance = self.balance('ages', False) 1614 rest = case[total]['rest'] 1615 if debug: 1616 print('source', ages_cache_balance, ages_fresh_balance, rest) 1617 assert ages_cache_balance == rest 1618 assert ages_fresh_balance == rest 1619 1620 future_cache_balance = self.balance('future') 1621 future_fresh_balance = self.balance('future', False) 1622 if debug: 1623 print('target', future_cache_balance, future_fresh_balance, total) 1624 print('refs', refs) 1625 assert future_cache_balance == total 1626 assert future_fresh_balance == total 1627 1628 for ref in self._vault['account']['ages']['box']: 1629 ages_capital = self._vault['account']['ages']['box'][ref]['capital'] 1630 ages_rest = self._vault['account']['ages']['box'][ref]['rest'] 1631 future_capital = 0 1632 if ref in self._vault['account']['future']['box']: 1633 future_capital = self._vault['account']['future']['box'][ref]['capital'] 1634 future_rest = 0 1635 if ref in self._vault['account']['future']['box']: 1636 future_rest = self._vault['account']['future']['box'][ref]['rest'] 1637 if ages_capital != 0 and future_capital != 0 and future_rest != 0: 1638 if debug: 1639 print('================================================================') 1640 print('ages', ages_capital, ages_rest) 1641 print('future', future_capital, future_rest) 1642 if ages_rest == 0: 1643 assert ages_capital == future_capital 1644 elif ages_rest < 0: 1645 assert -ages_capital == future_capital 1646 elif ages_rest > 0: 1647 assert ages_capital == ages_rest + future_capital 1648 self.reset() 1649 assert len(self._vault['history']) == 0 1650 1651 assert self._history() 1652 assert self._history(False) is False 1653 assert self._history() is False 1654 assert self._history(True) 1655 assert self._history() 1656 1657 self._test_core(True, debug) 1658 self._test_core(False, debug) 1659 1660 transaction = [ 1661 ( 1662 20, 'wallet', 1, 800, 800, 800, 4, 5, 1663 -85, -85, -85, 6, 7, 1664 ), 1665 ( 1666 750, 'wallet', 'safe', 50, 50, 50, 4, 6, 1667 750, 750, 750, 1, 1, 1668 ), 1669 ( 1670 600, 'safe', 'bank', 150, 150, 150, 1, 2, 1671 600, 600, 600, 1, 1, 1672 ), 1673 ] 1674 for z in transaction: 1675 self.lock() 1676 x = z[1] 1677 y = z[2] 1678 self.transfer(z[0], x, y, 'test-transfer', debug=debug) 1679 assert self.balance(x) == z[3] 1680 xx = self.accounts()[x] 1681 assert xx == z[3] 1682 assert self.balance(x, False) == z[4] 1683 assert xx == z[4] 1684 1685 l = self._vault['account'][x]['log'] 1686 s = 0 1687 for i in l: 1688 s += l[i]['value'] 1689 if debug: 1690 print('s', s, 'z[5]', z[5]) 1691 assert s == z[5] 1692 1693 assert self.box_size(x) == z[6] 1694 assert self.log_size(x) == z[7] 1695 1696 yy = self.accounts()[y] 1697 assert self.balance(y) == z[8] 1698 assert yy == z[8] 1699 assert self.balance(y, False) == z[9] 1700 assert yy == z[9] 1701 1702 l = self._vault['account'][y]['log'] 1703 s = 0 1704 for i in l: 1705 s += l[i]['value'] 1706 assert s == z[10] 1707 1708 assert self.box_size(y) == z[11] 1709 assert self.log_size(y) == z[12] 1710 1711 if debug: 1712 pp().pprint(self.check(2.17)) 1713 1714 assert not self.nolock() 1715 history_count = len(self._vault['history']) 1716 if debug: 1717 print('history-count', history_count) 1718 assert history_count == 11 1719 assert not self.free(ZakatTracker.time()) 1720 assert self.free(self.lock()) 1721 assert self.nolock() 1722 assert len(self._vault['history']) == 11 1723 1724 # storage 1725 1726 _path = self.path('test.pickle') 1727 if os.path.exists(_path): 1728 os.remove(_path) 1729 self.save() 1730 assert os.path.getsize(_path) > 0 1731 self.reset() 1732 assert self.recall(False, debug) is False 1733 self.load() 1734 assert self._vault['account'] is not None 1735 1736 # recall 1737 1738 assert self.nolock() 1739 assert len(self._vault['history']) == 11 1740 assert self.recall(False, debug) is True 1741 assert len(self._vault['history']) == 10 1742 assert self.recall(False, debug) is True 1743 assert len(self._vault['history']) == 9 1744 1745 # csv 1746 1747 _path = "test.csv" 1748 count = 1000 1749 if os.path.exists(_path): 1750 os.remove(_path) 1751 self.generate_random_csv_file(_path, count) 1752 assert os.path.getsize(_path) > 0 1753 tmp = "tmp" 1754 if os.path.exists(tmp): 1755 os.remove(tmp) 1756 (created, found, bad) = self.import_csv(_path) 1757 bad_count = len(bad) 1758 if debug: 1759 print(f"csv-imported: ({created}, {found}, {bad_count})") 1760 tmp_size = os.path.getsize(tmp) 1761 assert tmp_size > 0 1762 assert created + found + bad_count == count 1763 assert created == count 1764 assert bad_count == 0 1765 (created_2, found_2, bad_2) = self.import_csv(_path) 1766 bad_2_count = len(bad_2) 1767 if debug: 1768 print(f"csv-imported: ({created_2}, {found_2}, {bad_2_count})") 1769 print(bad) 1770 assert tmp_size == os.path.getsize(tmp) 1771 assert created_2 + found_2 + bad_2_count == count 1772 assert created == found_2 1773 assert bad_count == bad_2_count 1774 assert found_2 == count 1775 assert bad_2_count == 0 1776 assert created_2 == 0 1777 1778 # payment parts 1779 1780 positive_parts = self.build_payment_parts(100, positive_only=True) 1781 assert self.check_payment_parts(positive_parts) != 0 1782 assert self.check_payment_parts(positive_parts) != 0 1783 all_parts = self.build_payment_parts(300, positive_only= False) 1784 assert self.check_payment_parts(all_parts) != 0 1785 assert self.check_payment_parts(all_parts) != 0 1786 if debug: 1787 pp().pprint(positive_parts) 1788 pp().pprint(all_parts) 1789 # dynamic discount 1790 suite = [] 1791 count = 3 1792 for exceed in [False, True]: 1793 case = [] 1794 for parts in [positive_parts, all_parts]: 1795 part = parts.copy() 1796 demand = part['demand'] 1797 if debug: 1798 print(demand, part['total']) 1799 i = 0 1800 z = demand / count 1801 cp = { 1802 'account': {}, 1803 'demand': demand, 1804 'exceed': exceed, 1805 'total': part['total'], 1806 } 1807 for x, y in part['account'].items(): 1808 if exceed and z <= demand: 1809 i += 1 1810 y['part'] = z 1811 if debug: 1812 print(exceed, y) 1813 cp['account'][x] = y 1814 case.append(y) 1815 elif not exceed and y['balance'] >= z: 1816 i += 1 1817 y['part'] = z 1818 if debug: 1819 print(exceed, y) 1820 cp['account'][x] = y 1821 case.append(y) 1822 if i >= count: 1823 break 1824 if len(cp['account'][x]) > 0: 1825 suite.append(cp) 1826 if debug: 1827 print('suite', len(suite)) 1828 for case in suite: 1829 if debug: 1830 print(case) 1831 result = self.check_payment_parts(case) 1832 if debug: 1833 print('check_payment_parts', result) 1834 assert result == 0 1835 1836 report = self.check(2.17, None, debug) 1837 (valid, brief, plan) = report 1838 if debug: 1839 print('valid', valid) 1840 assert self.zakat(report, parts=suite, debug=debug) 1841 1842 # exchange 1843 1844 self.exchange("cash", 25, 3.75, "2024-06-25") 1845 self.exchange("cash", 22, 3.73, "2024-06-22") 1846 self.exchange("cash", 15, 3.69, "2024-06-15") 1847 self.exchange("cash", 10, 3.66) 1848 1849 for i in range(1, 30): 1850 rate, description = self.exchange("cash", i).values() 1851 if debug: 1852 print(i, rate, description) 1853 if i < 10: 1854 assert rate == 1 1855 assert description is None 1856 elif i == 10: 1857 assert rate == 3.66 1858 assert description is None 1859 elif i < 15: 1860 assert rate == 3.66 1861 assert description is None 1862 elif i == 15: 1863 assert rate == 3.69 1864 assert description is not None 1865 elif i < 22: 1866 assert rate == 3.69 1867 assert description is not None 1868 elif i == 22: 1869 assert rate == 3.73 1870 assert description is not None 1871 elif i >= 25: 1872 assert rate == 3.75 1873 assert description is not None 1874 rate, description = self.exchange("bank", i).values() 1875 if debug: 1876 print(i, rate, description) 1877 assert rate == 1 1878 assert description is None 1879 1880 assert len(self._vault['exchange']) > 0 1881 assert len(self.exchanges()) > 0 1882 self._vault['exchange'].clear() 1883 assert len(self._vault['exchange']) == 0 1884 assert len(self.exchanges()) == 0 1885 1886 # حفظ أسعار الصرف باستخدام التواريخ بالنانو ثانية 1887 self.exchange("cash", ZakatTracker.day_to_time(25), 3.75, "2024-06-25") 1888 self.exchange("cash", ZakatTracker.day_to_time(22), 3.73, "2024-06-22") 1889 self.exchange("cash", ZakatTracker.day_to_time(15), 3.69, "2024-06-15") 1890 self.exchange("cash", ZakatTracker.day_to_time(10), 3.66) 1891 1892 for i in [x * 0.12 for x in range(-15, 21)]: 1893 if i <= 1: 1894 assert self.exchange("test", ZakatTracker.time(), i, f"range({i})") is None 1895 else: 1896 assert self.exchange("test", ZakatTracker.time(), i, f"range({i})") is not None 1897 1898 # اختبار النتائج باستخدام التواريخ بالنانو ثانية 1899 for i in range(1, 31): 1900 timestamp_ns = ZakatTracker.day_to_time(i) 1901 rate, description = self.exchange("cash", timestamp_ns).values() 1902 print(i, rate, description) 1903 if i < 10: 1904 assert rate == 1 1905 assert description is None 1906 elif i == 10: 1907 assert rate == 3.66 1908 assert description is None 1909 elif i < 15: 1910 assert rate == 3.66 1911 assert description is None 1912 elif i == 15: 1913 assert rate == 3.69 1914 assert description is not None 1915 elif i < 22: 1916 assert rate == 3.69 1917 assert description is not None 1918 elif i == 22: 1919 assert rate == 3.73 1920 assert description is not None 1921 elif i >= 25: 1922 assert rate == 3.75 1923 assert description is not None 1924 rate, description = self.exchange("bank", i).values() 1925 print(i, rate, description) 1926 assert rate == 1 1927 assert description is None 1928 1929 assert self.export_json("1000-transactions-test.json") 1930 assert self.save("1000-transactions-test.pickle") 1931 1932 self.reset() 1933 1934 # test transfer between accounts with different exchange rate 1935 1936 debug = True 1937 a_SAR = "Bank (SAR)" 1938 b_USD = "Bank (USD)" 1939 c_SAR = "Safe (SAR)" 1940 # 0: track, 1: check-exchange, 2: do-exchange, 3: transfer 1941 for case in[ 1942 (0, a_SAR, "SAR Gift", 1000, 1000), 1943 (1, a_SAR, 1), 1944 (0, b_USD, "USD Gift", 500, 500), 1945 (1, b_USD, 1), 1946 (2, b_USD, 3.75), 1947 (1, b_USD, 3.75), 1948 (3, 100, b_USD, a_SAR, "100 USD -> SAR", 400, 1375), 1949 (0, c_SAR, "Salary", 750, 750), 1950 (3, 375, c_SAR, b_USD, "375 SAR -> USD", 375, 500), 1951 (3, 3.75, a_SAR, b_USD, "3.75 SAR -> USD", 1371.25, 501), 1952 ]: 1953 match(case[0]): 1954 case 0: # track 1955 _, account, desc, x, balance = case 1956 self.track(value=x, desc=desc, account=account, debug=debug) 1957 1958 cached_value = self.balance(account, cached=True) 1959 fresh_value = self.balance(account, cached=False) 1960 if debug: 1961 print('account', account, 'cached_value', cached_value, 'fresh_value', fresh_value) 1962 assert cached_value == balance 1963 assert fresh_value == balance 1964 case 1: # check-exchange 1965 _, account, expected_rate = case 1966 t_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug) 1967 if debug: 1968 print('t-exchange', t_exchange) 1969 assert t_exchange['rate'] == expected_rate 1970 case 2: # do-exchange 1971 _, account, rate = case 1972 self.exchange(account, rate=rate, debug=debug) 1973 b_exchange = self.exchange(account, created=ZakatTracker.time(), debug=debug) 1974 if debug: 1975 print('b-exchange', b_exchange) 1976 assert b_exchange['rate'] == rate 1977 case 3: # transfer 1978 _, x, a, b, desc, a_balance, b_balance = case 1979 self.transfer(x, a, b, desc, debug=debug) 1980 1981 cached_value = self.balance(a, cached=True) 1982 fresh_value = self.balance(a, cached=False) 1983 if debug: 1984 print('account', a, 'cached_value', cached_value, 'fresh_value', fresh_value) 1985 assert cached_value == a_balance 1986 assert fresh_value == a_balance 1987 1988 cached_value = self.balance(b, cached=True) 1989 fresh_value = self.balance(b, cached=False) 1990 if debug: 1991 print('account', b, 'cached_value', cached_value, 'fresh_value', fresh_value) 1992 assert cached_value == b_balance 1993 assert fresh_value == b_balance 1994 1995 # Transfer all in many chunks randomly from B to A 1996 a_SAR_balance = 1371.25 1997 b_USD_balance = 501 1998 b_USD_exchange = self.exchange(b_USD) 1999 amounts = ZakatTracker.create_random_list(b_USD_balance) 2000 if debug: 2001 print('amounts', amounts) 2002 i = 0 2003 for x in amounts: 2004 if debug: 2005 print(f'{i} - transfer-with-exchnage({x})') 2006 self.transfer(x, b_USD, a_SAR, f"{x} USD -> SAR", debug=debug) 2007 2008 b_USD_balance -= x 2009 cached_value = self.balance(b_USD, cached=True) 2010 fresh_value = self.balance(b_USD, cached=False) 2011 if debug: 2012 print('account', b_USD, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted', b_USD_balance) 2013 assert cached_value == b_USD_balance 2014 assert fresh_value == b_USD_balance 2015 2016 a_SAR_balance += x * b_USD_exchange['rate'] 2017 cached_value = self.balance(a_SAR, cached=True) 2018 fresh_value = self.balance(a_SAR, cached=False) 2019 if debug: 2020 print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected', a_SAR_balance, 'rate', b_USD_exchange['rate']) 2021 assert cached_value == a_SAR_balance 2022 assert fresh_value == a_SAR_balance 2023 i += 1 2024 2025 # Transfer all in many chunks randomly from C to A 2026 c_SAR_balance = 375 2027 amounts = ZakatTracker.create_random_list(c_SAR_balance) 2028 if debug: 2029 print('amounts', amounts) 2030 i = 0 2031 for x in amounts: 2032 if debug: 2033 print(f'{i} - transfer-with-exchnage({x})') 2034 self.transfer(x, c_SAR, a_SAR, f"{x} SAR -> a_SAR", debug=debug) 2035 2036 c_SAR_balance -= x 2037 cached_value = self.balance(c_SAR, cached=True) 2038 fresh_value = self.balance(c_SAR, cached=False) 2039 if debug: 2040 print('account', c_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'excepted', c_SAR_balance) 2041 assert cached_value == c_SAR_balance 2042 assert fresh_value == c_SAR_balance 2043 2044 a_SAR_balance += x 2045 cached_value = self.balance(a_SAR, cached=True) 2046 fresh_value = self.balance(a_SAR, cached=False) 2047 if debug: 2048 print('account', a_SAR, 'cached_value', cached_value, 'fresh_value', fresh_value, 'expected', a_SAR_balance) 2049 assert cached_value == a_SAR_balance 2050 assert fresh_value == a_SAR_balance 2051 i += 1 2052 2053 assert self.export_json("accounts-transfer-with-exchange-rates.json") 2054 assert self.save("accounts-transfer-with-exchange-rates.pickle") 2055 2056 # check & zakat 2057 2058 cases = [ 2059 (1000, 'safe', ZakatTracker.time()-ZakatTracker.TimeCycle(), [ 2060 {'safe': {0: {'below_nisab': 25}}}, 2061 ], False), 2062 (2000, 'safe', ZakatTracker.time()-ZakatTracker.TimeCycle(), [ 2063 {'safe': {0: {'count': 1, 'total': 50}}}, 2064 ], True), 2065 (10000, 'cave', ZakatTracker.time()-(ZakatTracker.TimeCycle()*3), [ 2066 {'cave': {0: {'count': 3, 'total': 731.40625}}}, 2067 ], True), 2068 ] 2069 for case in cases: 2070 if debug: 2071 print("############# check #############") 2072 self.reset() 2073 self.track(case[0], 'test-check', case[1], True, case[2]) 2074 2075 assert self.nolock() 2076 assert len(self._vault['history']) == 1 2077 assert self.lock() 2078 assert not self.nolock() 2079 report = self.check(2.17, None, debug) 2080 (valid, brief, plan) = report 2081 assert valid == case[4] 2082 if debug: 2083 print(brief) 2084 assert case[0] == brief[0] 2085 assert case[0] == brief[1] 2086 2087 if debug: 2088 pp().pprint(plan) 2089 2090 for x in plan: 2091 assert case[1] == x 2092 if 'total' in case[3][0][x][0].keys(): 2093 assert case[3][0][x][0]['total'] == brief[2] 2094 assert plan[x][0]['total'] == case[3][0][x][0]['total'] 2095 assert plan[x][0]['count'] == case[3][0][x][0]['count'] 2096 else: 2097 assert plan[x][0]['below_nisab'] == case[3][0][x][0]['below_nisab'] 2098 if debug: 2099 pp().pprint(report) 2100 result = self.zakat(report, debug=debug) 2101 if debug: 2102 print('zakat-result', result, case[4]) 2103 assert result == case[4] 2104 report = self.check(2.17, None, debug) 2105 (valid, brief, plan) = report 2106 assert valid is False 2107 2108 assert len(self._vault['history']) == 2 2109 assert not self.nolock() 2110 assert self.recall(False, debug) is False 2111 self.free(self.lock()) 2112 assert self.nolock() 2113 assert len(self._vault['history']) == 2 2114 assert self.recall(False, debug) is True 2115 assert len(self._vault['history']) == 1 2116 2117 assert self.nolock() 2118 assert len(self._vault['history']) == 1 2119 2120 assert self.recall(False, debug) is True 2121 assert len(self._vault['history']) == 0 2122 2123 assert len(self._vault['account']) == 0 2124 assert len(self._vault['history']) == 0 2125 assert len(self._vault['report']) == 0 2126 assert self.nolock() 2127 except: 2128 # pp().pprint(self._vault) 2129 assert self.export_json("test-snapshot.json") 2130 assert self.save("test-snapshot.pickle") 2131 raise
68class Action(Enum): 69 CREATE = auto() 70 TRACK = auto() 71 LOG = auto() 72 SUB = auto() 73 ADD_FILE = auto() 74 REMOVE_FILE = auto() 75 BOX_TRANSFER = auto() 76 EXCHANGE = auto() 77 REPORT = auto() 78 ZAKAT = auto()
80class JSONEncoder(json.JSONEncoder): 81 def default(self, obj): 82 if isinstance(obj, Action) or isinstance(obj, MathOperation): 83 return obj.name # Serialize as the enum member's name 84 return super().default(obj)
Extensible JSON https://json.org encoder for Python data structures.
Supports the following objects and types by default:
+-------------------+---------------+ | Python | JSON | +===================+===============+ | dict | object | +-------------------+---------------+ | list, tuple | array | +-------------------+---------------+ | str | string | +-------------------+---------------+ | int, float | number | +-------------------+---------------+ | True | true | +-------------------+---------------+ | False | false | +-------------------+---------------+ | None | null | +-------------------+---------------+
To extend this to recognize other objects, subclass and implement a
.default()
method with another method that returns a serializable
object for o
if possible, otherwise it should call the superclass
implementation (to raise TypeError
).
81 def default(self, obj): 82 if isinstance(obj, Action) or isinstance(obj, MathOperation): 83 return obj.name # Serialize as the enum member's name 84 return super().default(obj)
Implement this method in a subclass such that it returns
a serializable object for o
, or calls the base implementation
(to raise a TypeError
).
For example, to support arbitrary iterators, you could implement default like this::
def default(self, o):
try:
iterable = iter(o)
except TypeError:
pass
else:
return list(iterable)
# Let the base class default method raise the TypeError
return super().default(o)