此类问题可能更适合Programmers Stack Exchange 站点或Code Review 站点。由于它是在询问实施,我认为在这里问很好。这些网站往往有一些overlap。
正如@DondiMichaelStroma 所指出的,并且您已经知道,您的代码运行良好!但是,有不止一种方法可以做到这一点。对我来说,如果这是在一个小脚本中,我可能会保持原样并继续进行项目的下一部分。如果这是在更专业的代码库中,我会进行一些更改。
对我来说,在为专业代码库编写代码时,我会尽量记住一些事情。
那么让我们来看看你的代码:
my %data = (
'B2' => {
'one' => {
timestamp => '00:12:30'
},
'two' => {
timestamp => '00:09:30'
}
},
'C3' => {
'three' => {
timestamp => '00:13:45'
},
'adam' => {
timestamp => '00:09:30'
}
}
);
定义数据的方式非常好,格式也很好。这可能不是 %data 在您的代码中构建的方式,但单元测试可能会有这样的哈希。
my @flattened;
for my $outer_key (keys %data) {
for my $inner_key (keys %{$data{$outer_key}}) {
push @flattened, [
$data{$outer_key}{$inner_key}{timestamp}
, $outer_key
, $inner_key
];
}
}
for my $ary (sort { $a->[0] cmp $b->[0] || $a->[2] cmp $b->[2] } @flattened) {
print join ',' => @$ary;
print "\n";
}
变量名可以更具描述性,@flattened 数组中有一些冗余数据。用Data::Dumper打印,你可以看到我们在多个地方有C3和B2。
$VAR1 = [
'00:13:45',
'C3',
'three'
];
$VAR2 = [
'00:09:30',
'C3',
'adam'
];
$VAR3 = [
'00:12:30',
'B2',
'one'
];
$VAR4 = [
'00:09:30',
'B2',
'two'
];
也许这没什么大不了的,或者也许您想保留在密钥 B2 下获取所有数据的功能。
这是我们可以存储该数据的另一种方式:
my %flattened = (
'B2' => [['one', '00:12:30'],
['two', '00:09:30']],
'C3' => [['three','00:13:45'],
['adam', '00:09:30']]
);
它可能会使排序更复杂,但它使数据结构更简单!也许这越来越接近镀金,或者您可能会从代码的另一部分中的这种数据结构中受益。我的偏好是保持数据结构简单,并在处理它们时添加额外的代码。如果您决定需要将 %flattened 转储到日志文件中,您可能会因为没有看到重复数据而感到高兴。
实施
设计:我认为我们希望将其保留为两个不同的操作。这将有助于代码清晰,我们可以单独测试每个函数。第一个函数将在我们想要使用的数据格式之间进行转换,第二个函数将对数据进行排序。这些函数应该在 Perl 模块中,我们可以使用Test::More 进行单元测试。我不知道我们从哪里调用这些函数,所以假设我们从main.pl 调用它们,我们可以将这些函数放在一个名为Helper.pm 的模块中。这些名称应该更具描述性,但我再次不确定应用程序是什么!伟大的名字带来可读的代码。
main.pl
这就是main.pl 的样子。即使没有 cmets,描述性名称也可以使其自我记录。这些名字还有待改进!
#!/usr/bin/env perl
use strict;
use warnings;
use Data::Dumper;
use Utilities::Helper qw(sort_by_times_then_names convert_to_simple_format);
my %data = populate_data();
my @sorted_data = @{ sort_by_times_then_names( convert_to_simple_format( \%data ) ) };
print Dumper(@sorted_data);
实用程序/Helper.pm
这是可读和优雅的吗?我认为它可以使用一些改进。更具描述性的变量名称也将有助于此模块。然而,它很容易测试,并且保持我们的主要代码干净和数据结构简单。
package Utilities::Helper;
use strict;
use warnings;
use Exporter qw(import);
our @EXPORT_OK = qw(sort_by_times_then_names convert_to_simple_format);
# We could put a comment here explaning the expected input and output formats.
sub sort_by_times_then_names {
my ( $data_ref ) = @_;
# Here we can use the Schwartzian Transform to sort it
# Normally, we would just be sorting an array. But here we
# are converting the hash into an array and then sorting it.
# Maybe that should be broken up into two steps to make to more clear!
#my @sorted = map { $_ } we don't actually need this map
my @sorted = sort {
$a->[2] cmp $b->[2] # sort by timestamp
||
$a->[1] cmp $b->[1] # then sort by name
}
map { my $outer_key=$_; # convert $data_ref to an array of arrays
map { # first element is the outer_key
[$outer_key, @{$_}] # second element is the name
} # third element is the timestamp
@{$data_ref->{$_}}
}
keys %{$data_ref};
# If you want the elements in a different order in the array,
# you could modify the above code or change it when you print it.
return \@sorted;
}
# We could put a comment here explaining the expected input and output formats.
sub convert_to_simple_format {
my ( $data_ref ) = @_;
my %reformatted_data;
# $outer_key and $inner_key could be renamed to more accurately describe what the data they are representing.
# Are they names? IDs? Places? License plate numbers?
# Maybe we want to keep it generic so this function can handle different kinds of data.
# I still like the idea of using nested for loops for this logic, because it is clear and intuitive.
for my $outer_key ( keys %{$data_ref} ) {
for my $inner_key ( keys %{$data_ref->{$outer_key}} ) {
push @{$reformatted_data{$outer_key}},
[$inner_key, $data_ref->{$outer_key}{$inner_key}{timestamp}];
}
}
return \%reformatted_data;
}
1;
run_unit_tests.pl
最后,让我们进行一些单元测试。这可能比您在这个问题上所寻找的要多,但我认为用于测试的干净接缝是优雅代码的一部分,我想证明这一点。 Test::More 非常适合这个。我什至会加入一个测试工具和格式化程序,这样我们就可以获得一些优雅的输出。如果没有安装TAP::Formatter::JUnit,可以使用TAP::Formatter::Console。
#!/usr/bin/env perl
use strict;
use warnings;
use TAP::Harness;
my $harness = TAP::Harness->new({
formatter_class => 'TAP::Formatter::JUnit',
merge => 1,
verbosity => 1,
normalize => 1,
color => 1,
timer => 1,
});
$harness->runtests('t/helper.t');
t/helper.t
#!/usr/bin/env perl
use strict;
use warnings;
use Test::More;
use Utilities::Helper qw(sort_by_times_then_names convert_to_simple_format);
my %data = (
'B2' => {
'one' => {
timestamp => '00:12:30'
},
'two' => {
timestamp => '00:09:30'
}
},
'C3' => {
'three' => {
timestamp => '00:13:45'
},
'adam' => {
timestamp => '00:09:30'
}
}
);
my %formatted_data = %{ convert_to_simple_format( \%data ) };
my %expected_formatted_data = (
'B2' => [['one', '00:12:30'],
['two', '00:09:30']],
'C3' => [['three','00:13:45'],
['adam', '00:09:30']]
);
is_deeply(\%formatted_data, \%expected_formatted_data, "convert_to_simple_format test");
my @sorted_data = @{ sort_by_times_then_names( \%formatted_data ) };
my @expected_sorted_data = ( ['C3','adam', '00:09:30'],
['B2','two', '00:09:30'],
['B2','one', '00:12:30'],
['C3','thee','00:13:45'] #intentionally typo to demonstrate output
);
is_deeply(\@sorted_data, \@expected_sorted_data, "sort_by_times_then_names test");
done_testing;
测试输出
以这种方式进行测试的好处是,它会在测试失败时告诉您哪里出了问题。
<testsuites>
<testsuite failures="1"
errors="1"
time="0.0478239059448242"
tests="2"
name="helper_t">
<testcase time="0.0452120304107666"
name="1 - convert_to_simple_format test"></testcase>
<testcase time="0.000266075134277344"
name="2 - sort_by_times_then_names test">
<failure type="TestFailed"
message="not ok 2 - sort_by_times_then_names test"><![CDATA[not o
k 2 - sort_by_times_then_names test
# Failed test 'sort_by_times_then_names test'
# at t/helper.t line 45.
# Structures begin differing at:
# $got->[3][1] = 'three'
# $expected->[3][1] = 'thee']]></failure>
</testcase>
<testcase time="0.00154280662536621" name="(teardown)" />
<system-out><![CDATA[ok 1 - convert_to_simple_format test
not ok 2 - sort_by_times_then_names test
# Failed test 'sort_by_times_then_names test'
# at t/helper.t line 45.
# Structures begin differing at:
# $got->[3][1] = 'three'
# $expected->[3][1] = 'thee'
1..2
]]></system-out>
<system-err><![CDATA[Dubious, test returned 1 (wstat 256, 0x100)
]]></system-err>
<error message="Dubious, test returned 1 (wstat 256, 0x100)" />
</testsuite>
</testsuites>
总之,比起简洁,我更喜欢可读性和清晰性。有时,您可以编写更容易编写且逻辑更简单的效率较低的代码。将丑陋的代码放入函数中是隐藏它的好方法!运行代码以节省 15 毫秒是不值得的。如果您的数据集足够大以至于性能成为问题,那么 Perl 可能不是适合这项工作的工具。如果您真的在寻找一些简洁的代码,请在Code Golf Stack Exchange 上发帖挑战。