// Copyright (C) 2019 David N. Cohen
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package main
import (
"errors"
"fmt"
"math/big"
"strings"
"time"
"src.d10.dev/command"
)
func init() {
command.RegisterOperation(command.Operation{
Handler: baseMain,
Name: "base",
Syntax: "base [-b=<begin date>]",
Description: "Convert price/cost information to base currency (using ledger-cli price data).",
})
}
func baseMain() error {
// define flags
beginFlag := command.OperationFlagSet.String("b", "", "begin date")
err := command.OperationFlagSet.Parse(command.Args()[1:])
if err != nil {
return err
}
// validate flags
if base == "" {
return errors.New("A base currency is required, i.e. `-base=USD`.")
}
var begin time.Time
if *beginFlag != "" {
begin, err = time.Parse("2006/01/02", *beginFlag)
if err != nil {
command.Check(fmt.Errorf("bad begin date (%q): %w", *beginFlag, err))
}
}
for scanner.Scan() {
txLines := scanner.Lines()
// observe price information, if any
// https://www.ledger-cli.org/3.0/doc/ledger3.html#Commodity-price-histories
priceHistory := make(map[string]*big.Rat)
for _, line := range txLines.Line {
// we're looking for, i.e. "P 2004/06/21 02:17:58 TWCUX 27.76 USD"
if strings.HasPrefix(line, "P ") {
command.V(2).Info("\t", line) // debug
field := strings.Fields(line)
date, err := time.Parse("2006/01/02 15:04:05", strings.Join(field[1:3], " "))
if err != nil {
command.Check(fmt.Errorf("failed to parse historical price (%q): %w", line, err))
}
if field[5] != string(base) {
command.V(1).Infof("ignoring non-base price (%q)", line)
continue
}
price, ok := new(big.Rat).SetString(field[4])
if !ok {
command.Check(fmt.Errorf("failed to parse historical price (%q)", line))
}
key := historyKey(date, Asset(field[3]))
old, ok := priceHistory[key]
if ok {
command.V(1).Infof("updating price history (was %f, now %f)\n\t%s", old, price, line)
}
priceHistory[key] = price
}
} // end collect price history
payee, payeeIndex := txLines.Payee()
if payeeIndex == PayeeNotFound {
// not a transaction (maybe a comment)
writeLines(append(txLines.Line, "")) // with a blank
continue
}
if begin.After(txLines.Date) {
writeLines(append(txLines.Line, "")) // with a blank
continue
}
command.V(2).Info("\t", payee) // debug
// first pass, find conversions to base
conversion := make(map[string]Amount)
for _, line := range txLines.Line[payeeIndex+1:] {
split, ok := parseSplit(line)
if !ok {
if !strings.HasPrefix(strings.TrimLeft(line, " \t"), ";") { // check comment
command.Check(fmt.Errorf("failed to parse transaction split: %q", line))
}
continue // comment is noop
}
if split.cost == nil && split.price == nil {
// no price or cost to convert
continue
}
cost := split.Cost()
if cost == nil || cost.Asset == base {
continue
}
// here we have a cost that must be converted into base currency
key := historyKey(txLines.Date, cost.Asset)
price, ok := priceHistory[key]
if !ok {
// strict
command.Check(fmt.Errorf("price of %s on %s unknown", cost, txLines.Date.Format("2006/01/02")))
}
tmp := new(big.Rat).Mul(price, cost.Rat)
basis := NewAmount(base, *tmp)
command.V(1).Infof("%s @@ %s", cost, basis)
conversion[cost.String()] = basis
} // end first pass
// second pass, alter
for index, line := range txLines.Line[payeeIndex+1:] {
split, ok := parseSplit(line)
if !ok {
continue // comment is noop
}
if split.cost != nil || split.price != nil {
basis, ok := conversion[split.Cost().String()]
if ok {
// replace existing cost/price with basis
txLines.Line[payeeIndex+1+index] = strings.Replace(line, "@", fmt.Sprintf("@@ %s ; @", basis), 1)
}
} else if split.delta != nil {
deltaStr := split.delta.NegClone().String()
basis, ok := conversion[deltaStr]
if ok {
// add basis where there may be no price
txLines.Line[payeeIndex+1+index] = strings.Replace(line, deltaStr, fmt.Sprintf("%s @@ %s ; ", deltaStr, basis), 1)
}
}
} // end second pass
// write txLines (which may have been modified above)
writeLines(txLines.Line)
fmt.Println("") // blank line between transactions
} // end scan loop
return nil
}
func historyKey(date time.Time, asset Asset) string {
return fmt.Sprintf("%s %s", date.Format("2006/01/02"), asset)
}