use super::funejson::Value; use super::dates_add_days::epoch_day_from_iso; use super::money_amount::{money_from_value, money_to_value, Money}; /// The identification rules below apply to disposals on or after this date (FA 2008). const RULES_START: &str = "2008-04-06"; /// One day's purchases or sales, added together as the same-day rule requires. struct DayTotal { date: String, day: i64, quantity: i64, amount: i128, /// Shares and cost still unmatched (purchases) or unidentified (sales). remaining: i64, remaining_amount: i128, } struct Pending { rule: &'static str, acquisition_date: Option, quantity: i64, cost: i128, } fn gbp(minor: i128) -> Money { Money { minor: minor as i64, currency: "GBP".to_string(), } } /// a * b / c rounded half-up, all non-negative. fn share(a: i128, b: i128, c: i128) -> i128 { (2 * a * b + c) / (2 * c) } /// Take `quantity` shares from a purchase (or the pool) and the cost that goes /// with them. Cost is apportioned by number of shares, rounded half-up to the /// penny, and taken from what is left, so the pieces always add back to the /// whole: the last shares out take the last pennies. fn take(total: &mut DayTotal, quantity: i64) -> i128 { let cost = if quantity == total.remaining { total.remaining_amount } else { share(total.remaining_amount, quantity as i128, total.remaining as i128) }; total.remaining -= quantity; total.remaining_amount -= cost; cost } fn check(tx: &ShareTransaction, previous: Option<&str>) { epoch_day_from_iso(&tx.date); if let Some(previous) = previous { if tx.date.as_str() < previous { panic!("transactions must be in date order: {} comes after {}", tx.date, previous); } } if tx.kind != "buy" && tx.kind != "sell" { panic!("kind must be buy or sell, received \"{}\"", tx.kind); } if tx.quantity <= 0 { panic!("quantity must be a whole number greater than zero, received {} on {}", tx.quantity, tx.date); } if tx.amount.currency != "GBP" { panic!("amounts must be in GBP, received {} on {}", tx.amount.currency, tx.date); } if tx.amount.minor < 0 { panic!( "amount must be a whole number of pence, not negative, received {} on {}", tx.amount.minor, tx.date ); } if tx.kind == "sell" && tx.date.as_str() < RULES_START { panic!( "disposals before {} follow earlier identification rules, received {}", RULES_START, tx.date ); } } fn new_day(date: &str) -> DayTotal { DayTotal { date: date.to_string(), day: epoch_day_from_iso(date), quantity: 0, amount: 0, remaining: 0, remaining_amount: 0, } } /// Identify each disposal of shares with acquisitions the way TCGA 1992 /// s105-106A require for individuals from 6 April 2008: first with shares /// bought the same day, then with shares bought in the 30 days after the /// disposal (earliest first), then with the Section 104 pool at average cost. /// /// Same-day matching is resolved for every day before any 30-day matching, /// and 30-day matching for every disposal (earliest disposal first) before /// the pool is walked, because a purchase claimed by an earlier rule never /// enters the pool. /// /// # Panics /// Panics on transactions out of date order, a bad kind, quantity or amount, /// an amount not in GBP, a disposal before 6 April 2008, or a sale of more /// shares than are held. pub fn section104_pool(transactions: &[ShareTransaction]) -> Section104Result { let mut buys: Vec = Vec::new(); let mut sells: Vec = Vec::new(); let mut previous: Option = None; for tx in transactions { check(tx, previous.as_deref()); previous = Some(tx.date.clone()); let group = if tx.kind == "buy" { &mut buys } else { &mut sells }; if group.last().map(|d| d.date != tx.date).unwrap_or(true) { group.push(new_day(&tx.date)); } let last = group.last_mut().unwrap(); last.quantity += tx.quantity; last.amount += tx.amount.minor as i128; last.remaining = last.quantity; last.remaining_amount = last.amount; } // Nobody can sell shares they do not hold at the end of that day. let mut held: i64 = 0; let mut b = 0; for sell in &sells { while b < buys.len() && buys[b].date <= sell.date { held += buys[b].quantity; b += 1; } held -= sell.quantity; if held < 0 { panic!("cannot sell more shares than are held: {} sold on {}", sell.quantity, sell.date); } } let mut matches: Vec> = sells.iter().map(|_| Vec::new()).collect(); // 1. Same day: TCGA 1992 s105(1)(b). for (s, sell) in sells.iter_mut().enumerate() { if let Some(buy) = buys.iter_mut().find(|candidate| candidate.date == sell.date) { let quantity = sell.remaining.min(buy.remaining); if quantity == 0 { continue; } let cost = take(buy, quantity); matches[s].push(Pending { rule: "same-day", acquisition_date: Some(buy.date.clone()), quantity, cost, }); sell.remaining -= quantity; } } // 2. The next 30 days, earliest acquisition first: TCGA 1992 s106A(5). for (s, sell) in sells.iter_mut().enumerate() { for buy in buys.iter_mut() { if sell.remaining == 0 { break; } let after = buy.day - sell.day; if after < 1 || after > 30 || buy.remaining == 0 { continue; } let quantity = sell.remaining.min(buy.remaining); let cost = take(buy, quantity); matches[s].push(Pending { rule: "bed-and-breakfast", acquisition_date: Some(buy.date.clone()), quantity, cost, }); sell.remaining -= quantity; } } // 3. Everything else through the Section 104 pool, in date order. let mut pool = new_day(RULES_START); let mut disposals: Vec = Vec::new(); let mut next = 0; for (s, sell) in sells.iter().enumerate() { while next < buys.len() && buys[next].date <= sell.date { pool.remaining += buys[next].remaining; pool.remaining_amount += buys[next].remaining_amount; next += 1; } if sell.remaining > 0 { if sell.remaining > pool.remaining { panic!("cannot sell more shares than are held: {} sold on {}", sell.quantity, sell.date); } let cost = take(&mut pool, sell.remaining); matches[s].push(Pending { rule: "section-104", acquisition_date: None, quantity: sell.remaining, cost, }); } disposals.push(dispose(sell, &matches[s])); } while next < buys.len() { pool.remaining += buys[next].remaining; pool.remaining_amount += buys[next].remaining_amount; next += 1; } Section104Result { disposals, pool_quantity: pool.remaining, pool_cost: gbp(pool.remaining_amount), } } /// Apportion the day's proceeds across its matches by shares, the last match /// taking what is left. fn dispose(sell: &DayTotal, pending: &[Pending]) -> ShareDisposal { let mut proceeds_left = sell.amount; let mut shares_left = sell.quantity; let mut allowable: i128 = 0; let mut out: Vec = Vec::new(); for m in pending { let proceeds = if m.quantity == shares_left { proceeds_left } else { share(proceeds_left, m.quantity as i128, shares_left as i128) }; proceeds_left -= proceeds; shares_left -= m.quantity; allowable += m.cost; out.push(ShareMatch { rule: m.rule.to_string(), acquisition_date: m.acquisition_date.clone(), quantity: m.quantity, proceeds: gbp(proceeds), cost: gbp(m.cost), gain: gbp(proceeds - m.cost), }); } ShareDisposal { date: sell.date.clone(), quantity: sell.quantity, proceeds: gbp(sell.amount), allowable_cost: gbp(allowable), gain: gbp(sell.amount - allowable), matches: out, } } pub fn share_match_to_value(m: &ShareMatch) -> Value { Value::obj(vec![ ("rule", Value::str(&m.rule)), ( "acquisitionDate", match &m.acquisition_date { Some(date) => Value::str(date), None => Value::Null, }, ), ("quantity", Value::Int(m.quantity)), ("proceeds", money_to_value(&m.proceeds)), ("cost", money_to_value(&m.cost)), ("gain", money_to_value(&m.gain)), ]) } pub fn share_disposal_to_value(d: &ShareDisposal) -> Value { Value::obj(vec![ ("date", Value::str(&d.date)), ("quantity", Value::Int(d.quantity)), ("proceeds", money_to_value(&d.proceeds)), ("allowableCost", money_to_value(&d.allowable_cost)), ("gain", money_to_value(&d.gain)), ("matches", Value::Arr(d.matches.iter().map(share_match_to_value).collect())), ]) } pub fn section104_result_to_value(r: &Section104Result) -> Value { Value::obj(vec![ ("disposals", Value::Arr(r.disposals.iter().map(share_disposal_to_value).collect())), ("poolQuantity", Value::Int(r.pool_quantity)), ("poolCost", money_to_value(&r.pool_cost)), ]) } pub fn share_transaction_from_value(v: &Value) -> ShareTransaction { // Refuse what the typed record cannot hold, with the wording TypeScript // and Python use, rather than let the conversion quietly truncate it. if let Value::Float(f) = v.get("quantity") { panic!( "quantity must be a whole number greater than zero, received {} on {}", f, v.get("date").as_str() ); } if let Value::Float(f) = v.get("amount").get("minor") { panic!( "amount must be a whole number of pence, not negative, received {} on {}", f, v.get("date").as_str() ); } ShareTransaction { date: v.get("date").as_str().to_string(), kind: v.get("kind").as_str().to_string(), quantity: v.get("quantity").as_i64(), amount: money_from_value(v.get("amount")), } } pub fn fune_vector(args: &[Value]) -> Value { let transactions: Vec = args[0].as_arr().iter().map(share_transaction_from_value).collect(); section104_result_to_value(§ion104_pool(&transactions)) }