def test_sorted_uniquify_last(self): data = [('d', 9), ('b', 4), ('c', 8), ('c', 6), ('c', 7), ('a', 3), ('a', 1), ('a', 2), ('b', 5)] unique_data = misc_utils.sorted_uniquify(data, lambda x: x[0], last=True) self.assertEqual([('a', 2), ('b', 5), ('c', 7), ('d', 9)], list(unique_data))
def build_price_map(entries): """Build a price map from a list of arbitrary entries. If multiple prices are found for the same (currency, cost-currency) pair at the same date, the latest date is kept and the earlier ones (for that day) are discarded. If inverse price pairs are found, e.g. USD in AUD and AUD in USD, the inverse that has the smallest number of price points is converted into the one that has the most price points. In that way they are reconciled into a single one. Args: entries: A list of directives, hopefully including some Price and/or Transaction entries. Returns: A dict of (currency, cost-currency) keys to sorted lists of (date, number) pairs, where 'date' is the date the price occurs at and 'number' a Decimal that represents the price, or rate, between these two currencies/commodities. Each date occurs only once in the sorted list of prices of a particular key. All of the inverses are automatically generated in the price map. """ # Fetch a list of all the price entries seen in the ledger. price_entries = [entry for entry in entries if isinstance(entry, Price)] # Build a map of exchange rates between these units. # (base-currency, quote-currency) -> List of (date, rate). price_map = collections.defaultdict(list) for price in price_entries: base_quote = (price.currency, price.amount.currency) price_map[base_quote].append((price.date, price.amount.number)) # Find pairs of inversed units. inversed_units = [] for base_quote, values in price_map.items(): base, quote = base_quote if (quote, base) in price_map: inversed_units.append(base_quote) # Find pairs of inversed units, and swallow the conversion with the smaller # number of rates into the other one. for base, quote in inversed_units: bq_prices = price_map[(base, quote)] qb_prices = price_map[(quote, base)] remove = ((base, quote) if len(bq_prices) < len(qb_prices) else (quote, base)) base, quote = remove remove_list = price_map[remove] insert_list = price_map[(quote, base)] del price_map[remove] inverted_list = [(date, ONE/rate) for (date, rate) in remove_list if rate != ZERO] insert_list.extend(inverted_list) # Unzip and sort each of the entries and eliminate duplicates on the date. sorted_price_map = PriceMap({ base_quote: list(misc_utils.sorted_uniquify(date_rates, lambda x: x[0], last=True)) for (base_quote, date_rates) in price_map.items()}) # Compute and insert all the inverted rates. forward_pairs = list(sorted_price_map.keys()) for (base, quote), price_list in list(sorted_price_map.items()): # Note: You have to filter out zero prices for zero-cost postings, like # gifted options. sorted_price_map[(quote, base)] = [ (date, ONE/price) for date, price in price_list if price != ZERO] sorted_price_map.forward_pairs = forward_pairs return sorted_price_map