我正在尝试创建一种方法,在我不知道最终用户使用这两种互斥方式编写数字的方式中,提供“尽力而为”的十进制输入解析:
该方法在下面的代码中实现为parse_decimal(..)
。此外,我已经定义了20个测试用例,显示了该方法的启发式方法应该如何工作。
虽然下面的代码通过了测试,但它非常可怕且难以理解。我确信有一种更紧凑和可读的方法来实现该方法。可能包括更聪明地使用正则表达式。
我的问题很简单:鉴于下面的代码和测试用例,你如何改进parse_decimal(...)以使其在传递测试时更加紧凑和可读?
澄清:
^\d{1,3}[\.,]\d{3}$
是不明确的,因为无法逻辑确定哪个字符用作千位分隔符,哪个用作小数分隔符。在不明确的情况下,我们将简单地假设使用美式小数:“,”作为千位分隔符和“。”作为小数分隔符。相关代码包括测试用例:
#!/usr/bin/perl -wT
use strict;
use warnings;
use Test::More tests => 20;
ok(&parse_decimal("1,234,567") == 1234567);
ok(&parse_decimal("1,234567") == 1.234567);
ok(&parse_decimal("1.234.567") == 1234567);
ok(&parse_decimal("1.234567") == 1.234567);
ok(&parse_decimal("12,345") == 12345);
ok(&parse_decimal("12,345,678") == 12345678);
ok(&parse_decimal("12,345.67") == 12345.67);
ok(&parse_decimal("12,34567") == 12.34567);
ok(&parse_decimal("12.34") == 12.34);
ok(&parse_decimal("12.345") == 12345);
ok(&parse_decimal("12.345,67") == 12345.67);
ok(&parse_decimal("12.345.678") == 12345678);
ok(&parse_decimal("12.34567") == 12.34567);
ok(&parse_decimal("123,4567") == 123.4567);
ok(&parse_decimal("123.4567") == 123.4567);
ok(&parse_decimal("1234,567") == 1234.567);
ok(&parse_decimal("1234.567") == 1234.567);
ok(&parse_decimal("12345") == 12345);
ok(&parse_decimal("12345,67") == 12345.67);
ok(&parse_decimal("1234567") == 1234567);
sub parse_decimal($) {
my $input = shift;
$input =~ s/[^\d,\.]//g;
if ($input !~ /[,\.]/) {
return &parse_with_separators($input, '.', ',');
} elsif ($input =~ /\d,\d+\.\d/) {
return &parse_with_separators($input, '.', ',');
} elsif ($input =~ /\d\.\d+,\d/) {
return &parse_with_separators($input, ',', '.');
} elsif ($input =~ /\d\.\d+\.\d/) {
return &parse_with_separators($input, ',', '.');
} elsif ($input =~ /\d,\d+,\d/) {
return &parse_with_separators($input, '.', ',');
} elsif ($input =~ /\d{4},\d/) {
return &parse_with_separators($input, ',', '.');
} elsif ($input =~ /\d{4}\.\d/) {
return &parse_with_separators($input, '.', ',');
} elsif ($input =~ /\d,\d{3}$/) {
return &parse_with_separators($input, '.', ',');
} elsif ($input =~ /\d\.\d{3}$/) {
return &parse_with_separators($input, ',', '.');
} elsif ($input =~ /\d,\d/) {
return &parse_with_separators($input, ',', '.');
} elsif ($input =~ /\d\.\d/) {
return &parse_with_separators($input, '.', ',');
} else {
return &parse_with_separators($input, '.', ',');
}
}
sub parse_with_separators($$$) {
my $input = shift;
my $decimal_separator = shift;
my $thousand_separator = shift;
my $output = $input;
$output =~ s/\Q${thousand_separator}\E//g;
$output =~ s/\Q${decimal_separator}\E/./g;
return $output;
}
答案 0 :(得分:5)
这类似于自动猜测输入字符编码的程序 - 它有时可能会起作用,但通常是一种非常糟糕的策略,会导致错误和混淆不确定的行为。
例如,如果您看到“123,456”,则表示您没有足够的信息来猜测这意味着什么。
所以我会谨慎对待这个问题,并且永远不要将这种技术用于任何重要的事情。
答案 1 :(得分:3)
这些问题的想法是查看代码并找出你输入两次的地方。当你看到它时,努力将其删除。我的程序处理测试数据中的所有内容,而且我不必重复执行程序逻辑结构。这让我专注于数据而不是程序流程。
首先,让我们清理你的测试。你确实有一组要测试的对,所以让我们将它们放入数据结构中。您可以根据需要在数据结构中添加或删除项目,测试将自动调整:
use Test::More 'no_plan';
my @pairs = (
# got expect
[ "1,234,567", 1234567 ],
[ "1,234567", 1.234567 ],
[ "1.234.567", 1234567 ],
[ "1.234567", 1.234567 ],
[ "12,345", 12345 ],
[ "12,345,678", 12345678 ],
[ "12,345.67", 12345.67 ],
[ "12,34567", 12.34567 ],
[ "12.34", 12.34 ],
[ "12.345", 12345 ], # odd case!
[ "12.345,67", 12345.67 ],
[ "12.345.678", 12345678 ],
[ "12.34567", 12.34567 ],
[ "123,4567", 123.4567 ],
[ "123.4567", 123.4567 ],
[ "1234,567", 1234.567 ],
[ "1234.567", 1234.567 ],
[ "12345", 12345 ],
[ "12345,67", 12345.67 ],
[ "1234567", 1234567 ],
);
现在您已将其置于数据结构中,您的大量测试将缩短为短foreach
循环:
foreach my $pair ( @pairs ) {
my( $original, $expected ) = @$pair;
my $got = parse_number( $original );
is( $got, $expected, "$original translates to $expected" );
}
parse_number
例程同样压缩成这个简单的代码。你的诀窍是在源代码中一遍又一遍地找出你在做什么,而不是那样做。我没有试图找出奇怪的调用约定和条件的长链,而是将数据规范化。我弄清楚哪些情况是奇怪的,然后把它们变成非奇怪的情况。在这段代码中,我将有关分隔符的所有知识浓缩成少数正则表达式,并返回两个可能列表中的一个,以向我展示千位分隔符和小数分隔符。一旦我有了,我完全删除千位分隔符并使小数分隔符完全停止。当我发现更多情况时,我只是添加了一个正则表达式,在该情况下返回true:
sub parse_number
{
my $string = shift;
my( $separator, $decimal ) = do {
local $_ = $string;
if(
/\.\d\d\d\./ || # two dots
/\.\d\d\d,/ || # dot before comma
/,\d{4,}/ || # comma with many following digits
/\d{4,},/ || # comma with many leading digits
/^\d{1,3}\.\d\d\d\z/ || # odd case of 123.456
0
)
{ qw( . , ) }
else { qw( , . ) }
};
$string =~ s/\Q$separator//g;
$string =~ s/\Q$decimal/./;
$string;
}
这是我在Mastering Perl的动态子例程章节中讨论的事情。虽然我不会在这里讨论它,但我可能会将这一系列正则表达式转换为某种类型的管道并使用grep。
这只是通过测试的程序的一部分。我将添加另一个步骤来验证该数字是否是处理脏数据的预期格式,但这并不是那么困难,只是一个简单的编程问题。
答案 2 :(得分:1)
这是一个更短更完整的功能版本:
sub parse_decimal($) {
my $input = shift;
my %other = ("." => ",", "," => ".");
$input =~ s/[^\d,.]//g;
if ($input !~ /[,.]/) {
return &parse_with_separators($input, '.', ',');
} elsif ($input =~ /(\.).*(,)/ or $input =~ /(,).*(\.)/) { # Both separators present
return &parse_with_separators($input, $2, $1);
} elsif ($input =~ /([,.])$/ or $input =~ /^([,.])/) { # Number ends or begins with decimal separator
return &parse_with_separators($input, $1, $other{$1});
} elsif ($input =~ /\d{4}([,.])/ or $input =~ /([,.])\d{4}/) { # group of 4+ digits next to a separator
return &parse_with_separators($input, $1, $other{$1});
} elsif ($input =~ /([,.]).*\1/) { # More than one of the same separator
return &parse_with_separators($input, $other{$1}, $1);
} elsif ($input =~ /\d*([,.])\d{0,2}$/) { # Fewer than 2 digits to the right of the separator
return &parse_with_separators($input, $1, $other{$1});
} else { # Assume '.' is decimal separator and ',' is thousands separator
return &parse_with_separators($input, '.', ',');
}
}
一些重要的事情:
答案 3 :(得分:0)
尝试猜测任何事物的区域设置始终是一项持续不断的努力。你有什么用这个功能的?以下测试看起来对我来说是错误的:
ok(&parse_decimal("12.34") == 12.34);
ok(&parse_decimal("12.345") == 12345);
如果我正在解析带有值的单个文档,我会非常恼火地发现这个结果。
我会设计此功能,并在包中使用一些旋钮和拨号来使用区域设置信息(使用localeconv())或ad-hoc值(如this answer中所示。)
修改强> 好的,让我试着更好地解释一下。对于“单一来源”,我指的是上下文或范围分隔符。我知道你可以从不同的来源导入;这是导入数据的本质。我们也知道我们事先无法知道这些不同来源的编码。
我要做的是对要导入的文件进行初步扫描(只是取样,而不是整个读取)并检查数值。如果我可以从示例中确定区域设置,那么我会尝试使用相同的区域设置导入整个文件。对我来说,一个文件是单一来源,我不希望它突然改变它的语言环境。
这就是我再问一次的原因:这个计划的目的是什么?
答案 4 :(得分:0)
要回答您的问题,不要质疑申请的有效性,
您的算法可归结为以下伪代码:
if (num_separators == 1) {
if (trailing_digits != 3 || leading_digits > 3) {
# Replace comma with period
return output
}
}
# Replace first separator with nothing, and second with '.'
return output
perl中的以下内容:
my $output = $input;
my $num_separators = (scalar split /[\,\.]/, $input) - 1;
if($num_separators == 1) {
my ($leading_digits, $trailing_digits) = split /[\,\.]/, $input;
if(length($trailing_digits) != 3 || length($leading_digits) > 3) {
$output =~ s/\,/\./;
return eval($output);
}
}
if($output =~ /^\d+\.\d+/) {
# Swap commas and periods if periods are first
$output =~ tr/\.\,/\,\./;
}
# remove commas
$output =~ s/\,//g;
return eval($output);
不知道这是否更好,但它是一般化的。