## DEV Community is a community of 616,766 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

# Daily Challenge #4 - Checkbook Balancing

dev.to staff on July 01, 2019

Good morning, everyone. Don’t say I didn’t warn you, we’re moving from letters to numbers with this challenge. Today’s challenge comes from use...
I'm Luis! \^-^/

Here's my attempt:

``````const checkbook = `
1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;
`.trim()

const round = value => Math.round(value * 100) / 100

const cleanedCheckBook = checkbook.replace(/[^A-Za-z0-9\s\.]/gmi, '')
const lines = cleanedCheckBook.split("\n")
const originalBalance = parseFloat(lines.shift())

let totalExpenses = 0
const expenses = []

const linesProcessed = lines.map(line => {
const [number, category, expense] = line.split(' ')
const expenseFloat = parseFloat(expense)
totalExpenses += expenseFloat
expenses.push(expenseFloat)

const currentBalance = originalBalance - totalExpenses

return `\${number} \${category} \${round(expense)} Balance \${round(currentBalance)}`
}).join("\n")

const averageSpent = expenses.reduce((total, sum) => total + sum) / expenses.length

console.log(`
Original_Balance: \${round(originalBalance)}
\${linesProcessed}
Total Expenses: \${round(totalExpenses)}
Average spent: \${round(averageSpent)}
`)
``````

``````const checkbook = `
1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;
`.trim() // this will remove spaces, tabs, and breaklines \n at the beginning and end

// Math.round doesn't round to X number of digits so I'm using this hacky method
const round = value => Math.round(value * 100) / 100
// skip all non A-Za-z0-9\s\. caracters, globally, multiline, and case insensitive
const cleanedCheckBook = checkbook.replace(/[^A-Za-z0-9\s\.]/gmi, '')
// make an array where every element is a line
const lines = cleanedCheckBook.split("\n")
// extract the first element and save the value in float
const originalBalance = parseFloat(lines.shift())

let totalExpenses = 0
const expenses = [] // for the average

// go thru each line and change it
const linesProcessed = lines.map(line => {
const [number, category, expense] = line.split(' ')
const expenseFloat = parseFloat(expense)

totalExpenses += expenseFloat
expenses.push(expenseFloat)

const currentBalance = originalBalance - totalExpenses

return `\${number} \${category} \${round(expense)} Balance \${round(currentBalance)}`
}).join("\n")

const averageSpent = expenses.reduce((total, sum) => total + sum) / expenses.length

console.log(`
Original_Balance: \${round(originalBalance)}
\${linesProcessed}
Total Expenses: \${round(totalExpenses)}
Average spent: \${round(averageSpent)}
`)
``````
Donald Feury • Edited

Here is my attempt, did it in Go: Github

This executable boils down to:

``````package main

import (
"fmt"

"github.com/Dak425/dev-to-challenge-4-go/pkg/checkbook/memory"
)

func main() {
raw := `1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;`

cb := memory.NewInMemoryCheckBook(raw)

fmt.Print(cb.FullReport())
}
``````

Output:

``````Starting Balance: 1233.00
[1] -> Check Number: 125, Category: Hardware, Amount: 24.80, Remaining Balance: 1208.20
[2] -> Check Number: 123, Category: Flowers, Amount: 93.50, Remaining Balance: 1114.70
[3] -> Check Number: 127, Category: Meat, Amount: 120.90, Remaining Balance: 993.80
[4] -> Check Number: 120, Category: Picture, Amount: 34.00, Remaining Balance: 959.80
[5] -> Check Number: 124, Category: Gasoline, Amount: 11.00, Remaining Balance: 948.80
[6] -> Check Number: 123, Category: Photos, Amount: 71.40, Remaining Balance: 877.40
[7] -> Check Number: 122, Category: Picture, Amount: 93.50, Remaining Balance: 783.90
[8] -> Check Number: 132, Category: Tires, Amount: 19.00, Remaining Balance: 764.90
[9] -> Check Number: 129, Category: Stamps, Amount: 13.60, Remaining Balance: 751.30
[10] -> Check Number: 129, Category: Fruits, Amount: 17.60, Remaining Balance: 733.70
[11] -> Check Number: 129, Category: Market, Amount: 128.00, Remaining Balance: 605.70
[12] -> Check Number: 121, Category: Gasoline, Amount: 13.60, Remaining Balance: 592.10
Total Costs: 640.90
Average Cost: 53.41
``````

Also, wooo first post on here 🎉

Kerri Shotts

Here's my take (JavaScript). A few notes:

• I sort by line #, and then by category
• The first line might actually be blank, so I trim the input
• Categories are assumed to be single words (no spaces allowed)
• Output includes digit grouping by current locale
• Full code (incl some basic tests): gist.github.com/kerrishotts/461a90...
``````
const sanitize = str => str.replace(/[^0-9A-Za-z\.\s]/g, "");

const notBlank = str => str !== "";

const extract = str => {
const [ line, category, expense ] = str.split(/\s+/);
return { line: Number(line), category, expense: Number(expense) };
};

const byLineAndCategory = (a, b) => a.line < b.line
? -1 : a.line > b.line
? 1 : a.category < b.category
? -1 : a.category > b.category
? 1 : 0;

const balanceReducer = (
{openingBalance, totalExpenses, entries},
{line, category, expense}
) => {
const newTotal = totalExpenses + expense;
const newBalance = openingBalance - newTotal;
return {
openingBalance,
totalExpenses: newTotal,
averageExpense: newTotal / (entries.length + 1),
entries: [ ...entries, {line, category, expense, balance: newBalance }]
}
};

const round2 = n => (Math.round(n * 100) / 100)
.toLocaleString(undefined, {
style: "decimal",
minimumFractionDigits: 2,
useGrouping: true
});

const balanceCheckbook = (checkbook) => {
const [openingBalanceStr, ...entries] =
sanitize(checkbook)
.trim()
.split("\n")
.filter(notBlank);

const openingBalance = Number(openingBalanceStr);

const initialState = {
openingBalance,
entries: [],
averageExpense: 0,
totalExpenses: 0
};

const report =
entries
.map(extract)
.sort(byLineAndCategory)
.reduce( balanceReducer, initialState );

return `
Original Balance: \${round2(report.openingBalance)}
\${report.entries.map(({line, category, expense, balance}) =>
`\${line} \${category} \${round2(expense)} Balance \${round2(balance)}`
).join("\n")}
Total Expenses: \${round2(report.totalExpenses)}
Average Expense: \${round2(report.averageExpense)}
`.trim();
};

``````
I'm Luis! \^-^/

I think is the most scalable solution since you first move all the data to a manipulable format, deal with it and then output it.

Ceban Dumitru • Edited

BASH

``````#!/bin/bash
input='challenge.txt';
declare -a checkbook;
total=0;
for line in \$(cat \${input}|tr -cd [' ','0-9','.','A-Z','a-z','\n']);do
checkbook+=("\${line}");
done;

