xirr/
lib.rs

1// Copyright 2018 Chandra Sekar S
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![cfg_attr(docsrs, feature(doc_auto_cfg))]
16
17//! # XIRR
18//!
19//! `xirr` implements the XIRR function found in spreadsheet applications like LibreOffice Calc.
20//!
21//! # Example
22//!
23//! ```
24//! use jiff::civil::Date;
25//! use xirr::*;
26//!
27//! let payments = vec![
28//!     Payment { date: "2015-06-11".parse().unwrap(), amount: -1000.0 },
29//!     Payment { date: "2015-07-21".parse().unwrap(), amount: -9000.0 },
30//!     Payment { date: "2018-06-10".parse().unwrap(), amount: 20000.0 },
31//!     Payment { date: "2015-10-17".parse().unwrap(), amount: -3000.0 },
32//! ];
33//!
34//!  assert_eq!(0.1635371584432641, compute::<Date>(&payments).unwrap());
35//! ```
36//!
37//! If you use chrono, enable the `chrono` feature and replace
38//! [`jiff::civil::Date`](::jiff::civil::Date) with [`chrono::NaiveDate`](::chrono::NaiveDate).
39
40use std::error::Error;
41use std::fmt;
42use std::fmt::{Display, Formatter};
43
44#[cfg(feature = "chrono")]
45mod chrono;
46#[cfg(feature = "jiff")]
47mod jiff;
48
49const MAX_ERROR: f64 = 1e-10;
50const MAX_COMPUTE_WITH_GUESS_ITERATIONS: u32 = 50;
51
52/// A payment made or received on a particular date.
53///
54/// `amount` must be negative for payment made and positive for payment received.
55#[derive(Copy, Clone)]
56pub struct Payment<T: PaymentDate> {
57    pub amount: f64,
58    pub date: T,
59}
60
61/// Calculates the internal rate of return of a series of irregular payments.
62///
63/// It tries to identify the rate of return using Newton's method with an initial guess of 0.1.
64/// If that does not provide a solution, it attempts with guesses from -0.99 to 0.99
65/// in increments of 0.01 and returns NaN if that fails too.
66///
67/// # Errors
68///
69/// This function will return [`InvalidPaymentsError`](struct.InvalidPaymentsError.html)
70/// if both positive and negative payments are not provided.
71pub fn compute<T: PaymentDate>(payments: &Vec<Payment<T>>) -> Result<f64, InvalidPaymentsError> {
72    validate(payments)?;
73
74    let mut sorted: Vec<_> = payments.iter().collect();
75    sorted.sort_by_key(|p| &p.date);
76
77    let mut rate = compute_with_guess(&sorted, 0.1);
78    let mut guess = -0.99;
79
80    while guess < 1.0 && (rate.is_nan() || rate.is_infinite()) {
81        rate = compute_with_guess(&sorted, guess);
82        guess += 0.01;
83    }
84
85    Ok(rate)
86}
87
88/// An error returned when the payments provided to [`compute`](fn.compute.html) do not contain
89/// both negative and positive payments.
90#[derive(Debug)]
91pub struct InvalidPaymentsError;
92
93impl Display for InvalidPaymentsError {
94    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
95        "negative and positive payments are required".fmt(f)
96    }
97}
98
99impl Error for InvalidPaymentsError {}
100
101fn compute_with_guess<T: PaymentDate>(payments: &Vec<&Payment<T>>, guess: f64) -> f64 {
102    let mut r = guess;
103    let mut e = 1.0;
104
105    for _ in 0..MAX_COMPUTE_WITH_GUESS_ITERATIONS {
106        if e <= MAX_ERROR {
107            return r;
108        }
109
110        let r1 = r - xirr(payments, r) / dxirr(payments, r);
111        e = (r1 - r).abs();
112        r = r1;
113    }
114
115    f64::NAN
116}
117
118fn xirr<T: PaymentDate>(payments: &Vec<&Payment<T>>, rate: f64) -> f64 {
119    let mut result = 0.0;
120    for p in payments {
121        let exp = get_exp(p, payments[0]);
122        result += p.amount / (1.0 + rate).powf(exp)
123    }
124    result
125}
126
127fn dxirr<T: PaymentDate>(payments: &Vec<&Payment<T>>, rate: f64) -> f64 {
128    let mut result = 0.0;
129    for p in payments {
130        let exp = get_exp(p, payments[0]);
131        result -= p.amount * exp / (1.0 + rate).powf(exp + 1.0)
132    }
133    result
134}
135
136fn validate<T: PaymentDate>(payments: &Vec<Payment<T>>) -> Result<(), InvalidPaymentsError> {
137    let positive = payments.iter().any(|p| p.amount > 0.0);
138    let negative = payments.iter().any(|p| p.amount < 0.0);
139
140    if positive && negative {
141        Ok(())
142    } else {
143        Err(InvalidPaymentsError)
144    }
145}
146
147fn get_exp<T: PaymentDate>(p: &Payment<T>, p0: &Payment<T>) -> f64 {
148    p.date.days_since(p0.date) as f64 / 365.0
149}
150
151/// A trait representing the date on which a payment was made.
152///
153/// This trait is implemented for [`jiff::civil::Date`](::jiff::civil::Date)
154/// and [`chrono::NaiveDate`](::chrono::NaiveDate).
155pub trait PaymentDate: Ord + Sized + Copy {
156    /// Calculates the number days from the `other` date to this date.
157    fn days_since(self, other: Self) -> i32;
158}