【问题标题】:Sorting a hash in Perl when the keys are dynamic当键是动态的时在 Perl 中对哈希进行排序
【发布时间】:2015-08-21 01:41:09
【问题描述】:

我有一个哈希如下:

my %data = (
    'B2' => {
        'one' => {
            timestamp => '00:12:30'
        },
        'two' => {
            timestamp => '00:09:30'
        }
    },
    'C3' => {
        'three' => {
            timestamp => '00:13:45'
        },
        'adam' => {
            timestamp => '00:09:30'
        }
    }
);

(结构实际上比这更复杂;我在这里简化它。)

我希望对时间戳进行“全局”排序,然后对内部哈希的键(一、二、三亚当)进行排序。但是内部哈希的键是动态的;在从文件中读取数据之前,我不知道它们会是什么。

我希望上述哈希的排序输出为:

00:09:30,C3,adam
00:09:30,B2,two
00:12:30,B2,one
00:13:45,C3,three

我已经查看了许多关于按键和/或值对哈希进行排序的问题/答案,但我无法弄清楚何时不知道键名。 (或者也许我只是不明白。)

我现在要做的是两个步骤。

将散列扁平化为数组:

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";
}

我想知道是否有更简洁、优雅、高效的方法来做到这一点?

【问题讨论】:

  • 我喜欢使用 Schwartzian 变换来处理这样的事情:en.wikipedia.org/wiki/Schwartzian_transform
  • 你写的我觉得不错。
  • 好的,谢谢,DondiMichaelStroma。我在想可能有一种方法可以一次性完成。 @chilemagic,我不知道如何使 Schwartzian Transform 起作用。
  • @user3112401 我会在几个小时后发布答案,先去理发!

标签: perl sorting hash


【解决方案1】:

此类问题可能更适合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打印,你可以看到我们在多个地方有C3B2

$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 上发帖挑战。

【讨论】:

  • 哇!我询问了关于排序的问题,并获得了关于 Perl 良好编程实践的课程。谢谢!我把它打印出来并通读。如果我有任何其他问题,我们会尽快回复您。
猜你喜欢
  • 1970-01-01
  • 2019-03-17
  • 1970-01-01
  • 2020-11-10
  • 2011-10-30
  • 2015-03-12
  • 1970-01-01
  • 2018-05-10
  • 1970-01-01
相关资源
最近更新 更多