echo "Original_Balance: "\${checkbook[0]};
for (( i = 1; i < \${#checkbook[@]}; i+=3 )); do
echo \${checkbook[@]:\${i}:3};
total=`echo "scale=2;\${total} + \${checkbook[\${i}+2]}" |bc`;
done;
average=`echo "scale=2;\${total}/((\${#checkbook[@]}-1)/3)"|bc`;

echo "Total expense: " \${total};
echo "Average expense: " \${average};
``````

Alvaro Montoro • Edited

JavaScript

This is going to be one of those "don't do this at home" types of code (or maybe "do it at home but not at work"). I tried to do it as a single chain of commands, assuming that the string is going to be valid. It can be further cleaned and reduced, I'll try later.

Here is the code commented step-by-step:

``````const generateReport = checkbook => {
let current = 0;
// use regular expressions to remove unwanted characters
return checkbook.replace(/[^0-9a-z\. \n]/gi, "")
// separate the string into an array splitting by new line
.split("\n")
// update each value to include the total at the end
.map((val, index) => {
current = index === 0 ? val : (current - val.split(" ")[2]).toFixed(2);
return index === 0 ? "Original Balance: " + val : val + ` \${current}`;
})
// convert array into string again
.join("\n")
// concatenate the total and average
.concat(`\nTotal expense: \${(checkbook.split("\n")[0] - current).toFixed(2)}`)
.concat(`\nAverage expense: \${((checkbook.split("\n")[0] - current)/(checkbook.split("\n").length-1) || 0).toFixed(2)}`);
}
``````

You can see it working on this CodePen.

Alvaro Montoro

And as an extra, here is a version in which the checks are also sorted:

``````const generateReport = checkbook => {
let current = 0;
return checkbook.replace(/[^0-9a-z\. \n]/gi, "")
.split("\n")
.sort((a,b) => {
const arrA = a.split(" ");
const arrB = b.split(" ");
if (arrA.length > arrB.length) {
return 1;
} else if (arrB.length > arrA.length) {
return -1;
} else {
return parseInt(arrA[0]) > parseInt(arrB[0]) ? 1 : -1;
}
})
.map((val, index) => {
current = index === 0 ? val : (current - val.split(" ")[2]).toFixed(2);
return index === 0 ? "Original Balance: " + val : val + ` \${current}`;
})
.join("\n")
.concat(`\nTotal expense: \${(checkbook.split("\n")[0] - current).toFixed(2)}`)
.concat(`\nAverage expense: \${((checkbook.split("\n")[0] - current)/(checkbook.split("\n").length-1) || 0).toFixed(2)}`);
}
``````
Siddharth Venkatesan

Ruby

``````class Checkbook

def initialize
@entries = []
@balance = 0.0
@orig_bal = 0.0
end

def total_expense
@entries.map {|e| e.check_amount}.sum
end

def average_expense
total_expense / @entries.size
end

@balance = @orig_bal = content[0].to_f
(1..content.size-1).each do |n|
line = content[n]
@entries << CheckEntry.new(line)
end
calculate_balance!
end

def calculate_balance!
@entries.sort!
@entries.each do |entry|
#puts "#{@balance} : #{entry.check_amount}"
@balance -= entry.check_amount
entry.running_balance = @balance
end
end

def to_s
disp = []
disp << "%.2f" % @orig_bal
disp << @entries.map {|e| e.to_s}
disp << "Total Expenses = #{"%.2f" % total_expense}"
disp << "Average Expenses = #{"%.2f" % average_expense}"

disp.join("\n")
end
end

class CheckEntry

attr_accessor :check_number, :category, :check_amount, :running_balance

def initialize(line)
@check_number = 0
@category = ""
@check_amount = 0.0
@running_balance = 0.0
end

line = sanitize(line)
matches = /^(\d+)\s+(.*?)\s(.*)\$/.match(line)
@check_number = matches[1].to_i
@category = matches[2]
@check_amount = matches[3].to_f
end

def sanitize(line)
line.gsub(/([^\d\w\s\.])/, '')
end

def to_s
"#{@check_number} #{@category} #{"%.2f" % @check_amount} #{"%.2f" % @running_balance}"
end

def <=>(other)
@check_number <=> other.check_number
end
end

puts Checkbook.new
``````

Output :

``````1233.00
120 Picture 34.00 1199.00
121 Gasoline 13.60 1185.40
122 Picture 93.50 1091.90
123 Flowers 93.50 998.40
123 Photos 71.40 927.00
124 Gasoline 11.00 916.00
125 Hardware 24.80 891.20
127 Meat 120.90 770.30
129 Stamps 13.60 756.70
129 Fruits 17.60 739.10
129 Market 128.00 611.10
132 Tires 19.00 592.10
Total Expenses = 640.90
Average Expenses = 53.41
``````
Jaime López

Here my contribution in javascript:

``````const input = `1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;`;

// clean the input
let balanceInputs = input
.split('\n')
.map(x => x.split(' ')
.filter(x => x.trim() !== ''));

// process input to create an object with all information
function processData(balance, ...inputs) {
const data = {
original : parseFloat(balance[0]),
totalExpense: 0.0,
averageExpense: 0.0
};

const orders = inputs.map(i => {
const order = {
id: parseInt(i[0]),
concept: i[1].match(/[0-9a-zA-Z.\s]/g).reduce((acc, current) => acc.concat(current)),
value: parseFloat(i[2]),
balance: data.original - data.totalExpense - parseFloat(i[2])
};
data.totalExpense += order.value;
return order;
});

data.averageExpense = data.totalExpense / orders.length;

return {
data: data,
orders: orders
}
}

// shows data in the console as a report
function createReport(report) {
console.log('Original_Balance: ' + report.data.original.toFixed(fixed));
report.orders.forEach(function(order) {
console.log(order.id + ' ' + order.concept + ' ' + order.value.toFixed(fixed) + ' Balance ' + order.balance.toFixed(fixed));
});
console.log('Total expense ' + report.data.totalExpense.toFixed(fixed));
console.log('Average expense ' + report.data.averageExpense.toFixed(fixed));
}

const fixed = 2; // set the number of decimal places
const data = processData(...balanceInputs); // process data
createReport(data); // shows the report
``````
jmc • Edited

I'm impressed by how short many of the solutions are.

Clojure:

``````(ns checkbook
(:require [clojure.string :refer [join split]]))

;; split line into tokenized "entry"
(defn tokens [line]

;; append running balance onto entries
(defn with-running-balance [[start & entries]]
(reduce
(fn [acc entry]
(let [prev-bal  (last (last acc))
cur-bal   (- prev-bal (last entry))
new-entry (conj (vec entry) "Balance" cur-bal)]
(conj acc new-entry)))
[["Original_Balance" (last start)]]
entries))

;; output entry as string, with numbers rounded
(defn format-entry [xs]
(case (count xs)
2 (apply format "%s %.2f" xs)
5 (apply format "%s %s %.2f %s %.2f" xs)))

;; append running balance, include total & average, format nums
(defn balance [s]
(let [lines   (split s #"\n")
entries (map tokens lines)
\$\$      (map last (rest entries))]
(join "\n"
(map format-entry
(conj (with-running-balance entries)
["Total expense" (apply + \$\$)]
["Average expense" (/ (apply + \$\$) (count \$\$))])))))
``````
Martin Himmel • Edited

PHP

It wasn't specified, but I sorted the check order. Also noticed that checks 123 and 129 are repeated two and three times, respectively, while 126, 128, and 131 are missing. I'm guessing the duplicate number were supposed to be the missing numbers. 😄

``````\$text = '1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;';

function checkbook_report(string \$str) {
\$data = format_checkbook_string(\$str);
\$balance = floatval(array_shift(\$data));
\$output = 'Original Balance: ' . number_format(\$balance, 2) . PHP_EOL;
\$expenses = [];

sort(\$data);

foreach (\$data as \$index => \$line) {
\$parts = explode(' ', \$line);
// handles multi-word categories (even though they don't exist in this challenge)
foreach (\$parts as \$line_segment) {
if (\$line_segment != end(\$parts)) {
\$output .= "\$line_segment ";
}
}
\$expenses[] = floatval(end(\$parts));
\$balance -= end(\$expenses);
\$output .= number_format(end(\$parts), 2) . ', Balance: ' . number_format(\$balance, 2) . PHP_EOL;
}

\$total_expenses = array_sum(\$expenses);
\$output .= 'Total expenses: ' . number_format(\$total_expenses, 2) . PHP_EOL;
\$output .= 'Average expense: ' . number_format(\$total_expenses / count(\$expenses), 2) . PHP_EOL;
return \$output;
}

function format_checkbook_string(string \$str) {
\$data = explode(PHP_EOL, \$str);
return array_map('filter_line', \$data);
}

function filter_line(string \$line) {
return preg_replace('/[^\w\s.]+/', '', \$line);
}

echo checkbook_report(\$text);
``````
Robin Victorino

Here's my Groovy take on this:

``````class CheckBook {

static final DecimalFormat DF = new DecimalFormat('#.00', DecimalFormatSymbols.getInstance(Locale.US))
static final String LINE_SEPARATOR = '\n'
static final String PROPERTY_SEPARATOR = ' '

List<BookEntry> entries = []
Float initialBalance = 0f

CheckBook(String input) {
parse(input)
entries.sort{it.id}
}

void printOperations() {
println "Original_Balance: \${DF.format(initialBalance)}"
Float currentBalance = initialBalance
List<Float> expenses = []
entries.each { BookEntry b ->
currentBalance -= b.amount
expenses << b.amount
println "\$b Balance \${DF.format(currentBalance)}"
}
Float total = expenses.sum()
println "Total expense \${DF.format(total)}"
println "Average expense \${DF.format(total / expenses.size())}"
}

private void parse(String input) {
List<String> lines = input.split(LINE_SEPARATOR)
initialBalance = Float.valueOf(lines[0])
parseEntries(lines.drop(1))
}

private void parseEntries(List<String> lines) {
lines.each { String line ->
parseEntry(line)
}
}

private void parseEntry(String line) {
List<String> properties = line.split(PROPERTY_SEPARATOR)
.collect{sanitizeInput(it)}
BookEntry newEntry = new BookEntry(
id: properties[0].toInteger(),
name: properties[1],
amount: Float.valueOf(properties[2])
)
entries << newEntry
}

private String sanitizeInput(String toSanitize) {
}

class BookEntry {
Integer id
String name
Float amount

@Override
String toString() {
return "\$id \$name \${DF.format(amount)}"
}
}
}

``````

Call is made like the following:

``````String checkBookInput = """
1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;
""".trim().stripIndent()

CheckBook checkBook = new CheckBook(checkBookInput)
checkBook.printOperations()
``````

Interesting points that are Groovy-related:

• left shift operator to insert new element in a collection
• regex evaluation operator
• the good old Java DecimalFormat to format numbers at print time
• triple quotes multiline Strings
Caleb Rudder

A little late to the party, but trying to work on a few skills. I thought I'd attempt to work on using closures a bit on this one:

``````const myCheckbook = checkbook(testData);
myCheckbook.balanceCheckbook();

function checkbook(input){
let userData = removeClutter(input);
let totalBalance = 0;
let totalExpenses = 0;
let expenseCounter = 0;
let averageExpense = 0;
function removeClutter(data){
let cleanData = data.replace(/([^a-zA-Z0-9\s\.])/g, '');
return cleanData.split('\n');
}

totalExpenses += expense;
totalBalance -= expense;
expenseCounter++;
averageExpense = totalExpenses / expenseCounter;
};

const balanceCheckbook = function(){
for(let i = 0; i<userData.length; i++){
if(i == 0){
totalBalance = userData[0];
console.log('Original_Balance: ' + totalBalance);
}else{
let line = userData[i].split(" ");
console.log(userData[i] + " Balance: " + totalBalance.toFixed(2));
}
}
console.log("Total Expenses: " + totalExpenses.toFixed(2));
console.log("Average expense: " + averageExpense.toFixed(2));
};

return{
userData: userData,
balanceCheckbook: balanceCheckbook
}
}
``````
official_dulin

Go:

``````import (
"bufio"
"fmt"
"log"
"regexp"
"strconv"
"strings"
)

var re = regexp.MustCompile(`(?mi)[^A-Za-z0-9\s\.]`)

func Balance(book string) string {
var r []string
book = re.ReplaceAllString(book, "")

var (
balance, total, average float64
err                     error
)
i := 0
for scanner.Scan() {
text := scanner.Text()
if text == "" {
continue
}
if i == 0 {
balance, err = strconv.ParseFloat(text, 32)
if err != nil {
log.Fatal(err)
}
r = append(r, fmt.Sprintf("Original Balance: %.2f", balance))
} else {
texts := strings.Split(text, " ")
cost, err := strconv.ParseFloat(texts[2], 32)
if err != nil {
log.Fatal(err)
}
text = fmt.Sprintf("%s %s %.2f", texts[0], texts[1], cost)
total += cost
balance = balance - cost
r = append(r, fmt.Sprintf("%s Balance %.2f", text, balance))
}
i++
}
average = total / float64(len(r)-1)
r = append(r, fmt.Sprintf("Total expense  %.2f", total), fmt.Sprintf("Average expense  %.2f", average))
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
return strings.Join(r, "\n")
}
``````
peter279k

Here is my simple solution to parse balanced book string:

``````function balance(\$book) {
\$result = "Original Balance: ";
\$book = explode("\n", \$book);
if (\$book[0] === "") {
\$totalDistance = sprintf("%.2f\n", (float)\$book[1]);
\$index = 2;
} else {
\$totalDistance = sprintf("%.2f\n", (float)\$book[0]);
\$index = 1;
}
\$result .= \$totalDistance;

\$totalExpense = 0.0;
\$currentDistance = (float)\$totalDistance;
\$currentCount = 0;
for(; \$index < count(\$book); \$index++) {
if (\$book[\$index] === "") {
continue;
}
\$currentCount += 1;
\$info = explode(' ', \$book[\$index]);
\$stringFormat = "%s %s %.2f Balance %.2f\n";
preg_match('/(\w+)/', \$info[1], \$matched);
\$info[1] = \$matched[0];

preg_match('/(\d+).(\d+)/', \$info[2], \$matched);
\$info[2] = (float)\$matched[0];
\$currentDistance = \$currentDistance - \$info[2];
\$result .= sprintf(\$stringFormat, \$info[0], \$info[1], (float)\$info[2], (float)\$currentDistance);
\$totalExpense += (float)\$info[2];
}

\$result .= sprintf("Total expense  %.2f\n", \$totalExpense);
\$result .= sprintf("Average expense  %.2f", (string)round(\$totalExpense / \$currentCount, 2));

return \$result;
}
``````
Dimitri Lahaye

JS below :)

``````function balanced(note) {
clean = (s) => s.match(/([a-z0-9\s.])/gi).join('');
let balance, total = 0, average = 0;
return [...note.split('\n').map((n, i) => {
const a = clean(n);
if (!i) {
balance = parseFloat(a).toFixed(2);
return `Original_Balance: \${balance}`;
}
let [num, obj, amount] = a.split(' ');
total += parseFloat(amount);
average = total / (i);
balance -= amount;
return `\${num} \${obj} \${amount} Balance \${balance.toFixed(2)}`;
}),
`Total expense \${total.toFixed(2)}`,
`Average expense \${average.toFixed(2)}`]
.join('\n');
}
``````
Dylan Paulus

Nim

``````import strutils
import sequtils
import re

proc parse(input: string): string =
var separatedInput = splitLines(input)
let originalBalance = parseFloat(separatedInput[0])
var balance = originalBalance

result &= "Original_Balance: " & \$balance & "\n"

separatedInput.delete(0, 0)

for item in separatedInput:
let trimmed = split(item, " ")
let title = replace(trimmed[1], re"\W", "");
let amount = parseFloat(replace(trimmed[2], re"""[-!\$%^&*()_+|~=`{}\[\]:";'<>?,\/]""", ""))

balance = balance - amount
result &= trimmed[0] & " " & title & " " & \$amount & " Balance " & \$balance & "\n"

let totalExpense = originalBalance - balance;

result &= "Total expense " & \$totalExpense & "\n"
result &= "Average expense " & \$(totalExpense / float(len(separatedInput) - 1))  & "\n"

if isMainModule:
const input = """1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;"""

echo parse(input)
``````

Doing a little catch up on challenges. Here's my solution in Go:

Enjoy the naming scheme :evil-smile:

``````type Checkbook struct {
bal      int64
cheques  []Cheque
totalExp int64
avgExp   int64
}

type Cheque struct {
no       int
category string
amount   int64
balance  int64
}

const scaling = 1000

var filter = regexp.MustCompile("[^a-zA-Z0-9 .\n]*")

// New parses a text listing of cheques
func New(chequeList string) (Checkbook, error) {
// remove unwanted runes
chequeList = filter.ReplaceAllString(strings.TrimSpace(chequeList), "")
lines := strings.Split(chequeList, "\n")
bal, ok := (&big.Float{}).SetString(strings.TrimSpace(lines[0]))
if !ok {
return Checkbook{}, errors.Errorf("balance invalid %q", lines[0])
}
b, _ := bal.Mul(bal, (&big.Float{}).SetInt64(scaling)).Int64()
cb := Checkbook{
bal:     b,
cheques: make([]Cheque, len(lines)-1),
}
tot := int64(0)
for i, v := range lines[1:] {
cq, err := parseCheque(v)
if err != nil {
return Checkbook{}, err
}
cq.balance, b = b-cq.amount, b-cq.amount
cb.cheques[i] = cq
tot += cq.amount
}
cb.totalExp = tot
cb.avgExp = tot / int64(len(cb.cheques))
return cb, nil
}

// parseCheque from an entry line in the checkbook.
func parseCheque(entry string) (Cheque, error) {
entry = strings.TrimSpace(entry)
if strings.Count(entry, " ") < 2 {
return Cheque{}, errors.Errorf("cheque line invalid %q", entry)
}
fi, li := strings.Index(entry, " "), strings.LastIndex(entry, " ")
chNo, cat, amt := entry[:fi], strings.TrimSpace(entry[fi:li]), entry[li+1:]
no, err := strconv.Atoi(chNo)
if err != nil {
return Cheque{}, err
}
cq := Cheque{no: no, category: cat}
bal, ok := (&big.Float{}).SetString(amt)
if !ok {
return Cheque{}, errors.Errorf("amount invalid %q", amt)
}
cq.amount, _ = bal.Mul(bal, (&big.Float{}).SetInt64(scaling)).Int64()
return cq, nil
}

// String display of entire checkbook.
func (cb Checkbook) String() string {
b := &strings.Builder{}
fmt.Fprintf(b, "Original Balance: %0.2f\n", float64(cb.bal)/scaling)
for _, v := range cb.cheques {
v.Write(b)
fmt.Fprintln(b)
}
fmt.Fprintf(b, "Total Expenses: %0.2f\n", float64(cb.totalExp)/scaling)
fmt.Fprintf(b, "Average Expenses: %0.2f\n", float64(cb.avgExp)/scaling)
return b.String()
}

// String display of Cheque data.
func (cq Cheque) String() string {
return fmt.Sprintf("No: %d, Category: %s, Amount: %0.2f, Balance: %0.2f",
cq.no, cq.category, float64(cq.amount)/scaling, float64(cq.balance)/scaling)
}

// Write string display to writer.
func (cq Cheque) Write(w io.Writer) (int, error) {
return fmt.Fprintf(w, "No: %d, Category: %s, Amount: %0.2f, Balance: %0.2f",
cq.no, cq.category, float64(cq.amount)/scaling, float64(cq.balance)/scaling)
}
``````
margo1993
``````package utils

import (
"fmt"
"regexp"
"strconv"
"strings"
)

type CheckbookLine struct {
checkNumber int
category string
checkAmount float64
}

func ProcessCheckbook(checkbook string) (string, error) {
lines := strings.Split(checkbook, "\n")

//Ignores all wrongly formatted lines
checkbookLines := parseCheckbookLines(lines[1:])

balance, e := findFloat(lines[0])
if e != nil {
return "", e
}

result := fmt.Sprintf("Original_Balance: %0.2f\n", balance)

totalExpense := 0.0
for _, checkbookLine := range checkbookLines {
totalExpense += checkbookLine.checkAmount

balance -= checkbookLine.checkAmount
result += fmt.Sprintf("%d %s %0.2f Balance %0.2f\n", checkbookLine.checkNumber, checkbookLine.category, checkbookLine.checkAmount, balance)
}

result += fmt.Sprintf("Total expense %0.2f\n", totalExpense)
checkbookLinesCount := len(checkbookLines)
if checkbookLinesCount > 0 {
result += fmt.Sprintf("Average expense %0.2f", totalExpense/float64(checkbookLinesCount))
}

return result, nil;
}

func parseCheckbookLines(lines []string) []CheckbookLine {
var checkbookLines []CheckbookLine
for _, line := range lines {

fields := strings.Fields(line)
if len(fields) < 3 {
continue
}

checkNumber, e := findInteger(fields[0])
if e != nil {
continue
}

category := findClearString(fields[1])
checkAmount, e := findFloat(fields[2])
if e != nil {
continue
}

checkbookLines = append(checkbookLines, CheckbookLine{checkNumber, category, checkAmount})
}
return checkbookLines
}

func findInteger(field string) (int, error) {
r := regexp.MustCompile("[0-9]+")
return strconv.Atoi(r.FindString(field))
}

func findClearString(field string) string {
r := regexp.MustCompile("[A-Za-z]+")
return r.FindString(field)
}

func findFloat(field string) (float64, error) {
r := regexp.MustCompile("[0-9.-]+")
return strconv.ParseFloat(r.FindString(field), 32)
}
``````
kesprit

My solution in Swift :

``````/// Daily Challenge #4 - Checkbook Balancing

let checkbook = """
1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;
"""

func analyseCheckbook(checkbook: String) {
typealias CheckbookEntry = (number: String, description: String, expense: Double)
let roundFormat = "%.2f"
var count: Double = 0
var totalExpense: Double = 0
var checkbookEntries = [CheckbookEntry]()

var cleanCheckbook = checkbook.filter { char -> Bool in
char.isLetter || char.isNumber || char.isWhitespace || char == "."
}.components(separatedBy: .newlines)

let originalBalance = Double(cleanCheckbook.removeFirst()) ?? 0
cleanCheckbook.forEach { checkbook in
let line = checkbook.components(separatedBy: .whitespaces)
if line.count == 3 {
checkbookEntries.append(CheckbookEntry(number: line[0], description: line[1], expense: Double(line[2]) ?? 0))
}
}

print("Original_Balance: \(originalBalance)")

checkbookEntries.forEach { entry in
count += entry.expense
totalExpense += entry.expense
print("\(entry.number) \(entry.description) Balance \(String(format: roundFormat, originalBalance - count))")
}
let averageExpense = (totalExpense / Double(checkbookEntries.count))
print("Total expense \(String(format: roundFormat, totalExpense))")
print("Average expense \(String(format: roundFormat, averageExpense))")
}
``````
Bárbara Perdigão

Java:

``````class Main {

public static void main(String[] args) {

Scanner scan = new Scanner(System.in);
String input = scan.nextLine();
String report = balanceCheckbook(input);

System.out.println(report);
}

static String  balanceCheckbook(String input){

DecimalFormat df = new DecimalFormat("####.00");

ArrayList<String> checkbook = new ArrayList<>(Arrays.asList(input.replaceAll("(\\b\\D\\W)", " ").split("\\n")));

float currentBalance = parseFloat(checkbook.get(0));
float totalExpense = 0;

StringBuilder reportBuilder = new StringBuilder(("Original_Balance: " + df.format(currentBalance) + "\n"));

for (int count = 1; count <checkbook.size(); count++){
String[] values = checkbook.get(count).replaceAll("\\b(\\s{2})", " ").split("\\s");
totalExpense += parseFloat(values[2]);
currentBalance -= parseFloat(values[2]);
checkbook.set(count, values[0] + " " + values[1] + " " + df.format(parseFloat(values[2])));
reportBuilder.append(checkbook.get(count)).append(" Balance ").append(df.format(currentBalance)).append("\n");
}

float averageExpense = totalExpense / (checkbook.size() - 1);

reportBuilder.append("Total expense ").append(df.format(totalExpense)).append("\n")
.append("Average expense ").append(df.format(averageExpense));

return reportBuilder.toString().replace(",", ".");
}
}
``````
cloudyhug

``````import Data.Char

processCheckbook :: String -> String
processCheckbook checkbook =
unlines \$ prettyBalance : (reverse prettyEntries) ++ [prettyTotal, prettyAverage]
where
(originalBalance : entries) = lines \$ filter (\x -> or [isAlphaNum x, isSpace x, x == '.']) checkbook
processEntry (prettyEntries, total, balance) entry =
let expense = read \$ words entry !! 2
newBalance = balance - expense
in ((entry ++ " Balance " ++ show newBalance) : prettyEntries, total + expense, newBalance)
(prettyEntries, totalExpense, _) = foldl processEntry ([], 0, read originalBalance) entries
prettyBalance = "Original_Balance: " ++ originalBalance
prettyTotal = "Total expense " ++ show totalExpense
prettyAverage = "Average expense " ++ show (totalExpense / (fromIntegral \$ length entries))
``````
Ryan Smith

My JavaScript version:

``````/**
* Given a string containing checkbook transactions, format the transactions and calculate the running total
*/
function balanceCheckbook (checkbook) {
let balance = 0
let totalExpense = 0
let balancedCheckbook = ''

// Split the string into an array of transactions based on new lines.
const transactions = checkbook.split('\n')

// Loop over the transactions to construct the checkbook and calculate total expense.
transactions.forEach((transaction) => {
const sanitizedTransaction = sanitizeInput(transaction).split(' ')

// If this transaction only has one column, set it to be the starting balance. Otherwise, process a transaction.
if (sanitizedTransaction.length === 1) {
balance = sanitizedTransaction
balancedCheckbook += `Original_Balance: \${sanitizedTransaction} \n`
} else {
const transactionNumber = sanitizedTransaction[0]
const description = sanitizedTransaction[1]
const cost = sanitizedTransaction[2]
const newBalance = (balance -= cost).toFixed(2)

totalExpense += parseFloat(cost)

// Format the transaction into a string.
balancedCheckbook += `\${transactionNumber} \${description} \${cost} Balance \${newBalance} \n`
}
})

balancedCheckbook += `Total expense \${totalExpense.toFixed(2)} \n`
balancedCheckbook += `Average expense \${(totalExpense / (transactions.length - 1)).toFixed(2)}`

return balancedCheckbook
}

/**
* Remove invalid characters from a transaction.
*/
function sanitizeInput (inputText) {
const removeSpecialCharactersRegex = /[^A-Z0-9\s.]/gi

return inputText.trim().replace(removeSpecialCharactersRegex, '')
}
``````
``````console.log(
balanceCheckbook(
`1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;`
)
)
``````

Output:

``````Original_Balance: 1233.00
125 Hardware 24.8 Balance 1208.20
123 Flowers 93.5 Balance 1114.70
127 Meat 120.90 Balance 993.80
120 Picture 34.00 Balance 959.80
124 Gasoline 11.00 Balance 948.80
123 Photos 71.4 Balance 877.40
122 Picture 93.5 Balance 783.90
132 Tires 19.00 Balance 764.90
129 Stamps 13.6 Balance 751.30
129 Fruits 17.6 Balance 733.70
129 Market 128.00 Balance 605.70
121 Gasoline 13.6 Balance 592.10
Total expense 640.90
Average expense 53.41
``````
``````const round2 = n => Math.round(n*100)/100;
const balance = book => {
const lines = book.replace(/[^\da-z\.\s]/gi, '').split('\n');
const [originalBalance, ...expenses] = lines.map(line => line.match(/\d+\.\d{1,2}/)[0]).map(Number);
const balances = expenses.reduce((acc, expense, index) => [ ...acc, acc[index] - expense ], [originalBalance]).slice(1);
const totalExpense = expenses.reduce((x, y) => x + y, 0);
return [
`Original_Balance: \${originalBalance}`,
...lines.slice(1).map((line, index) => `\${line} Balance \${round2(balances[index])}`),
`Total expense \${round2(totalExpense)}`,
`Average expense \${round2(totalExpense / expenses.length)}`
].join('\n');
}
``````
Kevin Sullivan • Edited
``````// UTILS
const pipe = (...fns) => x => fns.reduce((y, f) => f(y), x);
const map = (mapping: {
(x: any, index: number): string;
(value: any, index: number, array: any[]): {};
}) => (xs: any[]) => xs.map(mapping);
const sum = (values: number[]) =>
values.reduce((total, current) => total + current, 0);
const toLines = (text: string) => text.split("\n");
const sort = (sortFunction: (a: any, b: any) => number) => (xs: any[]) =>
xs.sort(sortFunction);

// DOMAIN
interface Checkbook {
initialBalance: number;
checks: Check[];
}
interface Check {
id: string;
amount: number;
type: string;
}
const total = (checks: Check[]) => sum(checks.map(toAmount));
const average = (checks: Check[]) => total(checks) / checks.length;
const toAmount = ({ amount }) => amount;
const currentBalance = (initialBalance: number, checks: Check[]) =>
initialBalance - total(checks);
const byId = {
asc: (a: Check, b: Check) => Number(a.id) - Number(b.id),
desc: (a: Check, b: Check) => Number(b.id) - Number(a.id),
};

// PARSING
const parseId = (text: string) => text;
const parseType = (text: string) =>
text.replace(/[^a-zA-Z0-9_]/, "").replace(/[!}]/, "");
const parseAmount = (text: string) => parseFloat(text);
const parseCheck = (text: string): Check => {
const [id, type, amount] = text.split(" ");
return {
id: parseId(id),
type: parseType(type),
amount: parseAmount(amount),
};
};
const parse = (text: string) => {
const [initialBalance, ...checks] = toLines(text);
return {
initialBalance: parseAmount(initialBalance),
checks: checks.map(parseCheck),
};
};

// HELPERS
const displayAmount = (amount: number) => amount.toFixed(2);
const col = (...children: string[]) => children.join("\n");
// const row = (...children: string[]) => children.join(" ");

// COMPONENTS
const InitialBalance = ({ balance }: { balance: number }) =>
`Original_Balance: \${displayAmount(balance)}`;
const Check = ({ check }: { check: Check }) =>
`\${check.id} \${check.type} \${displayAmount(check.amount)}`;
const RunningBalance = ({ balance }: { balance: number }) =>
`Balance \${displayAmount(balance)}`;
const CheckRow = ({ check, balance }) =>
`\${Check({ check })} \${RunningBalance({ balance })}`;
const TotalExpense = ({ total }: { total: number }) =>
`Total expense \${displayAmount(total)}`;
const AverageExpense = ({ average }: { average: number }) =>
`Average expense \${displayAmount(average)}`;

// APP
const Checkbook = ({
checkbook: { initialBalance, checks },
}: {
checkbook: Checkbook;
}) => {
const checkRows: (checks: Check[]) => string[] = pipe(
sort(byId.asc),
map((check: Check, index: number) =>
CheckRow({
check,
balance: currentBalance(initialBalance, checks.slice(0, index + 1)), // index + 1 means inclusive
}),
),
);

return col(
InitialBalance({ balance: initialBalance }),
...checkRows(checks),
TotalExpense({ total: total(checks) }),
AverageExpense({ average: average(checks) }),
);
};

// WRAPPER
export const checkbookBalancer = (text: string) => {
const checkbook = parse(text);
return Checkbook({ checkbook });
};
``````
cvanpoelje
``````tidyCheckbook = checkbook => {
sum = 0;
rules = checkbook.split("\n");
rules[0] = `Original_balance: \${rules[0]}`;
amountOfRules = rules.length-1;
return rules
.map(rule => {
rule = rule.replace(/[;?!{},]/g, "").split(" ");
return rule =  { id: rule[0], category: rule[1], checkAmount: parseFloat(rule[2]).toFixed(2)}
})
.sort((a, b) => a.id - b.id)
.map(rule => {
return (`\${rule.id} \${rule.category} \${isNaN(rule.checkAmount) ? '':rule.checkAmount}`)})
.join("\n")
.concat(`\n======================\nTotal expense: \${sum}\nAverage expense: \${sum / amountOfRules}`
)
}
``````
costicaaa

i think it is quite funny how most of the answers here ignore basically half of the requirement and do not print the new balance :)

i think it should be a signal you are ignoring the task and creating something that seems fun for you to solve ( looking at how everyone handles the 'checks numbers' in his own way, even tho it was not a req :) )

