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}