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!

Latest comments (37)

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
 
dimitrilahaye profile image
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');
}
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;
}
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
 
valerionarcisi profile image
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)}`;
};

Collapse
 
margo1993 profile image
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)
}
Collapse
 
kesprit profile image
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))")
}
Collapse
 
cvanpoelje profile image
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}`
    )
}
Collapse
 
wolverineks profile image
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 });
};