just my 2 cents, but always open for a good constructive namecalling kind of talk hihi

Valerio Narcisi

//TS
//TDD

``````
import checkbookBalancing from './checkbook-balancing';

describe('tdd checkbook balancing', () => {
const endOfLine = '\n';
test('Example Checkbook', () => {
const checkBook = `1000.00
125 Market 125.45
126 Hardware 34.95
127 Video 7.45
128 Book 14.32
129 Gasoline 16.10`;

const res = `Original_Balance: 1000.00\${endOfLine}125 Market 125.45 Balance 874.55\${endOfLine}126 Hardware 34.95 Balance 839.60\${endOfLine}127 Video 7.45 Balance 832.15\${endOfLine}128 Book 14.32 Balance 817.83\${endOfLine}129 Gasoline 16.10 Balance 801.73\${endOfLine}Total expense 198.27\${endOfLine}Average expense 39.65`;
expect(checkbookBalancing(checkBook)).toEqual(res);
});

test('Challenge Checkbook', () => {
const checkBook = `1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;`;

const res = `Original_Balance: 1233.00\${endOfLine}125 Hardware 24.8 Balance 1208.20\${endOfLine}123 Flowers 93.5 Balance 1114.70\${endOfLine}127 Meat 120.90 Balance 993.80\${endOfLine}120 Picture 34.00 Balance 959.80\${endOfLine}124 Gasoline 11.00 Balance 948.80\${endOfLine}123 Photos 71.4 Balance 877.40\${endOfLine}122 Picture 93.5 Balance 783.90\${endOfLine}132 Tires 19.00 Balance 764.90\${endOfLine}129 Stamps 13.6 Balance 751.30\${endOfLine}129 Fruits 17.6 Balance 733.70\${endOfLine}129 Market 128.00 Balance 605.70\${endOfLine}121 Gasoline 13.6 Balance 592.10\${endOfLine}Total expense 640.90\${endOfLine}Average expense 53.41`;
expect(checkbookBalancing(checkBook)).toEqual(res);
});
});
``````
``````
export default (str: string, endOfLine = '\n', separator = ' '): string | null => {
if (str === '') {
return null;
}

const initialState = {
originalBalance: null,
rows: '',
totalExpense: 0,
averageExpense: 0,
};

const round2 = num => Math.round(num * 100) / 100;

const res = str
.replace(/[^0-9A-Za-z\.\s]/g, '')
.split(endOfLine)
.map(val => val.trim().split(separator))
.reduce((prev, curr, i) => {
if (i === 0) {
return { ...initialState, ...{ originalBalance: round2(curr[0]) } };
}
const totalExpense = round2(Number(prev.totalExpense + Number(curr[2])));
const averageExpense = round2(totalExpense / i);
return {
...prev,
...{
originalBalance: prev.originalBalance,
rows: `\${prev.rows}\${curr[0]} \${curr[1]} \${curr[2]} Balance \${round2(
prev.originalBalance - prev.totalExpense - Number(curr[2]),
).toFixed(2)}\${endOfLine}`,
totalExpense: totalExpense,
averageExpense: averageExpense,
},
};
}, initialState);

return `Original_Balance: \${res.originalBalance.toFixed(2)}\${endOfLine}\${
res.rows
}Total expense \${res.totalExpense.toFixed(2)}\${endOfLine}Average expense \${res.averageExpense.toFixed(2)}`;
};

