From 58d37463c6dd5e6570cce3bedf5dd6623e3725a5 Mon Sep 17 00:00:00 2001 From: Chris Berkhout Date: Mon, 26 Apr 2021 13:43:05 +0200 Subject: [PATCH] Make a Format dataclass, add --quantize option. --- src/pricehist/cli.py | 37 +++++++++++++++++++++-------- src/pricehist/format.py | 27 +++++++++++++++++++++ src/pricehist/formatinfo.py | 7 ------ src/pricehist/outputs/beancount.py | 12 +++++----- src/pricehist/outputs/csv.py | 12 +++++----- src/pricehist/outputs/gnucashsql.py | 14 +++++------ src/pricehist/outputs/ledger.py | 21 ++++++++-------- 7 files changed, 83 insertions(+), 47 deletions(-) create mode 100644 src/pricehist/format.py delete mode 100644 src/pricehist/formatinfo.py diff --git a/src/pricehist/cli.py b/src/pricehist/cli.py index 31aaa28..2f0f683 100644 --- a/src/pricehist/cli.py +++ b/src/pricehist/cli.py @@ -3,7 +3,7 @@ from datetime import datetime, timedelta from pricehist import outputs, sources from pricehist import __version__ -from pricehist.formatinfo import FormatInfo +from pricehist.format import Format def cli(args=None): @@ -62,17 +62,24 @@ def cmd_fetch(args): for p in prices ] - default = FormatInfo() + default = Format() - fi = FormatInfo( - time=(args.renametime or default.time), - decimal=(args.formatdecimal or default.decimal), - thousands=(args.formatthousands or default.thousands), - symbol=(args.formatsymbol or default.symbol), - datesep=(args.formatdatesep or default.datesep), + def if_not_none(value, default): + if value is None: + return default + else: + return value + + fmt = Format( + time=if_not_none(args.renametime, default.time), + decimal=if_not_none(args.formatdecimal, default.decimal), + thousands=if_not_none(args.formatthousands, default.thousands), + symbol=if_not_none(args.formatsymbol, default.symbol), + datesep=if_not_none(args.formatdatesep, default.datesep), + decimal_places=if_not_none(args.quantize, default.decimal_places), ) - print(output.format(prices, format_info=fi), end="") + print(output.format(prices, fmt=fmt), end="") def build_parser(): @@ -122,7 +129,10 @@ def build_parser(): usage=( "pricehist fetch SOURCE PAIR " "[-h] (-s DATE | -sx DATE) [-e DATE] [-o FMT] " - "[--rename-base SYM] [--rename-quote SYM] [--rename-time TIME]" + "[--rename-base SYM] [--rename-quote SYM] [--rename-time TIME] " + "[--format-decimal CHAR] [--format-thousands CHAR] " + "[--format-symbol rightspace|right|leftspace|left] [--format-datesep CHAR] " + "[--quantize INT]" ), ) fetch_parser.add_argument( @@ -229,5 +239,12 @@ def build_parser(): type=str, help="date separator", ) + fetch_parser.add_argument( + "--quantize", + dest="quantize", + metavar="INT", + type=int, + help="quantize to given number of decimal places", + ) return parser diff --git a/src/pricehist/format.py b/src/pricehist/format.py new file mode 100644 index 0000000..889a452 --- /dev/null +++ b/src/pricehist/format.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass +from decimal import Decimal, getcontext + + +@dataclass(frozen=True) +class Format: + time: str = "00:00:00" + decimal: str = "." + thousands: str = "" + symbol: str = "rightspace" + datesep: str = "-" + decimal_places: int = None + + def quantize(self, num): + if self.decimal_places is None: + return num + else: + prec = getcontext().prec + digits = len(num.as_tuple().digits) + exponent = num.as_tuple().exponent + + fractional_digits = -exponent + whole_digits = digits - fractional_digits + max_decimal_places = prec - whole_digits + chosen_decimal_places = min(self.decimal_places, max_decimal_places) + + return num.quantize(Decimal("0." + ("0" * chosen_decimal_places))) diff --git a/src/pricehist/formatinfo.py b/src/pricehist/formatinfo.py deleted file mode 100644 index 735189c..0000000 --- a/src/pricehist/formatinfo.py +++ /dev/null @@ -1,7 +0,0 @@ -from collections import namedtuple - -FormatInfo = namedtuple( - "FormatInfo", - ["time", "decimal", "thousands", "symbol", "datesep"], - defaults=["00:00:00", ".", "", "rightspace", "-"], -) diff --git a/src/pricehist/outputs/beancount.py b/src/pricehist/outputs/beancount.py index 4a9776c..938c880 100644 --- a/src/pricehist/outputs/beancount.py +++ b/src/pricehist/outputs/beancount.py @@ -1,23 +1,23 @@ -from pricehist.formatinfo import FormatInfo +from pricehist.format import Format class Beancount: - def format(self, prices, format_info=FormatInfo()): + def format(self, prices, fmt=Format()): lines = [] for price in prices: - amount_parts = f"{price.amount:,}".split(".") - amount_parts[0] = amount_parts[0].replace(",", format_info.thousands) + amount_parts = f"{fmt.quantize(price.amount):,}".split(".") + amount_parts[0] = amount_parts[0].replace(",", fmt.thousands) amount = ".".join(amount_parts) qa_parts = [amount] - if format_info.symbol == "right": + if fmt.symbol == "right": qa_parts = qa_parts + [price.quote] else: qa_parts = qa_parts + [" ", price.quote] quote_amount = "".join(qa_parts) - date = str(price.date).replace("-", format_info.datesep) + date = str(price.date).replace("-", fmt.datesep) lines.append(f"{date} price {price.base} {quote_amount}") return "\n".join(lines) + "\n" diff --git a/src/pricehist/outputs/csv.py b/src/pricehist/outputs/csv.py index 330bfe4..99df4da 100644 --- a/src/pricehist/outputs/csv.py +++ b/src/pricehist/outputs/csv.py @@ -1,14 +1,14 @@ -from pricehist.formatinfo import FormatInfo +from pricehist.format import Format class CSV: - def format(self, prices, format_info=FormatInfo()): + def format(self, prices, fmt=Format()): lines = ["date,base,quote,amount"] for price in prices: - date = str(price.date).replace("-", format_info.datesep) - amount_parts = f"{price.amount:,}".split(".") - amount_parts[0] = amount_parts[0].replace(",", format_info.thousands) - amount = format_info.decimal.join(amount_parts) + date = str(price.date).replace("-", fmt.datesep) + amount_parts = f"{fmt.quantize(price.amount):,}".split(".") + amount_parts[0] = amount_parts[0].replace(",", fmt.thousands) + amount = fmt.decimal.join(amount_parts) line = ",".join([date, price.base, price.quote, amount]) lines.append(line) return "\n".join(lines) + "\n" diff --git a/src/pricehist/outputs/gnucashsql.py b/src/pricehist/outputs/gnucashsql.py index fa672a3..615c04b 100644 --- a/src/pricehist/outputs/gnucashsql.py +++ b/src/pricehist/outputs/gnucashsql.py @@ -3,27 +3,27 @@ from datetime import datetime from importlib.resources import read_text from pricehist import __version__ -from pricehist.formatinfo import FormatInfo +from pricehist.format import Format class GnuCashSQL: - def format(self, prices, format_info=FormatInfo()): - fi = format_info + def format(self, prices, fmt=Format()): source = "pricehist" typ = "unknown" values_parts = [] for price in prices: - date = f"{price.date} {fi.time}" + date = f"{price.date} {fmt.time}" + amount = fmt.quantize(price.amount) m = hashlib.sha256() m.update( "".join( - [date, price.base, price.quote, source, typ, str(price.amount)] + [date, price.base, price.quote, source, typ, str(amount)] ).encode("utf-8") ) guid = m.hexdigest()[0:32] - value_num = str(price.amount).replace(".", "") - value_denom = 10 ** len(f"{price.amount}.".split(".")[1]) + value_num = str(amount).replace(".", "") + value_denom = 10 ** len(f"{amount}.".split(".")[1]) v = ( "(" f"'{guid}', " diff --git a/src/pricehist/outputs/ledger.py b/src/pricehist/outputs/ledger.py index 3b0c0af..0467c19 100644 --- a/src/pricehist/outputs/ledger.py +++ b/src/pricehist/outputs/ledger.py @@ -1,29 +1,28 @@ -from pricehist.formatinfo import FormatInfo +from pricehist.format import Format class Ledger: - def format(self, prices, format_info=FormatInfo()): - fi = format_info + def format(self, prices, fmt=Format()): lines = [] for price in prices: - date = str(price.date).replace("-", fi.datesep) + date = str(price.date).replace("-", fmt.datesep) - amount_parts = f"{price.amount:,}".split(".") - amount_parts[0] = amount_parts[0].replace(",", format_info.thousands) - amount = format_info.decimal.join(amount_parts) + amount_parts = f"{fmt.quantize(price.amount):,}".split(".") + amount_parts[0] = amount_parts[0].replace(",", fmt.thousands) + amount = fmt.decimal.join(amount_parts) qa_parts = [amount] - if format_info.symbol == "left": + if fmt.symbol == "left": qa_parts = [price.quote] + qa_parts - elif format_info.symbol == "leftspace": + elif fmt.symbol == "leftspace": qa_parts = [price.quote, " "] + qa_parts - elif format_info.symbol == "right": + elif fmt.symbol == "right": qa_parts = qa_parts + [price.quote] else: qa_parts = qa_parts + [" ", price.quote] quote_amount = "".join(qa_parts) - lines.append(f"P {date} {fi.time} {price.base} {quote_amount}") + lines.append(f"P {date} {fmt.time} {price.base} {quote_amount}") return "\n".join(lines) + "\n" # TODO support additional details of the format: