当前位置: 首页 > 知识库问答 >
问题:

当密钥是动态的时,在Perl中对哈希进行排序

容磊
2023-03-14

我的哈希如下:

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

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

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

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

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

我想知道是否有更简洁、优雅、高效的方法?

共有1个答案

巫健柏
2023-03-14

此类型问题可能更适合程序员堆栈交换站点或代码审查站点。既然是问的是执行问题,我认为在这里问这个问题很好。这些网站往往有一些重叠。

正如@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'
        }
    }
);
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";
}

变量名可以更具描述性,并且<code>扁平化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']]
);

这可能会使排序更复杂,但会使数据结构更简单!也许这离镀金越来越近了,或者您可能会在代码的另一部分受益于这种数据结构。我的偏好是保持数据结构简单,并在处理它们时添加额外的代码。如果您决定需要将<code>%flatted</code>转储到日志文件,您可能会希望看到重复数据。

设计:我认为我们希望保持这是两个不同的操作。这将有助于代码清晰,我们可以单独测试每个功能。第一个函数将在我们想要使用的数据格式之间进行转换,第二个函数将对数据进行排序。这些函数应该在Perl模块中,我们可以使用Test::More来进行单元测试。我不知道我们从哪里调用这些函数,所以让我们假设我们从< code>main.pl调用它们,我们可以将这些函数放在一个名为< code>Helper.pm的模块中。这些名称应该更具描述性,但是我也不确定这里的应用程序是什么!伟大的名字导致可读的代码。

这就是main.pl的样子。即使没有注释,描述性名称也可以使其自我记录。这些名称也可以改进!

#!/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);

这是可读的和优雅的吗?我认为它需要一些改进。在本模块中,更具描述性的变量名称也会有所帮助。然而,它很容易测试,并保持我们的主要代码干净和数据结构简单。

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;

最后,让我们实现一些单元测试。这可能比你在这个问题上想要的要多,但是我认为干净的测试接缝是优雅代码的一部分,我想演示一下。测试::更多非常适合这个。我甚至会加入一个测试工具和格式化程序,这样我们就可以得到一些优雅的输出。如果你没有安装TAP::F或事件::JUnit,你可以使用TAP::F或事件::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');
#!/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可能不是该工作的合适工具。如果您真的在寻找一些简洁的代码,请在代码高尔夫堆栈交换上发布一个挑战。

 类似资料:
  • 我有一个代码,我需要跟踪不同类别中给定位置的一些值(随机出现)(并且数量相当大;~40,000),所以我认为散列散列是最好的方法,类别作为第一层键,位置作为第二层,值作为值;类似于: 然后,我需要按照这两个类别的顺序对它们进行排序和打印,然后进行定位,得到如下输出文件: 但是我无法计算出嵌套排序的语法(或者,有人比这种方法有更好的想法吗?

  • 我已经完成了一个android应用程序的开发,该应用程序使用facebook sdk与facebook集成。目前,我对这个应用程序没有任何问题需要解决,因为它功能齐全,运行良好——但尽管如此,我相信即使应用程序正在运行,我仍然不完全理解Android密钥散列是什么 1)据我所知,facebook sdk为开发者提供了一个独特的应用程序id,以了解他们与谁“交谈”,这样他们也可以控制谁是谁之类的人,

  • 我有一个HashMap,其中类的对象(对象1,对象2,对象3)作为键,java.util.Date(日期1,日期2,日期3)作为值。HashMap已经根据值进行了排序,即基于日期对象。键对象具有名为name的属性。 现在,当HashMap的值相同时,即当值的日期相同时,我需要检查键对象的名称(obj.name),并根据键对象的名称属性对HashMap进行排序。请注意,只有当HasHMap和值的日期

  • 主要内容:实例,创建哈希,访问哈希元素,实例,读取哈希值,实例,读取哈希的 key 和 value,实例,实例,检测元素是否存在,实例,获取哈希大小,实例,哈希中添加或删除元素,实例,迭代哈希,实例 - 使用 foreach,实例 - 使用 while哈希是 key/value 对的集合。 Perl中哈希变量以百分号 (%) 标记开始。 访问哈希元素格式:${key}。 以下是一个简单的哈希实例: 实例 #!/usr/bin/perl %data = ('google', 'google.com

  • 哈希是 key/value 对的集合。 Perl中哈希变量以百分号 (%) 标记开始。 访问哈希元素格式:${key}。 以下是一个简单的哈希实例:#!/usr/bin/perl %data = ('google', 'google.com', 'runoob', 'runoob.com', 'taobao', 'taobao.com'); print "\$data{'google'} = $d

  • 问题内容: 假设我有一些新闻存储在哈希中。我有不同的哈希值(每个哈希值代表一个消息): 我想用KEYS命令检索所有键: 密钥未排序的问题: 我想以正确的顺序检索键列表。我不确定哈希是否是我需要的结构。但是,根据redis文档: Redis哈希是字符串字段和字符串值之间的映射,因此它们是表示对象的理想数据类型(例如,具有多个字段(例如名称,姓氏,年龄等)的用户): 将我的新闻对象存储在散列中似乎是个