``````
Petr Damborský

First time seeing these challenges. I don't know If there are any special requirements (e.g.: Should the code be production-ready? Will it be shared in a team? etc.) so I wrote just a simple solution for the task.

``````const input = `
1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;
`;

let output = 'Original_Balance: ';

const entries = input.split('\n').filter(e => e);
let balance = parseFloat(entries.splice(0, 1)[0].trim());
output += balance;

let spent = 0;
entries.forEach(e => {
const data = e.split(' ').map(d => {
if (isNaN(parseFloat(d))) {
return d.replace(/[^\w\s.]/gi, '');
} else {
return parseFloat(d);
}
});
const [check, category, amount] = [...data];
spent += amount;
output += `\n\${check} \${category} \${amount} Balance \${(balance-spent).toFixed(2)}`;
});

output += `
Total expense \${spent.toFixed(2)}
Average expense \${(spent/entries.length).toFixed(2)}
`;

console.log(output);
``````
Jarod Smith
``````function balanceCheckbook(checkbook = '') {
if(typeof(checkbook) !== 'string')
throw new Error('input must be a string');

checkbook = checkbook.trim();

let originalBalance = 0;
let entries = checkbook.split('\n');
let balance = originalBalance;

// exclude the orginalBalance at the top
let totalEntries = entries.length - 1;

for(index in entries) {
let entry = entries[index];

// clean the line
entry = entry.replace(/[^\.\w\d ]+/g, '');

// parse the line
if(index == 0)
{
originalBalance = parseFloat(entry);
balance = originalBalance;
console.log(`Original_Balance: \${originalBalance.toFixed(2)}`);
}
else
{
let section = entry.split(' ');
let expenseString = section[2];
let expense = parseFloat(expenseString);

balance -= expense;
console.log(`\${entry} Balance \${balance.toFixed(2)}`);
}
}

// compute summaries
let totalExpense = originalBalance - balance;
let averageExpense = totalExpense/totalEntries;

// print summary
console.log(`Total expense \${totalExpense.toFixed(2)}`);
console.log(`Average expense \${averageExpense.toFixed(2)}`)
}
const input = `
1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;
`;
balanceCheckbook(input);
``````
Mohamed ELIDRISSI

