diff --git a/analysis/database/zakat-pony.png b/analysis/database/zakat-pony.png
index 6981eae..1c23bfd 100644
Binary files a/analysis/database/zakat-pony.png and b/analysis/database/zakat-pony.png differ
diff --git a/analysis/database/zakat.pdf b/analysis/database/zakat.pdf
index 9ef6b12..592e0f2 100644
Binary files a/analysis/database/zakat.pdf and b/analysis/database/zakat.pdf differ
diff --git a/analysis/database/zakat.png b/analysis/database/zakat.png
index 704db48..ee0077e 100644
Binary files a/analysis/database/zakat.png and b/analysis/database/zakat.png differ
diff --git a/analysis/database/zakat.svg b/analysis/database/zakat.svg
index 06df9d7..b7a90b0 100644
--- a/analysis/database/zakat.svg
+++ b/analysis/database/zakat.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/zakat/zakat_tracker.py b/zakat/zakat_tracker.py
index cd99d9c..bff0223 100644
--- a/zakat/zakat_tracker.py
+++ b/zakat/zakat_tracker.py
@@ -59,14 +59,13 @@
import random
import datetime
import hashlib
-from time import sleep
+from time import sleep, time_ns
from pprint import PrettyPrinter as pp
from math import floor, ceil
from enum import Enum, auto
from decimal import Decimal
from typing import Dict, Any
from pathlib import Path
-from time import time_ns, sleep
from camelx import Camel, CamelRegistry
import shutil
from abc import ABC, abstractmethod
@@ -132,14 +131,14 @@ def path(self, path: str = None) -> str:
The function also creates the necessary directories if the provided path is a file.
Parameters:
- path (str): The new path to the database file. If not provided, the current path is returned.
+ path (str, Optional): The new path to the database file. If not provided, the current path is returned.
Returns:
str: The current or new path to the database file.
"""
@abstractmethod
- def sub(self, unscaled_value: float | int | Decimal, desc: str = '', account: int = 1, created: int = None,
+ def sub(self, unscaled_value: float | int | Decimal, desc: str = '', account: int = 1, created: datetime = None,
debug: bool = False) \
-> tuple[
int,
@@ -152,10 +151,10 @@ def sub(self, unscaled_value: float | int | Decimal, desc: str = '', account: in
Parameters:
unscaled_value (float | int | Decimal): The amount to be subtracted.
- desc (str): A description for the transaction. Defaults to an empty string.
- account (int): The account number 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.
+ desc (str, Optional): A description for the transaction. Defaults to an empty string.
+ account (int, Optional): The account number from which the value will be subtracted. Defaults to '1'.
+ created (datetime, Optional): The datetime of the transaction. If not provided, the current datetime will be used.
+ debug (bool, Optional): 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.
@@ -170,18 +169,18 @@ def sub(self, unscaled_value: float | int | Decimal, desc: str = '', account: in
@abstractmethod
def track(self, unscaled_value: float | int | Decimal = 0, desc: str = '', account: int = 1, logging: bool = True,
- created: int = None,
+ created: datetime = None,
debug: bool = False) -> int:
"""
This function tracks a transaction for a specific account.
Parameters:
unscaled_value (float | int | Decimal): The value of the transaction. Default is 0.
- desc (str): The description of the transaction. Default is an empty string.
- account (int): 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.
+ desc (str, Optional): The description of the transaction. Default is an empty string.
+ account (int, Optional): The account for which the transaction is being tracked. Default is '1'.
+ logging (bool, Optional): Whether to log the transaction. Default is True.
+ created (datetime, Optional): The datetime of the transaction. If not provided, it will be generated. Default is None.
+ debug (bool, Optional): Whether to print debug information. Default is False.
Returns:
int: The timestamp of the transaction.
@@ -302,31 +301,31 @@ def accounts(self) -> dict:
"""
@abstractmethod
- def set_exchange(self, account: int, created: int = None, rate: float = None, description: str = None,
+ def set_exchange(self, account: int, created: datetime = None, rate: float = None, description: str = None,
debug: bool = False) -> bool:
"""
This method is used to record exchange rates for a specific account.
Parameters:
- account (int): 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.
- - debug (bool): Whether to print debug information. Default is False.
+ - created (datetime, Optional): The datetime of the exchange rate. If not provided, the current datetime will be used.
+ - rate (float, Optional): The exchange rate to be recorded. If not provided, the method will retrieve the latest exchange rate.
+ - description (str, Optional): A description of the exchange rate.
+ - debug (bool, Optional): Whether to print debug information. Default is False.
Returns:
bool: True if exchange is created, False otherwise.
"""
@abstractmethod
- def exchange(self, account: int, created: int = None, debug: bool = False) -> dict:
+ def exchange(self, account: int, created: datetime = None, debug: bool = False) -> dict:
"""
This method is used to retrieve exchange rates for a specific account.
Parameters:
- account (int): 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.
- - debug (bool): Whether to print debug information. Default is False.
+ - created (datetime, Optional): The datetime of the exchange rate. If not provided, the current datetime will be used.
+ - debug (bool, Optional): Whether to print debug information. Default is False.
Returns:
- dict: A dictionary containing the latest exchange rate and its description. If no exchange rate is found,
@@ -388,7 +387,7 @@ def account(self, name: str = None, ref: int = None) -> tuple[int, str] | None:
@abstractmethod
def transfer(self, unscaled_amount: float | int | Decimal, from_account: int, to_account: int, desc: str = '',
- created: int = None,
+ created: datetime = None,
debug: bool = False) -> list[int]:
"""
Transfers a specified value from one account to another.
@@ -398,8 +397,8 @@ def transfer(self, unscaled_amount: float | int | Decimal, from_account: int, to
from_account (int): The account number from which the value will be transferred.
to_account (int): The account number 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.
+ created (datetime, optional): The datetime of the transaction. If not provided, the current datetime will be used.
+ debug (bool, Optional): 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.
@@ -459,7 +458,7 @@ def stats(self, ignore_ram: bool = True) -> dict[str, tuple[int, str]]:
(e.g., KB, MB).
Parameters:
- ignore_ram (bool): Whether to ignore the RAM size. Default is True
+ ignore_ram (bool, Optional): Whether to ignore the RAM size. Default is True
Returns:
dict[str, tuple]: A dictionary containing the following statistics:
@@ -511,8 +510,8 @@ def balance(self, account_id: int = 1, cached: bool = True) -> int:
Calculate and return the balance of a specific account.
Parameters:
- account_id (int): 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.
+ account_id (int, Optional): The account number. Default is '1'.
+ cached (bool, Optional): If True, use the cached balance. If False, calculate the balance from the box. Default is True.
Returns:
int: The balance of the account.
@@ -566,7 +565,7 @@ def load(self, path: str = None) -> bool:
Load the current state of the ZakatTracker object from a camel file.
Parameters:
- path (str): The path where the camel file is located. If not provided, it will use the default path.
+ path (str, Optional): The path where the camel file is located. If not provided, it will use the default path.
Returns:
bool: True if the load operation is successful, False otherwise.
@@ -589,18 +588,18 @@ def check(self,
silver_gram_price: float,
unscaled_nisab: float | int | Decimal = None,
debug: bool = False,
- now: int = None,
+ now: datetime = None,
cycle: float = None) -> tuple:
"""
Check the eligibility for Zakat based on the given parameters.
Parameters:
silver_gram_price (float): The price of a gram of silver.
- unscaled_nisab (float | int | Decimal): The minimum amount of wealth required for Zakat. If not provided,
+ unscaled_nisab (float | int | Decimal, Optional): 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().
+ debug (bool, Optional): Flag to enable debug mode.
+ now (datetime, Optional): The current datetime. If not provided, it will be calculated using ZakatTracker.time().
+ cycle (float, Optional): 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,
@@ -614,8 +613,8 @@ def zakat(self, report: tuple, parts: Dict[str, Dict | bool | Any] = None, debug
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.
+ parts (dict, Optional): A dictionary containing the payment parts for the zakat.
+ debug (bool, Optional): A flag indicating whether to print debug information.
Returns:
bool: True if the zakat calculation is successful, False otherwise.
@@ -658,8 +657,8 @@ def daily_logs(self, weekday: WeekDay = WeekDay.Friday, debug: bool = False):
and the values are dictionaries containing the total value and the logs for that group.
Parameters:
- weekday (WeekDay): Select the weekday is collected for the week data. Default is WeekDay.Friday.
- debug (bool): Whether to print debug information. Default is False.
+ weekday (WeekDay, Optional): Select the weekday is collected for the week data. Default is WeekDay.Friday.
+ debug (bool, Optional): Whether to print debug information. Default is False.
Returns:
dict: A dictionary containing the daily logs.
@@ -734,7 +733,7 @@ def export_json(self, path: str = "data.json") -> bool:
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".
+ path (str, Optional): The path where the JSON file will be saved. Default is "data.json".
Returns:
bool: True if the export is successful, False otherwise.
@@ -748,6 +747,9 @@ def vault(self, section: Vault = Vault.ALL) -> dict:
"""
Returns a copy of the internal vault in dictionary format.
+ Parameters:
+ section (Vault, Optional): The specific section of the vault to retrieve data from. Defaults to Vault.ALL
+
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.
@@ -788,18 +790,18 @@ def ext(self) -> str | None:
"""
@abstractmethod
- def log(self, value: float, desc: str = '', account_id: int = 1, created: int = None, ref: int = None,
+ def log(self, value: float, desc: str = '', account_id: int = 1, created: datetime = None, ref: int = None,
debug: bool = False) -> int:
"""
Log a transaction into the account's log.
Parameters:
value (float): The value of the transaction.
- desc (str): The description of the transaction.
- account_id (int): The account to log the transaction into. Default is 1.
- created (int): The timestamp of the transaction. If not provided, it will be generated.
- ref (int): The reference of the object.
- debug (bool): Whether to print debug information. Default is False.
+ desc (str, Optional): The description of the transaction.
+ account_id (int, Optional): The account to log the transaction into. Default is 1.
+ created (datetime, Optional): The datetime of the transaction. If not provided, it will be generated.
+ ref (int, Optional): The reference of the object.
+ debug (bool, Optional): Whether to print debug information. Default is False.
Returns:
int: The timestamp of the logged transaction.
@@ -857,8 +859,8 @@ def snapshots(self, hide_missing: bool = True, verified_hash_only: bool = False)
Retrieve a dictionary of snapshots, with their respective hashes, paths, and existence status.
Parameters:
- - hide_missing (bool): If True, only include snapshots that exist in the dictionary. Default is True.
- - verified_hash_only (bool): If True, only include snapshots with a valid hash. Default is False.
+ - hide_missing (bool, Optional): If True, only include snapshots that exist in the dictionary. Default is True.
+ - verified_hash_only (bool, Optional): If True, only include snapshots with a valid hash. Default is False.
Returns:
- dict[int, tuple[str, str, bool]]: A dictionary where the keys are the timestamps of the snapshots,
@@ -941,26 +943,12 @@ def minimum_time_diff_ns() -> tuple[int, int]:
while x == y:
y = Helper._time()
i += 1
- return y - x, i
+ _y = datetime.datetime.fromisoformat(y).timestamp() * (10 ** 9)
+ _x = datetime.datetime.fromisoformat(x).timestamp() * (10 ** 9)
+ return _y - _x, i
@staticmethod
- def time(now: datetime = None) -> int:
- """
- Gets a high-resolution timestamp in nanoseconds.
-
- This method attempts to obtain a timestamp with a minimum granularity
- determined by `minimum_time_diff_ms()`. If consecutive calls to
- `datetime.datetime.now()` return the same value, it waits for a minimum
- time difference before returning a new timestamp.
-
- Parameters:
- now (datetime, optional): A specific datetime object to use as
- the timestamp. Defaults to None, which uses `datetime.datetime.now()`.
-
- Returns:
- int: The timestamp in nanoseconds since the Unix epoch (January 1, 1970),
- before 1970 will return in negative until 1000AD.
- """
+ def time(now: datetime = None) -> str:
new_time = Helper._time(now)
if Helper.last_time is None:
Helper.last_time = new_time
@@ -975,26 +963,97 @@ def time(now: datetime = None) -> int:
return new_time
@staticmethod
- def _time(now: datetime = None) -> int:
+ def _time(now: datetime = None) -> str:
+ if now is None:
+ now = datetime.datetime.now()
+ return now.isoformat()
+
+ @staticmethod
+ def time_to_datetime(time_s: str) -> datetime:
+ return datetime.datetime.fromisoformat(time_s)
+
+ @staticmethod
+ def datetime_to_milliseconds(dt: datetime) -> int:
+ """
+ Converts a datetime object to milliseconds since the Unix epoch.
+
+ Parameters:
+ dt: The datetime object to convert.
+
+ Returns:
+ The number of milliseconds since the Unix epoch.
"""
- Converts a datetime object to a high-resolution timestamp in nanoseconds.
+ return int(dt.timestamp() * 1000)
- This method calculates a timestamp by combining the ordinal day (number
- of days since a reference date) and nanoseconds within the day.
+ @staticmethod
+ def time_to_milliseconds(time_s: str) -> datetime:
+ return Helper.datetime_to_milliseconds(Helper.time_to_datetime(time_s))
+
+ @staticmethod
+ def iso8601_to_int(iso_format: str, strict: bool = True, debug: bool = False) -> int:
+ """
+ Converts an ISO 8601 formatted string or a valid integer to a compact integer representation.
Parameters:
- now (datetime, optional): A specific datetime object to use as
- the timestamp. Defaults to None, which uses `datetime.datetime.now()`.
+ iso_format: The datetime string in ISO 8601 format (e.g., "2023-11-10T15:23:56.123456") or
+ a valid integer representation of a datetime.
+ strict: (bool, Optional) A boolean flag controlling parsing behavior. Defaults to True.
+ - If True, only accepts valid ISO 8601 formatted strings.
+ - If False, attempts to convert integers directly and also allows basic string parsing for digits-only strings.
+ debug (bool, Optional): Flag to enable debug mode.
Returns:
- int: The timestamp in nanoseconds since the Unix epoch (January 1, 1970),
- before 1970 will return in negative until 1000AD.
+ An integer representing the datetime information from the ISO 8601 string or the original integer if strict is False and input is already an integer.
+
+ Raises:
+ ValueError: If the input string is not in a valid ISO 8601 format (in strict mode) or if the input
+ is an invalid integer string (in non-strict mode).
+
+ This function converts a datetime string in ISO 8601 format to a compact integer representation.
+ The integer is constructed by packing the year, month, day, hour, minute, second, and microsecond
+ components of the datetime object into a single integer using a positional encoding scheme.
+ Each component is scaled by a factor of 10 raised to a power that reflects its position in the
+ date and time representation.
+
+ **Note:** This representation does not include timezone information.
+
+ **Strict vs. Non-Strict Mode:**
+
+ - In strict mode (default), the function only accepts valid ISO 8601 formatted strings.
+ - In non-strict mode, the function also attempts to convert integers directly and allows basic string
+ parsing for strings that consist only of digits. This can be useful for handling potential inconsistencies
+ in input data formats. However, use caution with non-strict mode as it might lead to unexpected
+ behavior if the input data is not well-controlled.
+
+ **Example Usage:**
+
+ ```python
+ iso_str = "2023-11-10T15:23:56.123456"
+ int_representation = Myclass.iso8601_to_int(iso_str)
+ print(int_representation)
+
+ # In non-strict mode, this would also work:
+ int_value = 20231110152356
+ int_representation = Myclass.iso8601_to_int(int_value, strict=False)
+ print(int_representation)
+ ```
"""
- if now is None:
- now = datetime.datetime.now()
- ordinal_day = now.toordinal()
- ns_in_day = (now - now.replace(hour=0, minute=0, second=0, microsecond=0)).total_seconds() * 10 ** 9
- return int((ordinal_day - 719_163) * 86_400_000_000_000 + ns_in_day)
+ if debug:
+ print(f'iso8601_to_int(iso_format={iso_format}: {iso_format.__class__.__name__}, strict={strict})')
+ if not strict:
+ if type(iso_format) is int:
+ return int(iso_format)
+ if type(iso_format) is str:
+ if iso_format.isdigit():
+ return int(iso_format)
+ dt = datetime.datetime.fromisoformat(iso_format) if type(iso_format) is str else iso_format
+ return ((dt.year * 10 ** 16) +
+ (dt.month * 10 ** 14) +
+ (dt.day * 10 ** 12) +
+ (dt.hour * 10 ** 10) +
+ (dt.minute * 10 ** 8) +
+ (dt.second * 10 ** 6) +
+ dt.microsecond)
@staticmethod
def is_windows() -> bool:
@@ -1046,19 +1105,19 @@ def exchange_calc(x: float, x_rate: float, y_rate: float) -> float:
@staticmethod
def TimeCycle(days: int = 355) -> int:
"""
- Calculates the approximate duration of a lunar year in nanoseconds.
+ Calculates the approximate duration of a lunar year in milliseconds.
This function calculates the approximate duration of a lunar year based on the given number of days.
- It converts the given number of days into nanoseconds for use in high-precision timing applications.
+ It converts the given number of days into milliseconds for use in high-precision timing applications.
Parameters:
- days: The number of days in a lunar year. Defaults to 355,
+ days (int, Optional): The number of days in a lunar year. Defaults to 355,
which is an approximation of the average length of a lunar year.
Returns:
- The approximate duration of a lunar year in nanoseconds.
+ The approximate duration of a lunar year in milliseconds.
"""
- return int(60 * 60 * 24 * days * 1e9) # Lunar Year in nanoseconds
+ return int(60 * 60 * 24 * days * 1e3) # Lunar Year in milliseconds
@staticmethod
def Nisab(gram_price: float, gram_quantity: float = 595) -> float:
@@ -1072,7 +1131,7 @@ def Nisab(gram_price: float, gram_quantity: float = 595) -> float:
Parameters:
- gram_price (float): The price per gram of Nisab.
- - gram_quantity (float): The quantity of grams in a Nisab. Default is 595 grams of silver.
+ - gram_quantity (float, Optional): The quantity of grams in a Nisab. Default is 595 grams of silver.
Returns:
- float: The total value of Nisab based on the given price per gram.
@@ -1086,7 +1145,7 @@ def check_payment_parts(parts: dict, debug: bool = False) -> int:
Parameters:
parts (dict): A dictionary containing payment parts information.
- debug (bool): Flag to enable debug mode.
+ debug (bool, Optional): Flag to enable debug mode.
Returns:
int: Returns 0 if the payment parts are valid, otherwise returns the error code.
@@ -1190,9 +1249,9 @@ def scale(x: float | int | Decimal, decimal_places: int = 2) -> int:
facilitate precise scaling operations, particularly useful in financial or scientific calculations.
Parameters:
- x: The numeric value to scale. Can be a floating-point number, integer, or decimal.
- decimal_places: The exponent for the scaling factor (10**y). Defaults to 2, meaning the input is scaled
- by a factor of 100 (e.g., converts 1.23 to 123).
+ x (float | int | Decimal): The numeric value to scale. Can be a floating-point number, integer, or decimal.
+ decimal_places (int, Optional): The exponent for the scaling factor (10**y).
+ Defaults to 2, meaning the input is scaled by a factor of 100 (e.g., converts 1.23 to 123).
Returns:
The scaled value, rounded to the nearest integer.
@@ -1218,9 +1277,9 @@ def unscale(x: int, return_type: type = float, decimal_places: int = 2) -> float
Unscales an integer by a power of 10.
Parameters:
- x: The integer to unscale.
- return_type: The desired type for the returned value. Can be float, int, or Decimal. Defaults to float.
- decimal_places: The power of 10 to use. Defaults to 2.
+ x (int): The integer to unscale.
+ return_type (type, Optional): The desired type for the returned value. Can be float, int, or Decimal. Defaults to float.
+ decimal_places (int, Optional): The power of 10 to use. Defaults to 2.
Returns:
The unscaled number, converted to the specified return_type.
@@ -1232,23 +1291,6 @@ def unscale(x: int, return_type: type = float, decimal_places: int = 2) -> float
raise TypeError(f'Invalid return_type({return_type}). Supported types are float, int, and Decimal.')
return round(return_type(x / (10 ** decimal_places)), decimal_places)
- @staticmethod
- def time_to_datetime(ordinal_ns: int) -> datetime:
- """
- 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.
- """
- ordinal_day = ordinal_ns // 86_400_000_000_000 + 719_163
- ns_in_day = ordinal_ns % 86_400_000_000_000
- d = datetime.datetime.fromordinal(ordinal_day)
- t = datetime.timedelta(seconds=ns_in_day // 10 ** 9)
- return datetime.datetime.combine(d, datetime.time()) + t
-
@staticmethod
def human_readable_size(size: float, decimal_places: int = 2) -> str:
"""
@@ -1366,14 +1408,14 @@ def duration_from_nanoseconds(ns: int,
return time_lapsed, spoken_time_separator.join(spoken_time_part)
@staticmethod
- def day_to_time(day: int, month: int = 6, year: int = 2024) -> int: # افتراض أن الشهر هو يونيو والسنة 2024
+ def day_to_time(day: int, month: int = 6, year: int = 2024) -> str: # افتراض أن الشهر هو يونيو والسنة 2024
"""
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.
+ month (int, Optional): The month of the year. Default is 6 (June).
+ year (int, Optional): The year. Default is 2024.
Returns:
int: The timestamp representing the given day, month, and year.
@@ -1423,10 +1465,10 @@ def test(debug: bool = False):
minute = 30
second = 45
for year in range(1000, 9999):
- ns = Helper.time(datetime.datetime.strptime(
+ s = Helper.time(datetime.datetime.strptime(
f"{year}-{month}-{day} {hour}:{minute}:{second}", "%Y-%m-%d %H:%M:%S"
))
- date = Helper.time_to_datetime(ns)
+ date = Helper.time_to_datetime(s)
if debug:
print(date,
f'year({date.year} = {year}), month({date.month} = {month}), day({date.day} = {day}), hour({date.hour} = {hour}), minute({date.minute} = {minute})')
@@ -1435,7 +1477,7 @@ def test(debug: bool = False):
assert date.day == day
assert date.hour == hour
assert date.minute == minute
- assert date.second in [second - 1, second]
+ assert date.second == second
# human_readable_size
@@ -1928,7 +1970,12 @@ def exchange(self, account: int, created: int = None, debug: bool = False) -> di
if created is None:
created = Helper.time()
if self.account_exists(account):
- valid_rates = [(ts, r) for ts, r in self._vault['account'][account]['exchange'].items() if ts <= created]
+ valid_rates = [
+ (ts, r)
+ for ts, r in self._vault['account'][account]['exchange'].items()
+ if Helper.iso8601_to_int(ts, strict=False, debug=debug) <=
+ Helper.iso8601_to_int(created, strict=False, debug=debug)
+ ]
if valid_rates:
latest_rate = max(valid_rates, key=lambda x: x[0])
if debug:
@@ -2100,8 +2147,8 @@ def transfer(self, unscaled_amount: float | int | Decimal, from_account: int, to
created = Helper.time()
(_, ages) = self.sub(unscaled_amount, desc, from_account, created, debug=debug)
times = []
- source_exchange = self.exchange(from_account, created)
- target_exchange = self.exchange(to_account, created)
+ source_exchange = self.exchange(from_account, created, debug=debug)
+ target_exchange = self.exchange(to_account, created, debug=debug)
if debug:
print('ages', ages)
@@ -2150,7 +2197,7 @@ def check(self,
if debug:
print('check', f'debug={debug}')
if now is None:
- now = Helper.time()
+ now = Helper.time_to_milliseconds(Helper.time())
if cycle is None:
cycle = Helper.TimeCycle()
if unscaled_nisab is None:
@@ -2174,15 +2221,17 @@ def check(self,
rest = float(_box[j]['rest'])
if rest <= 0:
continue
- exchange = self.exchange(x, created=Helper.time())
+ exchange = self.exchange(x, created=Helper.time_to_datetime(Helper.time()), debug=debug)
rest = Helper.exchange_calc(rest, float(exchange['rate']), 1)
brief[0] += rest
index = limit + i - 1
- epoch = (now - j) / cycle
+ jj = j if type(j) is int else Helper.time_to_milliseconds(j)
+ epoch = (now - jj) / cycle
if debug:
print(f"Epoch: {epoch}", _box[j])
- if _box[j]['last'] > 0:
- epoch = (now - _box[j]['last']) / cycle
+ last = _box[j]['last'] if type(_box[j]['last']) is int else Helper.time_to_milliseconds(_box[j]['last'])
+ if last > 0:
+ epoch = (now - last) / cycle
if debug:
print(f"Epoch: {epoch}")
epoch = floor(epoch)
@@ -2260,7 +2309,7 @@ def zakat(self, report: tuple, parts: Dict[str, Dict | bool | Any] = None, debug
self._vault['report'][report_time] = report
created = Helper.time()
for x in plan:
- target_exchange = self.exchange(x)
+ target_exchange = self.exchange(x, debug=debug)
if debug:
print(plan[x])
print('-------------')
@@ -2288,7 +2337,7 @@ def zakat(self, report: tuple, parts: Dict[str, Dict | bool | Any] = None, debug
continue
if debug:
print('zakat-part', account, part['rate'])
- target_exchange = self.exchange(account)
+ target_exchange = self.exchange(account, debug=debug)
amount = Helper.exchange_calc(part['part'], part['rate'], target_exchange['rate'])
self.sub(
unscaled_value=Helper.unscale(int(amount)),
@@ -2358,8 +2407,7 @@ class Box(db.Entity):
_table_ = 'box'
id = pony.PrimaryKey(int, auto=True)
account = pony.Required(Account, column='account_id')
- time = pony.Required(int, size=64, unique=True)
- record_date = pony.Required(datetime.datetime)
+ record_date = pony.Required(datetime.datetime, unique=True)
capital = pony.Required(int, size=64)
count = pony.Optional(int, size=64, default=0)
last = pony.Optional(datetime.datetime)
@@ -2373,8 +2421,7 @@ class Log(db.Entity):
_table_ = 'log'
id = pony.PrimaryKey(int, auto=True)
account = pony.Required(Account, column='account_id')
- time = pony.Required(int, size=64, unique=True)
- record_date = pony.Required(datetime.datetime)
+ record_date = pony.Required(datetime.datetime, unique=True)
value = pony.Required(int, size=64)
desc = pony.Required(pony.LongStr)
ref = pony.Optional(int, size=64)
@@ -2386,8 +2433,7 @@ class File(db.Entity):
_table_ = 'file'
id = pony.PrimaryKey(int, auto=True)
log = pony.Required(Log, column='log_id')
- time = pony.Required(int, size=64, unique=True)
- record_date = pony.Required(datetime.datetime)
+ record_date = pony.Required(datetime.datetime, unique=True)
path = pony.Required(pony.LongStr)
name = pony.Optional(pony.LongStr)
created_at = pony.Required(datetime.datetime, default=lambda: datetime.datetime.now())
@@ -2398,17 +2444,15 @@ class Exchange(db.Entity):
_table_ = 'exchange'
id = pony.PrimaryKey(int, auto=True)
account = pony.Required(Account, column='account_id')
- time = pony.Required(int, size=64, unique=True)
+ record_date = pony.Required(datetime.datetime, unique=True)
rate = pony.Required(Decimal)
desc = pony.Optional(pony.LongStr)
- record_date = pony.Required(datetime.datetime)
class Report(db.Entity):
_table_ = 'report'
id = pony.PrimaryKey(int, auto=True)
- time = pony.Required(int, size=64, unique=True)
- record_date = pony.Required(datetime.datetime)
+ record_date = pony.Required(datetime.datetime, unique=True)
details = pony.Required(pony.Json)
created_at = pony.Required(datetime.datetime, default=lambda: datetime.datetime.now())
@@ -3218,7 +3262,7 @@ def __init__(self, model: Model):
"""
self.db = model
- def build_payment_parts(self, scaled_demand: int, positive_only: bool = True) -> dict:
+ def build_payment_parts(self, scaled_demand: int, positive_only: bool = True, debug: bool = False) -> dict:
"""
Build payment parts for the Zakat distribution.
@@ -3248,7 +3292,7 @@ def build_payment_parts(self, scaled_demand: int, positive_only: bool = True) ->
if positive_only and y <= 0:
continue
total += float(y)
- exchange = self.db.exchange(account=x)
+ exchange = self.db.exchange(account=x, debug=debug)
parts['account'][x] = {'balance': y, 'rate': exchange['rate'], 'part': 0}
parts['total'] = total
return parts
@@ -3907,7 +3951,7 @@ def test(self, debug: bool = False) -> bool:
},
}
- selected_time = Helper.time() - Helper.TimeCycle()
+ selected_time = Helper.datetime_to_milliseconds(Helper.time_to_datetime(Helper.time())) - Helper.TimeCycle()
account_ages_ref, _ = self.db.account(name='ages')
account_future_ref, _ = self.db.account(name='future')
@@ -4046,7 +4090,7 @@ def test(self, debug: bool = False) -> bool:
assert self.db.log_size(y) == z[12]
if debug:
- pp().pprint(self.db.check(2.17))
+ pp().pprint(self.db.check(2.17, debug=debug))
# storage
@@ -4110,10 +4154,10 @@ def test(self, debug: bool = False) -> bool:
# payment parts
- positive_parts = self.build_payment_parts(100, positive_only=True)
+ positive_parts = self.build_payment_parts(100, positive_only=True, debug=debug)
assert Helper.check_payment_parts(positive_parts) != 0
assert Helper.check_payment_parts(positive_parts) != 0
- all_parts = self.build_payment_parts(300, positive_only=False)
+ all_parts = self.build_payment_parts(300, positive_only=False, debug=debug)
assert Helper.check_payment_parts(all_parts) != 0
assert Helper.check_payment_parts(all_parts) != 0
if debug:
@@ -4139,7 +4183,7 @@ def test(self, debug: bool = False) -> bool:
}
j = ''
for x, y in part['account'].items():
- x_exchange = self.db.exchange(x)
+ x_exchange = self.db.exchange(x, debug=debug)
zz = Helper.exchange_calc(z, 1, x_exchange['rate'])
if exceed and zz <= demand:
i += 1
@@ -4174,7 +4218,7 @@ def test(self, debug: bool = False) -> bool:
print('check_payment_parts', result, f'exceed: {exceed}')
assert result == 0
- report = self.db.check(2.17, None, debug)
+ report = self.db.check(2.17, debug=debug)
(valid, brief, plan) = report
if debug:
print('valid', valid)
@@ -4266,7 +4310,7 @@ def test(self, debug: bool = False) -> bool:
# Transfer all in many chunks randomly from B to A
a_SAR_balance = 137125
b_USD_balance = 50100
- b_USD_exchange = self.db.exchange(account_b_USD_ref)
+ b_USD_exchange = self.db.exchange(account_b_USD_ref, debug=debug)
amounts = ZakatTracker.create_random_list(b_USD_balance, max_value=1000)
if debug:
print('amounts', amounts)
@@ -4362,13 +4406,13 @@ def test(self, debug: bool = False) -> bool:
if debug:
print('rate', rate, 'values', values)
for case in [
- (a, account_safe_ref, Helper.time() - Helper.TimeCycle(), [
+ (a, account_safe_ref, Helper.time_to_milliseconds(Helper.time()) - Helper.TimeCycle(), [
{account_safe_ref: {0: {'below_nisab': x}}},
], False, m),
- (b, account_safe_ref, Helper.time() - Helper.TimeCycle(), [
+ (b, account_safe_ref, Helper.time_to_milliseconds(Helper.time()) - Helper.TimeCycle(), [
{account_safe_ref: {0: {'count': 1, 'total': y}}},
], True, n),
- (c, account_cave_ref, Helper.time() - (Helper.TimeCycle() * 3), [
+ (c, account_cave_ref, Helper.time_to_milliseconds(Helper.time()) - (Helper.TimeCycle() * 3), [
{account_cave_ref: {0: {'count': 3, 'total': z}}},
], True, o),
]:
@@ -4386,7 +4430,7 @@ def test(self, debug: bool = False) -> bool:
)
assert self.db.snapshot()
- report = self.db.check(2.17, None, debug)
+ report = self.db.check(2.17, debug=debug)
(valid, brief, plan) = report
if debug:
print('brief', brief)
@@ -4411,7 +4455,7 @@ def test(self, debug: bool = False) -> bool:
if debug:
print('zakat-result', result, case[4])
assert result == case[4]
- report = self.db.check(2.17, None, debug)
+ report = self.db.check(2.17, debug=debug)
(valid, brief, plan) = report
assert valid is False
return True
@@ -4443,11 +4487,11 @@ def test(debug: bool = False):
debug=True,
),
]:
- start = Helper.time()
+ start = time_ns()
assert model.test(debug=debug)
ledger = ZakatTracker(model=model)
assert ledger.test(debug=debug)
- durations[model.__class__.__name__] = Helper.time() - start
+ durations[model.__class__.__name__] = time_ns() - start
if debug:
print("#########################")
print("######## TEST DONE ########")