source: main/trunk/package-kits/scripts/perllib/Hash/Ordered.pm@ 29679

Last change on this file since 29679 was 29679, checked in by Jeremy Symon, 9 years ago

Switch to an Ordered Hash to stop the XML files from being scrambled

File size: 16.9 KB
Line 
1use 5.006;
2use strict;
3use warnings;
4
5package Hash::Ordered;
6# ABSTRACT: A compact, pure-Perl ordered hash class
7our $VERSION = '0.002'; # VERSION
8
9use Carp ();
10use List::Util 1.09 ();
11
12use constant {
13 _DATA => 0,
14 _KEYS => 1,
15};
16
17use overload 'bool' => sub { scalar @{ $_[0]->[_KEYS] } }, fallback => 1;
18
19#pod =method new
20#pod
21#pod $oh = Hash::Ordered->new;
22#pod $oh = Hash::Ordered->new( @pairs );
23#pod
24#pod Constructs an object, with an optional list of key-value pairs.
25#pod
26#pod =cut
27
28sub new {
29 my ( $class, @pairs ) = @_;
30
31 return bless [ {}, [] ], $class unless @pairs;
32
33 Carp::croak("new() requires key-value pairs") unless @pairs % 2 == 0;
34
35 my $self = [ {@pairs}, [ map { $_ % 2 == 0 ? ( $pairs[$_] ) : () } 0 .. $#pairs ] ];
36
37 return bless $self, $class;
38}
39
40#pod =method clone
41#pod
42#pod $oh2 = $oh->clone;
43#pod $oh2 = $oh->clone( @keys );
44#pod
45#pod Creates a shallow copy of an ordered hash object. If no arguments are
46#pod given, it produces an exact copy. If a list of keys is given, the new
47#pod object includes only those keys in the given order. Keys that aren't
48#pod in the original will have the value C<undef>.
49#pod
50#pod =cut
51
52sub clone {
53 my ( $self, @keys ) = @_;
54 my $clone;
55 if (@keys) {
56 my %subhash;
57 @subhash{@keys} = @{ $self->[_DATA] }{@keys};
58 $clone = [ \%subhash, \@keys ];
59 }
60 else {
61 $clone = [ { %{ $self->[_DATA] } }, [ @{ $self->[_KEYS] } ] ];
62 }
63 return bless $clone, ref $self;
64}
65
66#pod =method keys
67#pod
68#pod @keys = $oh->keys;
69#pod
70#pod Returns the ordered list of keys.
71#pod
72#pod =cut
73
74sub keys {
75 my ($self) = @_;
76 return @{ $self->[_KEYS] };
77}
78
79#pod =method values
80#pod
81#pod @values = $oh->values;
82#pod @values = $oh->values( @keys );
83#pod
84#pod Returns an ordered list of values. If no arguments are given, returns
85#pod the ordered values of the entire hash. If a list of keys is given, returns
86#pod values in order corresponding to those keys. If a key does not exist, C<undef>
87#pod will be returned for that value.
88#pod
89#pod =cut
90
91sub values {
92 my ( $self, @keys ) = @_;
93 return map { $self->[_DATA]{$_} } ( @keys ? @keys : @{ $self->[_KEYS] } );
94}
95
96#pod =method get
97#pod
98#pod $value = $oh->get("some key");
99#pod
100#pod Returns the value associated with the key, or C<undef> if it does not exist in
101#pod the hash.
102#pod
103#pod =cut
104
105sub get {
106 my ( $self, $key ) = @_;
107 return $self->[_DATA]{$key};
108}
109
110#pod =method set
111#pod
112#pod $oh->set("some key" => "some value");
113#pod
114#pod Associates a value with a key and returns the value. If the key does not
115#pod already exist in the hash, it will be added at the end.
116#pod
117#pod =cut
118
119sub set {
120 my ( $self, $key, $value ) = @_;
121 if ( !exists $self->[_DATA]{$key} ) {
122 push @{ $self->[_KEYS] }, $key;
123 }
124 return $self->[_DATA]{$key} = $value;
125}
126
127#pod =method exists
128#pod
129#pod if ( $oh->exists("some key") ) { ... }
130#pod
131#pod Test if some key exists in the hash (without creating it).
132#pod
133#pod =cut
134
135sub exists {
136 my ( $self, $key ) = @_;
137 return exists $self->[_DATA]{$key};
138}
139
140#pod =method delete
141#pod
142#pod $value = $oh->delete("some key");
143#pod
144#pod Removes a key-value pair from the hash and returns the value. This
145#pod is expensive, as the ordered list of keys has to be updated.
146#pod
147#pod =cut
148
149sub delete {
150 my ( $self, $key ) = @_;
151 if ( exists $self->[_DATA]{$key} ) {
152 # XXX could put an index on this later if linear search is too slow
153 my $r = $self->[_KEYS];
154 my $i = List::Util::first { $r->[$_] eq $key } 0 .. $#$r;
155 splice @$r, $i, 1;
156 return delete $self->[_DATA]{$key};
157 }
158 return undef; ## no critic
159}
160
161#pod =method push
162#pod
163#pod $oh->push( one => 1, two => 2);
164#pod
165#pod Add a list of key-value pairs to the end of the ordered hash. If a key already
166#pod exists in the hash, it will be deleted and re-inserted at the end with the new
167#pod value.
168#pod
169#pod Returns the number of keys after the push is complete.
170#pod
171#pod =cut
172
173sub push {
174 my ( $self, @pairs ) = @_;
175 while (@pairs) {
176 my ( $k, $v ) = splice( @pairs, 0, 2 );
177 if ( exists $self->[_DATA]{$k} ) {
178 # splice out key
179 # XXX could put an index on this later if linear search is too slow
180 my $r = $self->[_KEYS];
181 my $i = List::Util::first { $r->[$_] eq $k } 0 .. $#$r;
182 splice @$r, $i, 1;
183 }
184 push @{ $self->[_KEYS] }, $k;
185 $self->[_DATA]{$k} = $v;
186 }
187 return scalar @{ $self->[_KEYS] };
188}
189
190#pod =method pop
191#pod
192#pod ($key, $value) = $oh->pop;
193#pod
194#pod Removes and returns the last key-value pair in the ordered hash.
195#pod
196#pod =cut
197
198sub pop {
199 my ($self) = @_;
200 my $key = pop @{ $self->[_KEYS] };
201 return $key, delete $self->[_DATA]{$key};
202}
203
204#pod =method unshift
205#pod
206#pod $oh->unshift( one => 1, two => 2 );
207#pod
208#pod Adds a list of key-value pairs to the beginning of the ordered hash. If a key
209#pod already exists, it will be deleted and re-inserted at the beginning with the
210#pod new value.
211#pod
212#pod Returns the number of keys after the unshift is complete.
213#pod
214#pod =cut
215
216sub unshift {
217 my ( $self, @pairs ) = @_;
218 while (@pairs) {
219 my ( $k, $v ) = splice( @pairs, -2, 2 );
220 if ( exists $self->[_DATA]{$k} ) {
221 # splice out key
222 # XXX could put an index on this later if linear search is too slow
223 my $r = $self->[_KEYS];
224 my $i = List::Util::first { $r->[$_] eq $k } 0 .. $#$r;
225 splice @$r, $i, 1;
226 }
227 unshift @{ $self->[_KEYS] }, $k;
228 $self->[_DATA]{$k} = $v;
229 }
230 return scalar @{ $self->[_KEYS] };
231}
232
233#pod =method shift
234#pod
235#pod ($key, $value) = $oh->shift;
236#pod
237#pod Removes and returns the first key-value pair in the ordered hash.
238#pod
239#pod =cut
240
241sub shift {
242 my ($self) = @_;
243 my $key = shift @{ $self->[_KEYS] };
244 return $key, delete $self->[_DATA]{$key};
245}
246
247#pod =method merge
248#pod
249#pod $oh->merge( one => 1, two => 2 );
250#pod
251#pod Merges a list of key-value pairs into the ordered hash. If a key already
252#pod exists, its value is replaced. Otherwise, the key-value pair is added at
253#pod the end of the hash.
254#pod
255#pod =cut
256
257sub merge {
258 my ( $self, @pairs ) = @_;
259 while (@pairs) {
260 my ( $k, $v ) = splice( @pairs, -2, 2 );
261 if ( !exists $self->[_DATA]{$k} ) {
262 CORE::push @{ $self->[_KEYS] }, $k;
263 }
264 $self->[_DATA]{$k} = $v;
265 }
266 return scalar @{ $self->[_KEYS] };
267}
268
269#pod =method as_list
270#pod
271#pod @pairs = $oh->as_list;
272#pod @pairs = $oh->as_list( @keys );
273#pod
274#pod Returns an ordered list of key-value pairs. If no arguments are given, all
275#pod pairs in the hash are returned. If a list of keys is given, the returned list
276#pod includes only those key-value pairs in the given order. Keys that aren't in
277#pod the original will have the value C<undef>.
278#pod
279#pod =cut
280
281sub as_list {
282 my ( $self, @keys ) = @_;
283 @keys = @{ $self->[_KEYS] } unless @keys;
284 return map { ; $_ => $self->[_DATA]{$_} } @keys;
285}
286
287#pod =method iterator
288#pod
289#pod $iter = $oh->iterator;
290#pod $iter = $oh->iterator( reverse $oh->keys ); # reverse
291#pod
292#pod while ( my ($key,$value) = $iter->() ) { ... }
293#pod
294#pod Returns a code reference that returns a single key-value pair (in order) on
295#pod each invocation, or the empty list if all keys are visited.
296#pod
297#pod If no arguments are given, the iterator walks the entire hash in order. If a
298#pod list of keys is provided, the iterator walks the hash in that order. Unknown
299#pod keys will return C<undef>.
300#pod
301#pod The list of keys to return is set when the iterator is generator. Keys added
302#pod later will not be returned. Delete keys will return C<undef>.
303#pod
304#pod =cut
305
306sub iterator {
307 my ( $self, @keys ) = @_;
308 @keys = @{ $self->[_KEYS] } unless @keys;
309 my $data = $self->[_DATA];
310 return sub {
311 return unless @keys;
312 my $key = CORE::shift(@keys);
313 return ( $key => $data->{$key} );
314 };
315}
316
3171;
318
319
320# vim: ts=4 sts=4 sw=4 et:
321
322__END__
323
324=pod
325
326=encoding UTF-8
327
328=head1 NAME
329
330Hash::Ordered - A compact, pure-Perl ordered hash class
331
332=head1 VERSION
333
334version 0.002
335
336=head1 SYNOPSIS
337
338 use Hash::Ordered;
339
340 my $oh = Hash::Ordered->new( a => 1 );
341
342 $oh->get( 'a' );
343 $oh->set( 'a' => 2 );
344
345 $oh->exists( 'a' );
346 $val = $oh->delete( 'a' );
347
348 @keys = $oh->keys;
349 @vals = $oh->values;
350 @pairs = $oh->as_list
351
352 $oh->push( c => 3, d => 4 );
353 $oh->unshift( e => 5, f => 6 );
354
355 ( $k, $v ) = $oh->pop;
356 ( $k, $v ) = $oh->shift;
357
358 $iter = $oh->iterator;
359 while( ( $k, $v ) = $iter->() ) { ... }
360
361 $copy = $oh->clone;
362 $subset = $oh->clone( qw/c d/ );
363 $reversed = $oh->clone( reverse $oh->keys );
364
365 @value_slice = $oh->values( qw/c f/ ); # qw/3 6/
366 @pairs_slice = $oh->as_list( qw/f e/ ); # qw/f 6 e 5/
367
368=head1 DESCRIPTION
369
370This module implements an ordered hash, meaning that it associates keys with
371values like a Perl hash, but keeps the keys in a consistent order. Because it
372is implemented as an object and manipulated with method calls, it is much
373slower than a Perl hash. This is the cost of keeping order.
374
375=head1 METHODS
376
377=head2 new
378
379 $oh = Hash::Ordered->new;
380 $oh = Hash::Ordered->new( @pairs );
381
382Constructs an object, with an optional list of key-value pairs.
383
384=head2 clone
385
386 $oh2 = $oh->clone;
387 $oh2 = $oh->clone( @keys );
388
389Creates a shallow copy of an ordered hash object. If no arguments are
390given, it produces an exact copy. If a list of keys is given, the new
391object includes only those keys in the given order. Keys that aren't
392in the original will have the value C<undef>.
393
394=head2 keys
395
396 @keys = $oh->keys;
397
398Returns the ordered list of keys.
399
400=head2 values
401
402 @values = $oh->values;
403 @values = $oh->values( @keys );
404
405Returns an ordered list of values. If no arguments are given, returns
406the ordered values of the entire hash. If a list of keys is given, returns
407values in order corresponding to those keys. If a key does not exist, C<undef>
408will be returned for that value.
409
410=head2 get
411
412 $value = $oh->get("some key");
413
414Returns the value associated with the key, or C<undef> if it does not exist in
415the hash.
416
417=head2 set
418
419 $oh->set("some key" => "some value");
420
421Associates a value with a key and returns the value. If the key does not
422already exist in the hash, it will be added at the end.
423
424=head2 exists
425
426 if ( $oh->exists("some key") ) { ... }
427
428Test if some key exists in the hash (without creating it).
429
430=head2 delete
431
432 $value = $oh->delete("some key");
433
434Removes a key-value pair from the hash and returns the value. This
435is expensive, as the ordered list of keys has to be updated.
436
437=head2 push
438
439 $oh->push( one => 1, two => 2);
440
441Add a list of key-value pairs to the end of the ordered hash. If a key already
442exists in the hash, it will be deleted and re-inserted at the end with the new
443value.
444
445Returns the number of keys after the push is complete.
446
447=head2 pop
448
449 ($key, $value) = $oh->pop;
450
451Removes and returns the last key-value pair in the ordered hash.
452
453=head2 unshift
454
455 $oh->unshift( one => 1, two => 2 );
456
457Adds a list of key-value pairs to the beginning of the ordered hash. If a key
458already exists, it will be deleted and re-inserted at the beginning with the
459new value.
460
461Returns the number of keys after the unshift is complete.
462
463=head2 shift
464
465 ($key, $value) = $oh->shift;
466
467Removes and returns the first key-value pair in the ordered hash.
468
469=head2 merge
470
471 $oh->merge( one => 1, two => 2 );
472
473Merges a list of key-value pairs into the ordered hash. If a key already
474exists, its value is replaced. Otherwise, the key-value pair is added at
475the end of the hash.
476
477=head2 as_list
478
479 @pairs = $oh->as_list;
480 @pairs = $oh->as_list( @keys );
481
482Returns an ordered list of key-value pairs. If no arguments are given, all
483pairs in the hash are returned. If a list of keys is given, the returned list
484includes only those key-value pairs in the given order. Keys that aren't in
485the original will have the value C<undef>.
486
487=head2 iterator
488
489 $iter = $oh->iterator;
490 $iter = $oh->iterator( reverse $oh->keys ); # reverse
491
492 while ( my ($key,$value) = $iter->() ) { ... }
493
494Returns a code reference that returns a single key-value pair (in order) on
495each invocation, or the empty list if all keys are visited.
496
497If no arguments are given, the iterator walks the entire hash in order. If a
498list of keys is provided, the iterator walks the hash in that order. Unknown
499keys will return C<undef>.
500
501The list of keys to return is set when the iterator is generator. Keys added
502later will not be returned. Delete keys will return C<undef>.
503
504=head1 OVERLOADING
505
506=head2 Boolean
507
508 if ( $oh ) { ... }
509
510When used in boolean context, a Hash::Ordered object is true if it has any entries
511and false otherwise.
512
513=head1 MOTIVATION
514
515For a long time, I used L<Tie::IxHash> for ordered hashes, but I grew
516frustrated with things it lacked, like a cheap way to copy an IxHash object or
517a convenient iterator when not using the tied interface. As I looked at its
518implementation, it seemed more complex than I though it needed, with an extra
519level of indirection that slows data access.
520
521Given that frustration, I started experimenting with the simplest thing I
522thought could work for an ordered hash: a hash of key-value pairs and an array
523with key order.
524
525As I worked on this, I also started searching for other modules doing similar
526things. What I found fell broadly into two camps: modules based on tie (even
527if they offered an OO interface), and pure OO modules. They all either lacked
528features I deemed necessary or else seemed overly-complex in either
529implementation or API.
530
531Hash::Ordered attempts to find the sweet spot with simple implementation,
532reasonably good efficiency for most common operations, and a rich, intuitive
533API.
534
535=head1 SEE ALSO
536
537This section describes other ordered-hash modules I found on CPAN. For
538benchmarking results, see L<Hash::Ordered::Benchmarks>.
539
540=head2 Tie modules
541
542The following modules offer some sort of tie interface. I don't like ties, in
543general, because of the extra indirection involved over a direct method call, but
544if you are willing to pay that penalty, you might want to try one of these.
545
546L<Tie::IxHash> is probably the most well known and includes an OO API. If its
547warts and performance profile aren't a problem, it might serve.
548
549L<Tie::LLHash> I haven't used, but the linked-list implementation might be
550worthwhile if you expect to do a lot of deletions.
551
552L<Tie::Hash::Indexed> is implemented in XS and thus seems promising if pure-Perl
553isn't a criterion; it often fails tests on Perl 5.18 and above due to the hash
554randomization change.
555
556These other modules have very specific designs/limitations and I didn't find
557any of them suitable for general purpose use:
558
559=over 4
560
561=item *
562
563L<Tie::Array::AsHash> — array elements split with separator; tie API only
564
565=item *
566
567L<Tie::Hash::Array> — ordered alphabetically; tie API only
568
569=item *
570
571L<Tie::InsertOrderHash> — ordered by insertion; tie API only
572
573=item *
574
575L<Tie::StoredOrderHash> — ordered by last update; tie API only
576
577=back
578
579=head2 Other ordered hash modules
580
581Other modules stick with an object-oriented API, with a wide variety of
582implementation approaches.
583
584L<Array::AsHash> is essentially an inverse implementation from Hash::Ordered.
585It keeps pairs in an array and uses a hash to index into the array. I think
586this indirection makes hash-like operations slower, but getting the list of
587pairs back out is much faster. It takes an arrayref to initialize, but can
588shallow copy it if needed. I think this is a reasonable alternative if static
589construction and listing out contents is more common than individual item
590access.
591
592These other modules have restrictions or particularly complicated
593implementations (often relying on C<tie>) and thus I didn't think any of them
594really suitable for use:
595
596=over 4
597
598=item *
599
600L<Array::Assign> — arrays with named access; restricted keys
601
602=item *
603
604L<Array::OrdHash> — overloads array/hash deref and uses internal tied data
605
606=item *
607
608L<Data::Pairs> — array of key-value hashrefs; allows duplicate keys
609
610=item *
611
612L<Data::OMap> — array of key-value hashrefs; no duplicate keys
613
614=item *
615
616L<Data::XHash> — blessed, tied hashref with doubly-linked-list
617
618=back
619
620=for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan
621
622=head1 SUPPORT
623
624=head2 Bugs / Feature Requests
625
626Please report any bugs or feature requests through the issue tracker
627at L<https://github.com/dagolden/Hash-Ordered/issues>.
628You will be notified automatically of any progress on your issue.
629
630=head2 Source Code
631
632This is open source software. The code repository is available for
633public review and contribution under the terms of the license.
634
635L<https://github.com/dagolden/Hash-Ordered>
636
637 git clone https://github.com/dagolden/Hash-Ordered.git
638
639=head1 AUTHOR
640
641David Golden <[email protected]>
642
643=head1 COPYRIGHT AND LICENSE
644
645This software is Copyright (c) 2014 by David Golden.
646
647This is free software, licensed under:
648
649 The Apache License, Version 2.0, January 2004
650
651=cut
Note: See TracBrowser for help on using the repository browser.