My solution :)
Also available in my github repo: github.com/MohamedElidrissi/Daily-...

``````function balanceCheckbook(checkbook) {
checkbook = checkbook.replace(/[^A-Za-z0-9\.\n ]/g, '');
const reports = checkbook.match(/[0-9]+\s[A-Za-z]+\s[0-9]+(\.[0-9]{1,2})?/g);
const expenses = checkbook.match(/ [0-9]+(\.[0-9]{1,2})?/g);
const totalExpenses = expenses
.reduce((p, c) => parseFloat(p) + parseFloat(c))
.toFixed(2);
let balance = checkbook.match(/^[0-9]+(\.[0-9]{1,2})?/g)[0];
let output = `Original Balance: \${balance}\n`;

reports.forEach((report, index) => {
balance -= expenses[index];
output += `\${report} Balance \${balance.toFixed(2)}\n`;
});

output += `Total expense \${totalExpenses}\n`;
output += `Average expense \${(totalExpenses / expenses.length).toFixed(2)}`;

return output;
}
``````
Ilyes Chouia • Edited

PHP:

``````
function report(\$string) {

\$data = explode(PHP_EOL, \$string);
\$data = array_map(function(\$line){
return preg_replace('/[^\w\s.]+/', '', \$line);
}, \$data);

\$balance = floatval(array_shift(\$data));
\$result = 'Original Balance: ' . number_format(\$balance, 2) . "<br>";
\$expenses = [];
sort(\$data);
foreach (\$data as \$index => \$line) {
\$parts = explode(' ', \$line);
foreach (\$parts as \$line_segment) {
if (\$line_segment != end(\$parts)) {
\$result .= "\$line_segment ";
}
}
\$expenses[] = floatval(end(\$parts));
\$balance -= end(\$expenses);
\$result .= number_format(end(\$parts), 2) . ', Balance: ' . number_format(\$balance, 2) . "<br>";
}

\$total_expenses = array_sum(\$expenses);
\$result .= 'Total expenses: ' . number_format(\$total_expenses, 2) . "<br>";
\$result .= 'Average expense: ' . number_format(\$total_expenses / count(\$expenses), 2) . "<br>";
return \$result;
}

