File Coverage

blib/lib/Locale/CLDR/Collator.pm
Criterion Covered Total %
statement 67 153 43.7
branch 9 44 20.4
condition 5 27 18.5
subroutine 16 26 61.5
pod 0 13 0.0
total 97 263 36.8


line stmt bran cond sub pod time code
1             package Locale::CLDR::Collator;
2              
3 21     21   173 use version;
  21         62  
  21         222  
4             our $VERSION = version->declare('v0.34.2');
5              
6 21     21   2578 use v5.10.1;
  21         84  
7 21     21   207 use mro 'c3';
  21         87  
  21         273  
8 21     21   848 use utf8;
  21         53  
  21         247  
9 21     21   1005 use if $^V ge v5.12.0, feature => 'unicode_strings';
  21         64  
  21         1123  
10              
11             #line 6538
12             use Unicode::Normalize('NFD');
13             use Unicode::UCD qw( charinfo );
14             use List::MoreUtils qw(pairwise);
15             use Moo;
16             use Types::Standard qw(Str Int Maybe ArrayRef InstanceOf RegexpRef Bool);
17             with 'Locale::CLDR::CollatorBase';
18              
19             my $NUMBER_SORT_TOP = "\x{FD00}\x{0034}";
20             my $LEVEL_SEPARATOR = "\x{0001}";
21              
22             has 'type' => (
23             is => 'ro',
24             isa => Str,
25             default => 'standard',
26             );
27              
28             has 'locale' => (
29             is => 'ro',
30             isa => Maybe[InstanceOf['Locale::CLDR']],
31             default => undef,
32             predicate => 'has_locale',
33             );
34              
35             has 'alternate' => (
36             is => 'ro',
37             isa => Str,
38             default => 'noignore'
39             );
40              
41             # Note that backwards is only at level 2
42             has 'backwards' => (
43             is => 'ro',
44             isa => Str,
45             default => 'false',
46             );
47              
48             has 'case_level' => (
49             is => 'ro',
50             isa => Str,
51             default => 'false',
52             );
53              
54             has 'case_ordering' => (
55             is => 'ro',
56             isa => Str,
57             default => 'false',
58             );
59              
60             has 'normalization' => (
61             is => 'ro',
62             isa => Str,
63             default => 'true',
64             );
65              
66             has 'numeric' => (
67             is => 'ro',
68             isa => Str,
69             default => 'false',
70             );
71              
72             has 'reorder' => (
73             is => 'ro',
74             isa => ArrayRef,
75             default => sub { [] },
76             );
77              
78             has 'strength' => (
79             is => 'ro',
80             isa => Int,
81             default => 3,
82             );
83              
84             has 'max_variable' => (
85             is => 'ro',
86             isa => Str,
87             default => chr(0x0397),
88             );
89              
90             has _character_rx => (
91             is => 'ro',
92             isa => RegexpRef,
93             lazy => 1,
94             init_arg => undef,
95             default => sub {
96             my $self = shift;
97             my $list = join '|', @{$self->multi_rx()}, '.';
98             return qr/\G($list)/s;
99             },
100             );
101              
102             has _in_variable_weigting => (
103             is => 'rw',
104             isa => Bool,
105             init_arg => undef,
106             default => 0,
107             );
108              
109             # Set up the locale overrides
110             sub BUILD {
111             my $self = shift;
112            
113             my $overrides = [];
114             if ($self->has_locale) {
115             $overrides = $self->locale->_collation_overrides($self->type);
116             }
117            
118             foreach my $override (@$overrides) {
119             $self->_set_ce(@$override);
120             }
121             }
122              
123             # Get the collation element at the current strength
124             sub get_collation_elements {
125             my ($self, $string) = @_;
126             my @ce;
127             if ($self->numeric eq 'true' && $string =~/^\p{Nd}^/) {
128             my $numeric_top = $self->collation_elements()->{$NUMBER_SORT_TOP};
129             my @numbers = $self->_convert_digits_to_numbers($string);
130             @ce = map { "$numeric_top${LEVEL_SEPARATOR}№$_" } @numbers;
131             }
132             else {
133             my $rx = $self->_character_rx;
134             my @characters = $string =~ /$rx/g;
135            
136             foreach my $character (@characters) {
137             my @current_ce;
138             if (length $character > 1) {
139             # We have a collation element that dependeds on two or more codepoints
140             # Remove the code points that the collation element depends on and if
141             # there are still codepoints get the collation elements for them
142             my @multi_rx = @{$self->multi_rx};
143             my $multi;
144             for (my $count = 0; $count < @multi_rx; $count++) {
145             if ($character =~ /$multi_rx[$count]/) {
146             $multi = $self->multi_class()->[$count];
147             last;
148             }
149             }
150            
151             my $match = $character;
152             eval "\$match =~ tr/$multi//cd;";
153             push @current_ce, $self->collation_elements()->{$match};
154             $character =~ s/$multi//g;
155             if (length $character) {
156             foreach my $codepoint (split //, $character) {
157             push @current_ce,
158             $self->collation_elements()->{$codepoint}
159             // $self->generate_ce($codepoint);
160             }
161             }
162             }
163             else {
164             my $ce = $self->collation_elements()->{$character};
165             $ce //= $self->generate_ce($character);
166             push @current_ce, $ce;
167             }
168             push @ce, $self->_process_variable_weightings(@current_ce);
169             }
170             }
171             return @ce;
172             }
173              
174             sub _process_variable_weightings {
175             my ($self, @ce) = @_;
176             return @ce if $self->alternate() eq 'noignore';
177            
178             foreach my $ce (@ce) {
179             if ($ce->[0] le $self->max_variable) {
180             # Variable waighted codepoint
181             if ($self->alternate eq 'blanked') {
182             @$ce = map { chr() } qw(0 0 0);
183            
184             }
185             if ($self->alternate eq 'shifted') {
186             my $l4;
187             if ($ce->[0] eq "\0" && $ce->[1] eq "\0" && $ce->[2] eq "\0") {
188             $ce->[3] = "\0";
189             }
190             else {
191             $ce->[3] = $ce->[1];
192             }
193             @$ce[0 .. 2] = map { chr() } qw (0 0 0);
194             }
195             $self->_in_variable_weigting(1);
196             }
197             else {
198             if ($self->_in_variable_weigting()) {
199             if( $ce->[0] eq "\0" && $self->alternate eq 'shifted' ) {
200             $ce->[3] = "\0";
201             }
202             elsif($ce->[0] ne "\0") {
203             $self->_in_variable_weigting(0);
204             if ( $self->alternate eq 'shifted' ) {
205             $ce->[3] = chr(0xFFFF)
206             }
207             }
208             }
209             }
210             }
211             }
212              
213             # Converts $string into a sort key. Two sort keys can be correctly sorted by cmp
214             sub getSortKey {
215             my ($self, $string) = @_;
216              
217             $string = NFD($string) if $self->normalization eq 'true';
218              
219             my @sort_key;
220            
221             my @ce = $self->get_collation_elements($string);
222              
223             for (my $count = 0; $count < $self->strength(); $count++ ) {
224             foreach my $ce (@ce) {
225             $ce = [ split //, $ce] unless ref $ce;
226             if (defined $ce->[$count] && $ce->[$count] ne "\0") {
227             push @sort_key, $ce->[$count];
228             }
229             }
230             }
231            
232             return join "\0", @sort_key;
233             }
234              
235             sub generate_ce {
236             my ($self, $character) = @_;
237            
238             my $aaaa;
239             my $bbbb;
240            
241             if ($^V ge v5.26 && eval q($character =~ /(?!\p{Cn})(?:\p{Block=Tangut}|\p{Block=Tangut_Components})/)) {
242             $aaaa = 0xFB00;
243             $bbbb = (ord($character) - 0x17000) | 0x8000;
244             }
245             # Block Nushu was added in Perl 5.28
246             elsif ($^V ge v5.28 && eval q($character =~ /(?!\p{Cn})\p{Block=Nushu}/)) {
247             $aaaa = 0xFB01;
248             $bbbb = (ord($character) - 0x1B170) | 0x8000;
249             }
250             elsif ($character =~ /(?=\p{Unified_Ideograph=True})(?:\p{Block=CJK_Unified_Ideographs}|\p{Block=CJK_Compatibility_Ideographs})/) {
251             $aaaa = 0xFB40 + (ord($character) >> 15);
252             $bbbb = (ord($character) & 0x7FFFF) | 0x8000;
253             }
254             elsif ($character =~ /(?=\p{Unified_Ideograph=True})(?!\p{Block=CJK_Unified_Ideographs})(?!\p{Block=CJK_Compatibility_Ideographs})/) {
255             $aaaa = 0xFB80 + (ord($character) >> 15);
256             $bbbb = (ord($character) & 0x7FFFF) | 0x8000;
257             }
258             else {
259             $aaaa = 0xFBC0 + (ord($character) >> 15);
260             $bbbb = (ord($character) & 0x7FFFF) | 0x8000;
261             }
262             return join '', map {chr($_)} $aaaa, 0x0020, 0x0002, ord($LEVEL_SEPARATOR), $bbbb, 0, 0;
263             }
264              
265             # sorts a list according to the locales collation rules
266             sub sort {
267             my $self = shift;
268            
269             return map { $_->[0]}
270             sort { $a->[1] cmp $b->[1] }
271             map { [$_, $self->getSortKey($_)] }
272             @_;
273             }
274              
275             sub cmp {
276             my ($self, $a, $b) = @_;
277            
278             return $self->getSortKey($a) cmp $self->getSortKey($b);
279             }
280              
281             sub eq {
282             my ($self, $a, $b) = @_;
283            
284             return $self->getSortKey($a) eq $self->getSortKey($b);
285             }
286              
287             sub ne {
288             my ($self, $a, $b) = @_;
289            
290             return $self->getSortKey($a) ne $self->getSortKey($b);
291             }
292              
293             sub lt {
294             my ($self, $a, $b) = @_;
295            
296             return $self->getSortKey($a) lt $self->getSortKey($b);
297             }
298              
299             sub le {
300             my ($self, $a, $b) = @_;
301            
302             return $self->getSortKey($a) le $self->getSortKey($b);
303             }
304             sub gt {
305             my ($self, $a, $b) = @_;
306            
307             return $self->getSortKey($a) gt $self->getSortKey($b);
308             }
309              
310             sub ge {
311             my ($self, $a, $b) = @_;
312            
313             return $self->getSortKey($a) ge $self->getSortKey($b);
314             }
315              
316             # Get Human readable sort key
317             sub viewSortKey {
318             my ($self, $sort_key) = @_;
319            
320             my @levels = split/\x0/, $sort_key;
321            
322             foreach my $level (@levels) {
323             $level = join ' ', map { sprintf '%0.4X', ord } split //, $level;
324             }
325            
326             return '[ ' . join (' | ', @levels) . ' ]';
327             }
328              
329             sub _convert_digits_to_numbers {
330             my ($self, $digits) = @_;
331             my @numbers = ();
332             my $script = '';
333             foreach my $number (split //, $digits) {
334             my $char_info = charinfo(ord($number));
335             my ($decimal, $chr_script) = @{$char_info}{qw( decimal script )};
336             if ($chr_script eq $script) {
337             $numbers[-1] *= 10;
338             $numbers[-1] += $decimal;
339             }
340             else {
341             push @numbers, $decimal;
342             $script = $chr_script;
343             }
344             }
345             return @numbers;
346             }
347              
348             no Moo;
349              
350             1;
351              
352             # vim: tabstop=4