Code kata: infix calculator

  • Posted on
  • by

В продолжение к http://friendfeed.com/kkapp/b5d3658c — разбор и вычисление инфиксного арифметического выражения. Потратил несколько часов, и то сошлось только со второго захода. Убил кучу времени на попытки сделать двоичное AST и споткнулся на выражения типа (1 - 2 - 3), где важна ассоциативность слева.

#! /usr/bin/perl
use strict;
use warnings;

use Test::Most;

# expr = sub_expr
#        | sub_expr [+-] expr
# sub_expr = atom
#            | atom [*/] sub_expr
# atom = N
#        | ( expr )
# --->
#[3, +, [ 2, *, 3]]

sub parse_atom {
	if ($_[0] =~ /\G\s*\(/gc) {
		my @val = parse_expr($_[0]);
		$_[0] =~ /\G\s*\)/gc or return undef;
		return ([@val]);
	}

	if ($_[0] =~ /\G\s*([+-]?\d+)/gc) {
		return $1;
	}

	return undef;
}

sub parse_sub_expr {
	my @val = parse_atom($_[0]);

	if ($_[0] =~ /\G\s*([*\/])/gc) {
		push @val, $1;
		push @val, parse_sub_expr($_[0]);
	}

	return @val;
}

sub parse_expr {
	my @val = parse_sub_expr($_[0]);
	if (@val > 1) {
		@val = ([@val]);
	}

	if ($_[0] =~ /\G\s*([+-])/gc) {
		push @val, $1;
		push @val, parse_expr($_[0]);
	}

	return @val;
}

sub parse_calc {
	$_[0] =~ /^/g;
	if (my @val = parse_expr($_[0])) {
		if ($_[0] =~ /\G\s*$/gc) {
			if (@val == 1 && ref $val[0] eq 'ARRAY') {
				return $val[0];
			}
			else {
				return \@val;
			}
		}
	}

	return undef;
}

cmp_deeply(parse_calc('3'), [3]);
cmp_deeply(parse_calc('3 + 2'), [3, '+', 2]);
cmp_deeply(parse_calc('3 - 2 * 1'), [3, '-', [2, '*', 1]]);
cmp_deeply(parse_calc('(3 - 2) / 3'), [[3, '-', 2], '/', 3]);

sub calc { calc_tree(parse_calc($_[0])) }

my %f = (
	'+' => sub { $_[0] + $_[1] },
	'-' => sub { $_[0] - $_[1] },
	'*' => sub { $_[0] * $_[1] },
	'/' => sub { $_[0] / $_[1] },
);

sub calc_tree {
	my $tree = shift;
	unless (ref $tree) {
		return $tree;
	}

	my @rest = @$tree;
	my $head = shift @rest;

	my $rv = calc_tree($head);

	while (@rest >= 2) {
		my ($op, $right) = (shift @rest, shift @rest);
		$rv = $f{$op}->($rv, calc_tree($right));
	}

	return $rv;
}

is(calc('3'), 3);
is(calc('3 + 2'), 5);
is(calc('2 * 2 * 3'), 12);
is(calc('2 * 2 * 2 * 2 * 2'), 32);
is(calc('1+1+1'), 3);
is(calc('4-5+6-7'), -2);
is(calc('100/10*10'), 100);
is(calc('6 / 2 + -1'), 2);
is(calc('(6 + 3) * 2'), 18);
is(calc('((6 + 3) * 2 - 2) / 8'), 2);

done_testing;