Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::epoch_day_from_iso; ← from dates.add-days ^1.0.0 · built alongside by fune
use super::money_amount::{money_from_value, money_to_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
/// 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<String>,
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<DayTotal> = Vec::new();
let mut sells: Vec<DayTotal> = Vec::new();
let mut previous: Option<String> = 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<Vec<Pending>> = 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<ShareDisposal> = 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<ShareMatch> = 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<ShareTransaction> = args[0].as_arr().iter().map(share_transaction_from_value).collect();
section104_result_to_value(§ion104_pool(&transactions))
}