DEV Community

dev.to staff
dev.to staff

Posted on

Daily Challenge #4 - Checkbook Balancing

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 user @g964 on CodeWars.

You are given a small checkbook to balance that is given to you as a string. Sometimes, this checkbook will be cluttered by non-alphanumeric characters.

The first line shows the original balance. Each other (not blank) line gives information: check number, category, and check amount.

You need to clean the lines first, keeping only letters, digits, dots, and spaces. Next, return the report as a string. On each line of the report, you have to add the new balance. In the last two lines, return the total expenses and average expense. Round your results to two decimal places.

Example Checkbook

1000.00
125 Market 125.45
126 Hardware 34.95
127 Video 7.45
128 Book 14.32
129 Gasoline 16.10

Example Solution

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

Challenge 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?;

Good luck and happy coding!


Thank you to CodeWars, who has licensed redistribution of this challenge under the 2-Clause BSD License!

Want to propose a challenge for a future post? Email yo+challenge@dev.to with your suggestions!

Top comments (38)

Collapse
 
zerquix18 profile image
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)}
`)

My attempt with comments:

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)}
`)
Collapse
 
dak425 profile image
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 🎉

Collapse
 
johncip profile image
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]
  (map read-string (re-seq #"(?:\w|\.)+" 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 $$))])))))
Collapse
 
alvaromontoro profile image
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.

Collapse
 
alvaromontoro profile image
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)}`);
}
Collapse
 
jaloplo profile image
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
Collapse
 
martyhimmel profile image
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);
Collapse
 
v613 profile image
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};

result

Collapse
 
kerrishotts profile image
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();
};

Enter fullscreen mode Exit fullscreen mode
Collapse
 
zerquix18 profile image
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.

Collapse
 
neotamizhan profile image
Siddharth Venkatesan

Ruby

class Checkbook

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

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

  def average_expense
    total_expense / @entries.size
  end

  def load!
    content = File.readlines("input.txt")    
    @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    
    load!(line)
  end 

  def load!(line)    
    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
Collapse
 
ryansmith profile image
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
Collapse
 
rvictorino profile image
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) {
        return toSanitize - ~ /[^\w\.]+/
    }

    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
Collapse
 
petrdamborsky profile image
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);
Collapse
 
caleb_rudder profile image
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');
    }

    function addExpense(expense){
        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(" ");
                addExpense(parseFloat(line[2]));
                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
    }
}
Collapse
 
mrdulin profile image
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, "")

  scanner := bufio.NewScanner(strings.NewReader(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")
}
Collapse
 
peter279k profile image
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;
}