\$text = '1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;';

echo report(\$text);

``````
Corey Alexander

Walking through the requirements now while I'm on the train so I can get rolling on this tonight!

I see some people are sorting the checkbook before doing the balances by check number.
I am NOT going to do this, because I'm not confident the check number order is necessarily the order the checks were used / were processed by the bank. Therefore I think it might be more correct to keep the ordering of the given list!

Corey Alexander • Edited

Here is my Rust Version!

``````#[macro_use]
extern crate lazy_static;

use regex::Regex;

#[derive(Debug)]
struct Transaction {
check_number: String, // This is just an identifier to me. Who am I to stop your UUID check number
category: String,
amount: f32,
}

impl Transaction {
fn from_line(line: &str) -> Self {
lazy_static! {
static ref STRIP_REGEX: Regex = Regex::new(r"[^\w \.]").unwrap();
static ref PARSE_REGEX: Regex = Regex::new(r"(\d+) (.+) (\d*\.\d*)").unwrap();
}

let stripped_line: String = STRIP_REGEX.replace_all(line, "").to_string();

println!("{}", stripped_line);
let captures = PARSE_REGEX.captures(&stripped_line).unwrap();

Self {
check_number: captures.get(1).unwrap().as_str().to_owned(),
category: captures.get(2).unwrap().as_str().to_owned(),
amount: captures.get(3).unwrap().as_str().parse().unwrap(),
}
}

fn to_string(&self) -> String {
format!("{} {} {:.2}", self.check_number, self.category, self.amount)
}
}

