repay/
lib.rs

1//! CLI documentation
2//! =================
3//!
4//! Example usage:
5//!
6//! ```bash
7//! $ cargo install repay
8//!
9//! $ repay <<HERE
10//! a 150
11//! b 300
12//! c 100 c a
13//! HERE
14//! c owes b 100.00
15//! a owes b 50.00
16//! ```
17//!
18//! Library documentation
19//! =====================
20//!
21//! Example usage:
22//!
23//! ```
24//! extern crate repay;
25//!
26//! let input =
27//!     "a 150
28//!     b 300
29//!     c 100 c a";
30//! let result = repay::run(input.lines().map(str::to_owned));
31//! assert_eq!(2, result.len());
32//! assert_eq!("c owes b 100.00", format!("{}", result[0]));
33//! assert_eq!("a owes b 50.00", format!("{}", result[1]));
34//! ```
35
36#[cfg(test)]
37#[macro_use]
38extern crate pretty_assertions;
39
40mod money;
41
42use std::collections::BTreeMap;
43use std::collections::BTreeSet;
44use std::fmt;
45
46use crate::money::Money;
47
48#[derive(Debug, PartialEq, Eq)]
49struct Record {
50    creditor: String,
51    amount: Money,
52    debtors: BTreeSet<String>,
53}
54
55struct Records {
56    records: Vec<Record>,
57}
58
59#[derive(Debug, Eq, PartialEq)]
60pub struct Debt {
61    pub debtor: String,
62    pub amount: Money,
63    pub creditor: String,
64}
65
66pub fn run<T: IntoIterator<Item = String>>(lines: T) -> Vec<Debt> {
67    Records::new(lines).calc_debt_resolution()
68}
69
70fn normalize_input<T: IntoIterator<Item = String>>(lines: T) -> Vec<Record> {
71    let mut records = Vec::<Record>::default();
72    let mut lines = lines.into_iter().filter(|s| !s.is_empty());
73    while let Some(line) = lines.next() {
74        let mut tokens = line.split_whitespace().take_while(|&token| token != "#");
75        let Some(creditor) = tokens.next() else {
76            continue;
77        };
78        let Some(amount) = tokens.next() else {
79            continue;
80        };
81        let r = Record {
82            creditor: creditor.into(),
83            amount: money::parse(amount),
84            debtors: tokens.map(|s| s.to_owned()).collect(),
85        };
86        records.push(r);
87    }
88    let participants: BTreeSet<String> = records.iter().fold(BTreeSet::new(), |mut memo, elem| {
89        memo.insert(elem.creditor.to_owned());
90        memo.extend(elem.debtors.clone());
91        memo
92    });
93    let creditors: BTreeSet<String> = records.iter().map(|rec| rec.creditor.clone()).collect();
94    // We need records also for persons with no expenses so they become part of
95    // the calculation further down.
96    let debtors_with_no_credit = participants.difference(&creditors).map(|debtor| Record {
97        creditor: debtor.clone(),
98        amount: money::zero(),
99        debtors: BTreeSet::new(),
100    });
101    records
102        .into_iter()
103        .chain(debtors_with_no_credit)
104        .map(|record| {
105            let debtors: BTreeSet<String> = if record.debtors.is_empty() {
106                &participants
107            } else {
108                &record.debtors
109            }
110            .iter()
111            .cloned()
112            .collect();
113            Record {
114                debtors: debtors,
115                ..record
116            }
117        })
118        .collect()
119}
120
121fn sum<'a, I>(amounts: I) -> Money
122where
123    I: IntoIterator<Item = &'a Money>,
124{
125    amounts
126        .into_iter()
127        .fold(money::zero(), |memo, elem| memo + elem)
128}
129
130impl Records {
131    fn new<T: IntoIterator<Item = String>>(records_init: T) -> Records {
132        Records {
133            records: normalize_input(records_init),
134        }
135    }
136
137    fn calc_expenses_per_person2<R: AsRef<Record>>(records: &[R]) -> BTreeMap<String, Money> {
138        records
139            .iter()
140            .filter(|record| record.as_ref().amount != money::zero())
141            .fold(BTreeMap::new(), |mut memo, record| {
142                let record = record.as_ref();
143                {
144                    let amount = memo
145                        .entry(record.creditor.clone())
146                        .or_insert_with(money::zero);
147                    *amount = amount.clone() + &record.amount;
148                }
149                memo
150            })
151    }
152
153    fn calc_expenses_per_person_and_group(
154        &self,
155    ) -> BTreeMap<BTreeSet<String>, BTreeMap<String, Money>> {
156        self.records_by_group()
157            .iter()
158            .map(|(group, records)| (group.clone(), Self::calc_expenses_per_person2(records)))
159            .collect()
160    }
161
162    fn calc_share(records: &[&Record], group_size: usize) -> Money {
163        sum(records.iter().map(|r| &r.amount)) / money::from(group_size)
164    }
165
166    fn calc_share_per_person_and_group(
167        &self,
168    ) -> BTreeMap<BTreeSet<String>, BTreeMap<String, Money>> {
169        self.records_by_group()
170            .iter()
171            .map(|(group, records)| {
172                let share = Self::calc_share(records, group.len());
173                let share_per_person = records
174                    .iter()
175                    .flat_map(|rec| {
176                        rec.debtors
177                            .iter()
178                            .map(|debtor| (debtor.clone(), share.clone()))
179                    })
180                    .collect();
181                (group.clone(), share_per_person)
182            })
183            .collect()
184    }
185
186    fn records_by_group(&self) -> BTreeMap<BTreeSet<String>, Vec<&Record>> {
187        self.records.iter().fold(BTreeMap::new(), |mut memo, elem| {
188            memo.entry(elem.debtors.clone())
189                .or_insert_with(|| vec![])
190                .push(elem);
191            memo
192        })
193    }
194
195    fn expenses_creditor_not_part_of_group(&self) -> BTreeMap<String, Money> {
196        self.records
197            .iter()
198            .filter(|&rec| !rec.debtors.contains(&rec.creditor))
199            .fold(Default::default(), |mut memo, record| {
200                let entry = memo.entry(record.creditor.clone()).or_insert(money::zero());
201                *entry = &*entry + &record.amount * money::from(-1);
202                memo
203            })
204    }
205
206    fn calc_debt_per_person(&self) -> BTreeMap<String, Money> {
207        let share_per_person_and_group = self.calc_share_per_person_and_group();
208        let expenses_per_person_group = self.calc_expenses_per_person_and_group();
209        share_per_person_and_group
210            .into_iter()
211            .map(|(group, share_per_person)| {
212                let expenses_per_person = &expenses_per_person_group[&group];
213                share_per_person
214                    .into_iter()
215                    .map(|(person, share)| {
216                        let debt =
217                            share - expenses_per_person.get(&person).unwrap_or(&money::zero());
218                        (person.clone(), debt)
219                    })
220                    .collect::<BTreeMap<String, Money>>()
221            })
222            .chain(vec![self.expenses_creditor_not_part_of_group()].into_iter())
223            .fold(BTreeMap::new(), |mut memo, debt_per_person| {
224                for (person, debt) in debt_per_person {
225                    let debt_acc = memo.entry(person).or_insert_with(money::zero);
226                    *debt_acc = debt_acc.clone() + debt;
227                }
228                memo
229            })
230    }
231
232    fn calc_debt_resolution(&self) -> Vec<Debt> {
233        let debt_per_person = self.calc_debt_per_person();
234        let debt_per_person_by_debt = {
235            let mut d: Vec<(String, Money)> = debt_per_person
236                .iter()
237                .map(|(k, v)| (k.clone(), v.clone()))
238                .filter(|&(_, ref v)| v > &money::zero())
239                .collect();
240            d.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
241            d
242        };
243        let mut expense_per_person = {
244            let mut d: Vec<(String, Money)> = debt_per_person
245                .iter()
246                .map(|(k, v)| (k.clone(), v.clone()))
247                .filter(|&(_, ref v)| v < &money::zero())
248                .collect();
249            d.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap());
250            d
251        };
252        debt_per_person_by_debt
253            .into_iter()
254            .flat_map(|(person, debt)| {
255                self.resolve_for_person(&person, &debt, &mut expense_per_person)
256            })
257            .collect()
258    }
259
260    fn resolve_for_person(
261        &self,
262        person: &str,
263        debt: &Money,
264        expense_per_person: &mut [(String, Money)],
265    ) -> Vec<Debt> {
266        let mut debt = debt.clone();
267        let mut payouts = vec![];
268        let zero: Money = money::zero();
269        let mut last_debt = money::zero();
270        while &debt != &last_debt && &debt != &zero {
271            last_debt = debt.clone();
272            let pos_opt = expense_per_person
273                .iter()
274                .position(|x| x.0 != person && &x.1 < &zero);
275            if let Some(pos) = pos_opt {
276                let (ref creditor, ref mut expense) = expense_per_person[pos];
277                let remainder = &debt + expense.clone();
278                if &remainder >= &zero {
279                    debt = remainder;
280                    payouts.push((creditor.clone(), expense.clone() * money::from(-1)));
281                    *expense = money::zero();
282                } else if &remainder < &zero {
283                    *expense = remainder;
284                    payouts.push((creditor.clone(), debt.clone()));
285                    debt = money::zero();
286                }
287            } else {
288                break;
289            }
290        }
291        payouts
292            .into_iter()
293            .map(|(creditor, amount)| Debt {
294                debtor: person.to_owned(),
295                amount: amount,
296                creditor: creditor,
297            })
298            .collect()
299    }
300}
301
302impl fmt::Display for Record {
303    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
304        let debtors: Vec<_> = self.debtors.iter().map(|x| x.as_str()).collect();
305        write!(
306            f,
307            "Creditor: {}, Amount: {}, Debtors {}",
308            self.creditor,
309            self.amount,
310            debtors.as_slice().join(", ")
311        )
312    }
313}
314
315impl fmt::Display for Debt {
316    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
317        let amount = money::to_float(&self.amount);
318        write!(f, "{} owes {} {:.2}", self.debtor, self.creditor, amount)
319    }
320}
321
322impl AsRef<Record> for Record {
323    fn as_ref(&self) -> &Record {
324        self
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use std::collections::BTreeSet;
331    use std::fmt;
332    use std::str::FromStr;
333
334    use super::money;
335    use super::normalize_input;
336    use super::run;
337    use super::Debt;
338    use super::Record;
339    use super::Records;
340
341    #[test]
342    fn test_str2btree_set() {
343        let actual = str2btree_set("a b c");
344        let mut expected = BTreeSet::new();
345        expected.insert("a".into());
346        expected.insert("b".into());
347        expected.insert("c".into());
348        assert_eq!(actual, expected);
349    }
350
351    #[test]
352    fn test_normalize_input() {
353        let actual = normalize_input(get_input());
354        let expected = vec![
355            Record::new("a", "1200", "a b c"),
356            Record::new("b", "600", "a b c"),
357            Record::new("b", "200", "b c"),
358            Record::new("c", "100", "a c"),
359        ];
360        assert_eq!(actual, expected);
361    }
362
363    #[test]
364    fn test_normalize_input_no_debtors() {
365        let actual = normalize_input("a 100\nb 100\n\n".lines().map(|x| x.to_owned()));
366        let expected = vec![
367            Record::new("a", "100", "a b"),
368            Record::new("b", "100", "a b"),
369        ];
370        assert_eq!(actual, expected);
371    }
372
373    #[test]
374    fn test_calc_share_per_person_and_group() {
375        let records = Records::new(get_input());
376        let actual = records.calc_share_per_person_and_group();
377        assert_eq!(3, actual.len());
378        let expected = vec![
379            (
380                str2btree_set("a b c"),
381                vec![
382                    ("a".into(), parse("600")),
383                    ("b".into(), parse("600")),
384                    ("c".into(), parse("600")),
385                ]
386                .into_iter()
387                .collect(),
388            ),
389            (
390                str2btree_set("a c"),
391                vec![("a".into(), parse("50")), ("c".into(), parse("50"))]
392                    .into_iter()
393                    .collect(),
394            ),
395            (
396                str2btree_set("b c"),
397                vec![("b".into(), parse("100")), ("c".into(), parse("100"))]
398                    .into_iter()
399                    .collect(),
400            ),
401        ]
402        .into_iter()
403        .collect();
404        assert_eq!(actual, expected);
405    }
406
407    #[test]
408    fn test_calc_expenses_per_person_and_group() {
409        let records = Records::new(get_input());
410        let actual = records.calc_expenses_per_person_and_group();
411        let expected = vec![
412            (
413                str2btree_set("a b c"),
414                vec![("a".into(), parse("1200")), ("b".into(), parse("600"))]
415                    .into_iter()
416                    .collect(),
417            ),
418            (
419                str2btree_set("a c"),
420                vec![("c".into(), parse("100"))].into_iter().collect(),
421            ),
422            (
423                str2btree_set("b c"),
424                vec![("b".into(), parse("200"))].into_iter().collect(),
425            ),
426        ]
427        .into_iter()
428        .collect();
429        assert_eq!(actual, expected);
430    }
431
432    #[test]
433    fn test_calc_debt_per_person() {
434        let records = Records::new(get_input());
435        let actual = records.calc_debt_per_person();
436        let expected = vec![
437            ("a".into(), parse("-550")),
438            ("b".into(), parse("-100")),
439            ("c".into(), parse("650")),
440        ]
441        .into_iter()
442        .collect();
443        assert_eq!(actual, expected);
444    }
445
446    #[test]
447    fn test_calc_debt_resolution() {
448        let records = Records::new(get_input());
449        let actual = records.calc_debt_resolution();
450        let expected = vec![
451            Debt {
452                debtor: "c".into(),
453                amount: parse("550"),
454                creditor: "a".into(),
455            },
456            Debt {
457                debtor: "c".into(),
458                amount: parse("100"),
459                creditor: "b".into(),
460            },
461        ];
462        assert_eq!(actual, expected);
463    }
464
465    #[test]
466    fn test_run() {
467        let actual = run(get_input());
468        let expected = vec![
469            Debt {
470                debtor: "c".into(),
471                amount: parse("550"),
472                creditor: "a".into(),
473            },
474            Debt {
475                debtor: "c".into(),
476                amount: parse("100"),
477                creditor: "b".into(),
478            },
479        ];
480        assert_eq!(actual, expected);
481    }
482
483    #[test]
484    fn test_only_one_expenser() {
485        let actual = run(to_input("a 100 b"));
486        let expected = vec![mk_debt("b 100 a")];
487        assert_eq!(actual, expected);
488    }
489
490    #[test]
491    fn test_a_to_b_and_b_to_c() {
492        let actual = run(to_input("a 100 b\nb 50 c"));
493        let expected = vec![mk_debt("b 50 a"), mk_debt("c 50 a")];
494        assert_eq!(actual, expected);
495    }
496
497    #[test]
498    fn test_fractions() {
499        let actual = stringify(run(to_input("a 33.33 b c\nb 12.33 a c")));
500        let expected = stringify(vec![mk_debt("c 22.83 a"), mk_debt("b 4.33 a")]);
501        assert_eq!(expected, actual);
502    }
503
504    #[test]
505    fn test_comment_in_input() {
506        let input = to_input(
507            "
508            d 100 b # ignore this
509            d 200 a
510            # also ignore this
511            d 300 c",
512        );
513        let repay_calc = run(input.clone());
514        let actual = stringify(repay_calc);
515        assert_eq!(
516            actual,
517            ["c owes d 300.00", "a owes d 200.00", "b owes d 100.00"]
518        );
519    }
520
521    #[test]
522    fn test_buggy_case() {
523        let input = to_input(
524            "
525            d 291 b
526            d 286 a
527            d 301 c
528            d 393 d e
529            b 1965 e b d a c",
530        );
531        let mut repay_calc = run(input.clone());
532        let actual_sum: f64 = repay_calc.iter().map(|x| money::to_float(&x.amount)).sum();
533        let expected_sum: f64 = [694.0, 589.5, 587.0, 92.0].iter().sum();
534        assert_eq!(expected_sum, actual_sum);
535
536        let expected = [
537            "c owes b 694.00",
538            "e owes d 589.50",
539            "a owes b 587.00",
540            "a owes d 92.00",
541        ];
542        repay_calc.sort_by_key(|debt| (&debt.amount * money::from(-1), debt.debtor.clone()));
543        let actual = stringify(repay_calc);
544        assert_eq!(expected, &actual[..]);
545    }
546
547    fn to_input(s: &str) -> Vec<String> {
548        s.lines().map(|x| x.to_owned()).collect()
549    }
550
551    fn get_input() -> Vec<String> {
552        to_input(INPUT_1)
553    }
554
555    const INPUT_1: &'static str = "\
556                                   a 1200 a b c\n\
557                                   b 600 a b c\n\
558                                   b 200 b c\n\
559                                   c 100 a c\n\
560                                   ";
561
562    #[test]
563    fn test_input() {
564        assert_eq!(INPUT_1.lines().count(), 4);
565    }
566
567    fn mk_debt(debt: &str) -> Debt {
568        let mut itr = debt.split_whitespace();
569        let d = itr.next().unwrap();
570        let amt = itr.next().unwrap();
571        let c = itr.next().unwrap();
572        Debt {
573            debtor: d.into(),
574            amount: money::parse(amt).reduced(),
575            creditor: c.into(),
576        }
577    }
578
579    fn parse<T>(s: &str) -> T
580    where
581        T: FromStr,
582        T::Err: fmt::Debug,
583    {
584        s.parse().unwrap()
585    }
586
587    impl Record {
588        fn new(creditor: &str, amount: &str, debtors_init: &str) -> Record {
589            Record {
590                creditor: creditor.into(),
591                amount: parse(amount),
592                debtors: str2btree_set(debtors_init),
593            }
594        }
595    }
596
597    fn str2btree_set(xs: &str) -> BTreeSet<String> {
598        let mut out = BTreeSet::new();
599        out.extend(xs.split_whitespace().map(|x| x.to_owned()));
600        out
601    }
602
603    fn stringify(xs: Vec<Debt>) -> Vec<String> {
604        xs.into_iter().map(|x| x.to_string()).collect()
605    }
606}