diff --git a/README.md b/README.md index 2dd37d9..58169fc 100644 --- a/README.md +++ b/README.md @@ -24,16 +24,16 @@ took by a simple text format. This is a CLI program that is broken down to three separate programs: - `piggybank-put` — puts coins in a piggy bank and create a new one if - there is no such file; + there is no such file; - `piggybank-take` — takes coins from an existsing piggy bank file; - `piggybank-show` — shows summarised information on a piggy bank and, - optionally, print a list of transactions. + optionally, print a list of transactions. ## Common flags ```bash piggybank-* [(-h | --help) | (-v | --version) | (-L | --list-currencies) - | --set-default-currency] + | --set-default-currency] ``` These flags are used with every sub-program. @@ -92,7 +92,7 @@ All the parameters are explained above. piggybank-take -r 0 0 4 from example.pb ``` -## How to see what do you have +## How to see what you have in there ```bash piggybank-show FILE [ with (t | transactions)] @@ -107,4 +107,29 @@ file. ```bash piggybank-show example.pb with t -``` \ No newline at end of file +``` + +# Configuration + +Configuration file `piggybank.conf` is stored in directory specified by +`$XDG_CONFIG_HOME` environment variable or, if it isn't set, in `$HOME/.config` +on Linux and in `%APPDATA%` on Windows. + +By default it has following content: + + default-currency = SRUB + +Aside from `default-currency` there is also a `currency.*` parameter. `*` is a +currency's ISO code, e.g. `currency.RUB`, `currency.USD`, `currency.EUR` etc. +This parameter allows you to add a currency needed for you, but not presented in +program. + +Currency is defined following way: + + currency.ISO = ISO;NAME;DESCRIPTION;COINS_COUNT;COINS_NAME;FACE_VALUES + +That is best described with an example. Here it is: + + currency.SRUB = SRUB;Russian ruble (shortened);Russian Federation. Excluding coins of 1 and 5 kopek;6;10к.,50к.,1₽,2₽,5₽,10₽;10,50,100,200,500,1000 + +Yes, long and clunky way... May be I'll come up with something better. diff --git a/piggybank/__init__.py b/piggybank/__init__.py index 465b723..3f47e0c 100644 --- a/piggybank/__init__.py +++ b/piggybank/__init__.py @@ -3,7 +3,7 @@ __all__ = ["__date__", "__version__", "__author__", "__author_email__", "print_program_version"] -__date__ = "5 June 2020" +__date__ = "6 June 2020" __version__ = "1.0.0" __author__ = "Alexander \"Arav\" Andreev" __author_email__ = "me@arav.top" diff --git a/piggybank/configuration.py b/piggybank/configuration.py index f2046c2..b66d1e2 100644 --- a/piggybank/configuration.py +++ b/piggybank/configuration.py @@ -1,4 +1,14 @@ -"""An implementation of a simple key=value configuration.""" +"""An implementation of a slightly advanced key=value configuration file. +Commentary lines are started with # symbol. +Keys and values are separated by ' = ' (spaces are necessary). +Keys with . symbol are splited and right part is a nested dictionary. +Example: + # A commentary line + default_something = 10 + currency.RUB = ... +Above example is translated as a following dictionary: + { "default_something": 10, "currency": { "RUB": "..." } } +""" from os import getenv from os.path import exists, join @@ -8,7 +18,8 @@ from typing import Union __all__ = ["Configuration", "get_configuration_path"] -def get_configuration_path(): +def get_configuration_path() -> str: + """Returns a path to where configuration is stored.""" if system() == "Linux": return getenv("XDG_CONFIG_HOME") or f"{getenv('HOME')}/.config" elif system() == "Windows": @@ -16,8 +27,8 @@ def get_configuration_path(): DEFAULT_CONFIGURATION = { - "default-currency": "SRUB" -} + "default-currency": "SRUB" } + DEFAULT_CONFIGURATION_FILE = join(get_configuration_path(), "piggybank.conf") @@ -34,8 +45,14 @@ class Configuration: def load(self) -> None: for line in open(self._configuration_file, 'r'): + if line[0] == "#": + continue key, value = line[:-1].split(" = ") - self._configuration[key] = value + if key.find(".") != -1: + k0, k1 = key.split(".") + self._configuration[k0] = {k1 : value} + else: + self._configuration[key] = value def save(self) -> None: with open(self._configuration_file, 'w') as cf: diff --git a/piggybank/currencies.py b/piggybank/currencies.py index b7b9df5..1d166cd 100644 --- a/piggybank/currencies.py +++ b/piggybank/currencies.py @@ -1,28 +1,19 @@ -"""Here is a dictionary of supported currencies defined. Which could be easily -extended with new ones. +"""Dictionary of supported currencies as instances of a Currency class. -Each entry is a dictionary that has an ISO code of a currency as its key. Or it -can be slightly differ from an ISO code to represent a modified version of a -currency. +Face values are stored as decimals*. -Each entry consists of following fields: - name -- a full name of a currency; - description -- usually a country where this currency is used is being - mentioned. Plus additional information; - count -- a number of coins in a currency; - names -- an array of names for each coins' face values; - multipliers -- an array of multipliers for each face value in a decimal* - format. - -* - Decimal is used to avoid problems of rounding float numbers. So first two +* Decimal is used to avoid problems of rounding float numbers. So first two digits to the right are used to store a fraction part. You can simply divide -this number by 100 to get a regular floating-point number. +this number by 100 to get a floating-point number. """ +from __future__ import annotations +from operator import mul from typing import Dict, List, TypedDict -__all__ = ["CURRENCIES", "BaseCurrencyError", "CurrencyIsNotSupportedError", - "CurrenciesCoinCountMismatchError", "CurrencyMismatchError"] +__all__ = [ "Currency", "CURRENCIES", "BaseCurrencyError", + "CurrencyIsNotSupportedError", "CurrenciesCoinCountMismatchError", + "CurrencyMismatchError"] class BaseCurrencyError(Exception): @@ -52,63 +43,106 @@ class CurrenciesCoinCountMismatchError(BaseCurrencyError): pass -class Currency(TypedDict): - name: str - description: str - count: int - names: List[str] - multipliers: List[int] +class Currency: + """Currency's representation and methods to work with it. + + Arguments: + iso_code -- ISO code of a currency; + name -- full name of a currency; + description -- description of a currency; + coins_count -- number of face values of coins; + coin_names -- list of names of face values (e.g. 1c, 5c); + face_values -- list of values for each face value in decimal + (e.g. $476.40 is 47640); + """ + def __init__(self, iso_code: str, name: str, description: str, + coins_count: int, coin_names: List[str], + face_values: List[int]): + self._iso = iso_code.upper() + self._name = name + self._description = description + self._coins_count = coins_count + self._coin_names = coin_names + self._face_values = face_values + + @property + def iso(self) -> str: + """ISO code of a currency.""" + return self._iso + + @property + def name(self) -> str: + """Full name of a currency.""" + return self._name + + @property + def description(self) -> str: + """Description of a currency (e.g. country).""" + return self._description + + @property + def count(self): + """Count of coins of a currency.""" + return self._coins_count + + @property + def coin_names(self) -> List[str]: + """Names of coins (usually face values, e.g. 25¢). + From least to most.""" + return self._coin_names + + @property + def face_values(self) -> List[int]: + """Face values of coins. From least to most.""" + return self._face_values + + def multiply(self, lst: List[int]) -> List[float]: + """Multiplies a list by face values. Length of list must match + currency's coins count.""" + if len(lst) != len(self): + raise CurrenciesCoinCountMismatchError + return list(map(mul, lst, self.face_values)) + + @staticmethod + def from_string(currency: str) -> Currency: + """Creates a Currency object from its string representation.""" + iso, name, desc, ccnt, fvn, fv = currency.split(";") + return Currency(iso, name, desc, int(ccnt), + fvn.split(","), list(map(int, fv.split(",")))) + + def __mul__(self, lst: List[int]) -> List[float]: + return self.multiply(lst) + + def __len__(self) -> int: + return self.count + + def __str__(self): + return f"{self.iso};{self.name};{self.description};" \ + f"{self.count};{','.join(self.coin_names)};" \ + f"{','.join(list(map(str, self.face_values)))}" CURRENCIES: Dict[str, Currency] = { - "RUB": { - "name": "Ruble", - "description": "Russian Federation", - "count": 8, - "names": ["1к.", "5к.", "10к.", "50к.", "1₽", "2₽", "5₽", "10₽"], - "multipliers": [1, 5, 10, 50, 1_00, 2_00, 5_00, 10_00] - }, - "SRUB": { - "name": "Ruble (shortened)", - "description": "Russian Federation. Excluding coins of 1 and 5 kopek", - "count": 6, - "names": ["10к.", "50к.", "1₽", "2₽", "5₽", "10₽"], - "multipliers": [10, 50, 1_00, 2_00, 5_00, 10_00] - }, - "BYN": { - "name": "Belarusian ruble", - "description": "Belarus", - "count": 8, - "names": ["1к.", "2к.", "5к.", "10к.", "20к.", "50к.", "1р.", "2р."], - "multipliers": [1, 2, 5, 10, 20, 50, 1_00, 2_00] - }, - "UAH": { - "name": "Ukrainian hryvnia", - "description": "Ukraine", - "count": 10, - "names": ["1к.", "2к.", "5к.", "10к.", "25к.", "50к.", "₴1", "₴2", - "₴5", "₴10"], - "multipliers": [1, 2, 5, 10, 25, 50, 1_00, 2_00, 5_00, 10_00] - }, - "USD": { - "name": "Dollar", - "description": "United States of America", - "count": 6, - "names": ["1¢", "5¢", "10¢", "25¢", "50¢", "$1"], - "multipliers": [1, 5, 10, 25, 50, 1_00] - }, - "EUR": { - "name": "Euro", - "description": "European Union", - "count": 8, - "names": ["1c", "2c", "5c", "10c", "20c", "50c", "€1", "€2"], - "multipliers": [1, 2, 5, 10, 20, 50, 1_00, 2_00] - }, - "GBP": { - "name": "Pound sterling", - "description": "United Kingdom", - "count": 9, - "names": ["1p", "2p", "5p", "10p", "20p", "25p", "50p", "£1", "£2"], - "multipliers": [1, 2, 5, 10, 20, 25, 50, 1_00, 2_00] - } + "RUB": Currency("RUB", "Russian ruble", "Russian Federation", 8, + ["1к.", "5к.", "10к.", "50к.", "1₽", "2₽", "5₽", "10₽"], + [1, 5, 10, 50, 1_00, 2_00, 5_00, 10_00]), + "SRUB": Currency("SRUB", "Russian ruble (shortened)", + "Russian Federation. Excluding coins of 1 and 5 kopek.", + 8, ["10к.", "50к.", "1₽", "2₽", "5₽", "10₽"], + [10, 50, 1_00, 2_00, 5_00, 10_00]), + "BYN": Currency("BYN", "Belarusian ruble", "Belarus", 8, + ["1к.", "2к.", "5к.", "10к.", "20к.", "50к.", "1р.", "2р."], + [1, 2, 5, 10, 20, 50, 1_00, 2_00]), + "UAH": Currency("UAH", "Ukrainian hryvnia", "Ukraine", 10, + ["1к.", "2к.", "5к.", "10к.", "25к.", "50к.", "₴1", "₴2", "₴5", "₴10"], + [1, 2, 5, 10, 25, 50, 1_00, 2_00, 5_00, 10_00]), + "USD": Currency("USD", "US Dollar", "United States of America", 6, + ["1¢", "5¢", "10¢", "25¢", "50¢", "$1"], + [1, 5, 10, 25, 50, 1_00]), + "EUR": Currency("EUR", "Euro", "European Union", 8, + ["1c", "2c", "5c", "10c", "20c", "50c", "€1", "€2"], + [1, 2, 5, 10, 20, 50, 1_00, 2_00]), + "GBP": Currency("GBP", "Pound sterling", "United Kingdom", 9, + ["1p", "2p", "5p", "10p", "20p", "25p", "50p", "£1", "£2"], + [1, 2, 5, 10, 20, 25, 50, 1_00, 2_00]) }