pub fn process_checkbook_string(input: &str) -> String {
let mut lines = input.lines();
let starting_balance: f32 = lines.next().unwrap().parse().unwrap();

let transactions = lines.map(Transaction::from_line);

let mut current_balance = starting_balance;
let mut total_expense = 0.;
let mut transaction_count = 0;
let mut transaction_strings: Vec<String> = transactions
.map(|transaction| {
current_balance -= transaction.amount;
total_expense += transaction.amount;
transaction_count += 1;
format!("{} Balance {:.2}", transaction.to_string(), current_balance)
})
.collect();

let starting_balance_string = format!("Original_Balance: {:.2}", starting_balance);
let total_expense_string = format!("Total expense {:.2}", total_expense);
let avg_expense_string = format!(
"Average expense {:.2}",
total_expense / transaction_count as f32
);

let mut output = vec![starting_balance_string];
output.append(&mut transaction_strings);
output.push(total_expense_string);
output.push(avg_expense_string);
output.join("\n")
}

#[cfg(test)]
mod tests {
use crate::*;

const EXAMPLE_INPUT: &str = "1000.00
125 Market 125.45
126 Hardware 34.95
127 Video 7.45
128 Book 14.32
129 Gasoline 16.10";

const EXAMPLE_OUTPUT: &str = "Original_Balance: 1000.00
125 Market 125.45 Balance 874.55
126 Hardware 34.95 Balance 839.60
127 Video 7.45 Balance 832.15
128 Book 14.32 Balance 817.83
129 Gasoline 16.10 Balance 801.73
Total expense 198.27
Average expense 39.65";

const CHALLENGE_INPUT: &str = "1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;";

const CHALLENGE_OUTPUT: &str = "Original_Balance: 1233.00
125 Hardware 24.80 Balance 1208.20
123 Flowers 93.50 Balance 1114.70
127 Meat 120.90 Balance 993.80
120 Picture 34.00 Balance 959.80
124 Gasoline 11.00 Balance 948.80
123 Photos 71.40 Balance 877.40
122 Picture 93.50 Balance 783.90
132 Tires 19.00 Balance 764.90
129 Stamps 13.60 Balance 751.30
129 Fruits 17.60 Balance 733.70
129 Market 128.00 Balance 605.70
121 Gasoline 13.60 Balance 592.10
Total expense 640.90
Average expense 53.41";

#[test]
fn it_works_for_the_example() {
assert_eq!(EXAMPLE_OUTPUT, process_checkbook_string(EXAMPLE_INPUT));
}

#[test]
fn it_works_for_the_challenge() {
assert_eq!(CHALLENGE_OUTPUT, process_checkbook_string(CHALLENGE_INPUT));
}
}

``````

It's not quite as well factored as I'd like, and it definitely doesn't account for nearly as many edge cases as it should but it works!

Héctor Pascual

Python:

``````import re

checkbook = """1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;"""

def parse_checkbook():
check_list = checkbook.split('\n')
check_list_alphanum = [re.sub(r'[^\d\w.\s]', '', line) for line in check_list]
balance = check_list_alphanum[0]
prices = []
for row in check_list_alphanum:
m = re.search(r' (\d.*)', row)
if m is not None:
prices.append(m.group(0))

result = []
check_list_iter = iter(check_list_alphanum)
next(check_list_iter)
prices_iter = iter(prices)
result.append("Original_Balance : {}\n".format(balance))
for row in check_list_iter:
balance = "{0:.2f}".format(float(balance) - float(next(prices_iter)))
new_row = row + " Balance: {}".format(balance)
result.append(new_row+'\n')
prices = [float(p) for p in prices]
result.append("Total expense : {0:.2f}\n".format(sum(prices)))
result.append("Average expense : {0:.2f}\n".format(sum(prices)//len(prices)))
result = ''.join(result)
return result
``````
Michael Lang

## Ruby Language

Normally, I would build out classes for this sort of thing. A Checkbook class with Entries that can be Debit or Credit and so on so that it's easier to write and maintain tests in TDD style. This implementation is just a simplistic top-down functional approach. It's still testable, but it wasn't as easy to compose as an OOP solution would be.

### script with specs

``````def clean line
line.scan(/[\d\.a-z\s]/i).join
end

def etl line
line.scan(/(\d+\s\D+)\s([\d\.]+)/).flatten
end

def compose_entry prev_balance, line
entry = etl clean line
debit = entry[1].to_f
{
prev_balance: prev_balance,
memo: entry[0],
debit: debit,
new_balance: prev_balance - debit
}
end

def report lines
original_balance = clean(lines.shift).to_f
balance = original_balance
expenses = 0.0

debits = lines.map do |line|
entry = compose_entry balance, line
balance = entry[:new_balance]
expenses += entry[:debit]
[
entry[:memo],
("%0.2f" % entry[:debit]),
"Balance",
("%0.2f" % entry[:new_balance]),
].join(" ")
end

[
("Original_Balance: %0.2f" % original_balance),
debits,
("Total expense %0.2f" % expenses),
("Average expense %0.2f" % (expenses / debits.size)),
].join("\n")

end

require "spec"

DEMO_LEDGER = <<-DATA
1000.00
125 Market 125.45
126 Hardware 34.95
127 Video 7.45
128 Book 14.32
129 Gasoline 16.10
DATA
.split("\n").freeze

Original_Balance: 1000.00
125 Market 125.45 Balance 874.55
126 Hardware 34.95 Balance 839.60
127 Video 7.45 Balance 832.15
128 Book 14.32 Balance 817.83
129 Gasoline 16.10 Balance 801.73
Total expense 198.27
Average expense 39.65
DATA
.chomp.freeze

MESSY_LEDGER = <<-DATA
1233.00
125 Hardware;! 24.8?;
123 Flowers 93.5
127 Meat 120.90
120 Picture 34.00
124 Gasoline 11.00
123 Photos;! 71.4?;
122 Picture 93.5
132 Tires;! 19.00,?;
129 Stamps 13.6
129 Fruits{} 17.6
129 Market;! 128.00?;
121 Gasoline;! 13.6?;
DATA
.split("\n").freeze

Original_Balance: 1233.00
125 Hardware 24.80 Balance 1208.20
123 Flowers 93.50 Balance 1114.70
127 Meat 120.90 Balance 993.80
120 Picture 34.00 Balance 959.80
124 Gasoline 11.00 Balance 948.80
123 Photos 71.40 Balance 877.40
122 Picture 93.50 Balance 783.90
132 Tires 19.00 Balance 764.90
129 Stamps 13.60 Balance 751.30
129 Fruits 17.60 Balance 733.70
129 Market 128.00 Balance 605.70
121 Gasoline 13.60 Balance 592.10
Total expense 640.90
Average expense 53.41
DATA
.chomp.freeze

describe "#clean" do
let(:ledger) { MESSY_LEDGER.dup }
it { expect(clean(ledger[0])).to eq "1233.00" }
it { expect(clean(ledger[1])).to eq "125 Hardware 24.8" }
it { expect(clean(ledger[6])).to eq "123 Photos 71.4" }
end

describe "#etl" do
let(:ledger) { MESSY_LEDGER.dup }
it { expect(etl(clean(ledger[1]))).to eq ["125 Hardware", "24.8"] }
it { expect(etl(clean(ledger[10]))).to eq ["129 Fruits", "17.6"] }
end

describe "#compose_entry" do
let(:ledger) { MESSY_LEDGER.dup }
it { expect(compose_entry(1000, clean(ledger[1]))).to eq({ debit: 24.8, memo: "125 Hardware", new_balance: 975.2, prev_balance: 1000}) }
it { expect(compose_entry(1000, clean(ledger[10]))).to eq({ debit: 17.6, memo: "129 Fruits", new_balance: 982.4, prev_balance: 1000}) }
end

describe "#report" do
end
``````

### output

``````>> rspec checkbook.rb
.........

Finished in 0.00648 seconds (files took 0.15208 seconds to load)
9 examples, 0 failures
``````
Florian Rand

(There is a small typo, it's BSD-2 license, FreeBSD is the OS ;).

Brian Bethencourt

Thanks for spotting it, we'll get it fixed.