blib/lib/Lingua/PTD.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 27 | 29 | 93.1 |
branch | n/a | ||
condition | n/a | ||
subroutine | 10 | 10 | 100.0 |
pod | n/a | ||
total | 37 | 39 | 94.8 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package Lingua::PTD; | ||||||
2 | $Lingua::PTD::VERSION = '1.14'; | ||||||
3 | 1 | 1 | 21119 | use 5.010; | |||
1 | 4 | ||||||
4 | |||||||
5 | 1 | 1 | 696 | use parent 'Exporter'; | |||
1 | 293 | ||||||
1 | 6 | ||||||
6 | our @EXPORT = 'toentry'; | ||||||
7 | our @EXPORT_OK = qw/bws ucts/; | ||||||
8 | 1 | 1 | 69 | use warnings; | |||
1 | 6 | ||||||
1 | 28 | ||||||
9 | 1 | 1 | 5 | use strict; | |||
1 | 1 | ||||||
1 | 20 | ||||||
10 | |||||||
11 | 1 | 1 | 1012 | use utf8; | |||
1 | 10 | ||||||
1 | 5 | ||||||
12 | |||||||
13 | 1 | 1 | 745 | use Unicode::CaseFold; | |||
1 | 926 | ||||||
1 | 61 | ||||||
14 | |||||||
15 | 1 | 1 | 821 | use Time::HiRes; | |||
1 | 1614 | ||||||
1 | 5 | ||||||
16 | 1 | 1 | 621 | use Lingua::PTD::Dumper; | |||
1 | 3 | ||||||
1 | 49 | ||||||
17 | 1 | 1 | 505 | use Lingua::PTD::BzDmp; | |||
1 | 3 | ||||||
1 | 53 | ||||||
18 | 1 | 1 | 543 | use Lingua::PTD::XzDmp; | |||
0 | |||||||
0 | |||||||
19 | use Lingua::PTD::SQLite; | ||||||
20 | use Lingua::PTD::TSV; | ||||||
21 | use Lingua::PTD::StarDict; | ||||||
22 | |||||||
23 | =encoding UTF-8 | ||||||
24 | |||||||
25 | =head1 NAME | ||||||
26 | |||||||
27 | Lingua::PTD - Module to handle PTD files in Dumper Format | ||||||
28 | |||||||
29 | =head1 SYNOPSIS | ||||||
30 | |||||||
31 | use Lingua::PTD; | ||||||
32 | |||||||
33 | $ptd = Lingua::PTD->new( $ptd_file ); | ||||||
34 | |||||||
35 | =head1 DESCRIPTION | ||||||
36 | |||||||
37 | PTD files in Perl Dumper format are simple hashes references. But they | ||||||
38 | use a specific structure, and this module provides a simple interface to | ||||||
39 | manipulate it. | ||||||
40 | |||||||
41 | =head2 C |
||||||
42 | |||||||
43 | The C |
||||||
44 | receives a PTD file in dumper format. | ||||||
45 | |||||||
46 | my $ptd = Lingua::PTD->new( $ptd_file ); | ||||||
47 | |||||||
48 | If the filename matches with C<< /dmp.bz2$/ >> (that is, ends in | ||||||
49 | dmp.bz2) it is considered to be a bzip2 file and will be decompressed | ||||||
50 | in the fly. | ||||||
51 | |||||||
52 | If it ends in C<<.sqlite>>, then it is supposed to contain an SQLite | ||||||
53 | file with the dictionary (with Lingua::PTD standard schema!). | ||||||
54 | |||||||
55 | Extra arguments are a flatenned hash with configuration | ||||||
56 | variables. Following options are recognized: | ||||||
57 | |||||||
58 | =over 4 | ||||||
59 | |||||||
60 | =item C |
||||||
61 | |||||||
62 | Sets verbosity. | ||||||
63 | |||||||
64 | my $ptd = Lingua::PTD->new( $ptd_file, verbose => 1 ); | ||||||
65 | |||||||
66 | =back | ||||||
67 | |||||||
68 | =cut | ||||||
69 | |||||||
70 | sub new { | ||||||
71 | my ($class, $filename, %ops) = @_; | ||||||
72 | die "Can't find ptd [$filename]\n" unless -f $filename; | ||||||
73 | |||||||
74 | my $self; | ||||||
75 | # switch | ||||||
76 | $self = Lingua::PTD::Dumper->new($filename) if $filename =~ /\.dmp$/i; | ||||||
77 | $self = Lingua::PTD::BzDmp ->new($filename) if $filename =~ /\.dmp\.bz2$/i; | ||||||
78 | $self = Lingua::PTD::XzDmp ->new($filename) if $filename =~ /\.dmp\.xz$/i; | ||||||
79 | $self = Lingua::PTD::SQLite->new($filename) if $filename =~ /\.sqlite$/i; | ||||||
80 | |||||||
81 | # default | ||||||
82 | $self = Lingua::PTD::Dumper->new($filename) unless $self; | ||||||
83 | |||||||
84 | $self->_calculate_sizes() unless $self->size; # in case it is already calculated | ||||||
85 | |||||||
86 | # configuration variables | ||||||
87 | $self->verbose($ops{verbose}) if exists $ops{verbose}; | ||||||
88 | |||||||
89 | return $self; | ||||||
90 | } | ||||||
91 | |||||||
92 | =head2 C |
||||||
93 | |||||||
94 | With no arguments returns if the methods are configured to use verbose | ||||||
95 | mode, or not. If an argument is supplied, it is interpreted as a | ||||||
96 | boolean value, and sets methods verbosity. | ||||||
97 | |||||||
98 | $ptd->verbose(1); | ||||||
99 | |||||||
100 | =cut | ||||||
101 | |||||||
102 | sub verbose { | ||||||
103 | my $self = shift; | ||||||
104 | if (defined($_[0])) { | ||||||
105 | $self->{' verbose '} = shift | ||||||
106 | } else { | ||||||
107 | $self->{' verbose '} || 0 | ||||||
108 | } | ||||||
109 | } | ||||||
110 | |||||||
111 | =head2 C |
||||||
112 | |||||||
113 | The C |
||||||
114 | taking care to sort words lexicographically, and sorting translations by | ||||||
115 | their probability (starting with higher probabilities). | ||||||
116 | |||||||
117 | The format is Perl code, and thus, can be used independetly of this module. | ||||||
118 | |||||||
119 | $ptd->dump; | ||||||
120 | |||||||
121 | Note that the C |
||||||
122 | |||||||
123 | =cut | ||||||
124 | |||||||
125 | sub dump { | ||||||
126 | my $self = shift; | ||||||
127 | |||||||
128 | binmode STDOUT, ":utf8"; | ||||||
129 | print "use utf8;\n"; | ||||||
130 | print "\$a = {\n"; | ||||||
131 | $self->downtr( | ||||||
132 | sub { | ||||||
133 | my ($w,$c,%t) = @_; | ||||||
134 | printf " '%s' => {\n", _protect_quotes($w); | ||||||
135 | printf " count => %d,\n", $c; | ||||||
136 | printf " trans => {\n"; | ||||||
137 | for my $t (sort { $t{$b} <=> $t{$a} } keys %t) { | ||||||
138 | printf " '%s' => %.6f,\n", _protect_quotes($t), $t{$t}; | ||||||
139 | } | ||||||
140 | printf " }\n"; | ||||||
141 | printf " },\n"; | ||||||
142 | }, | ||||||
143 | sorted => 1, | ||||||
144 | task => 'dump', | ||||||
145 | ); | ||||||
146 | print "}\n"; | ||||||
147 | } | ||||||
148 | |||||||
149 | =head2 C |
||||||
150 | |||||||
151 | The C |
||||||
152 | words of the dictionary: its domain. Pass a true value as argument and | ||||||
153 | the list is returned sorted. | ||||||
154 | |||||||
155 | my @words = $ptd->words; | ||||||
156 | |||||||
157 | =cut | ||||||
158 | |||||||
159 | sub words { | ||||||
160 | my $self = shift; | ||||||
161 | my $sorted = shift; | ||||||
162 | if ($sorted) { | ||||||
163 | return sort grep {!/^ /} keys %$self; | ||||||
164 | } else { | ||||||
165 | return grep {!/^ /} keys %$self; | ||||||
166 | } | ||||||
167 | } | ||||||
168 | |||||||
169 | =head2 C |
||||||
170 | |||||||
171 | The C |
||||||
172 | translations. | ||||||
173 | |||||||
174 | my @translations = $ptd->trans( "dog" ); | ||||||
175 | |||||||
176 | =cut | ||||||
177 | |||||||
178 | sub trans { | ||||||
179 | my ($self, $word, $trans) = @_; | ||||||
180 | return () unless exists $self->{$word}; | ||||||
181 | if ($trans) { | ||||||
182 | return (exists($self->{$word}{trans}{$trans}))?1:0; | ||||||
183 | } else { | ||||||
184 | return keys %{$self->{$word}{trans}}; | ||||||
185 | } | ||||||
186 | } | ||||||
187 | |||||||
188 | |||||||
189 | =head2 C |
||||||
190 | |||||||
191 | Checks if a word is in a dictionary | ||||||
192 | |||||||
193 | =cut | ||||||
194 | |||||||
195 | sub exists { | ||||||
196 | my ($self, $word ) = @_; | ||||||
197 | return exists $self->{$word}; | ||||||
198 | } | ||||||
199 | |||||||
200 | =head2 C |
||||||
201 | |||||||
202 | The C |
||||||
203 | keys are the its possible translations, and values the corresponding | ||||||
204 | translation probabilities. | ||||||
205 | |||||||
206 | my %trans = $ptd->transHash( "dog" ); | ||||||
207 | |||||||
208 | Returns the empty hash if the word does not exist. | ||||||
209 | |||||||
210 | =cut | ||||||
211 | |||||||
212 | sub transHash { | ||||||
213 | my ($self, $word) = @_; | ||||||
214 | my %h = (); | ||||||
215 | for my $t ($self->trans($word)) { | ||||||
216 | $h{$t} = $self->prob($word, $t); | ||||||
217 | } | ||||||
218 | return %h; | ||||||
219 | } | ||||||
220 | |||||||
221 | =head2 C |
||||||
222 | |||||||
223 | The C |
||||||
224 | probability of that word being translated that way. | ||||||
225 | |||||||
226 | my $probability = $ptd->prob("cat", "gato"); | ||||||
227 | |||||||
228 | =cut | ||||||
229 | |||||||
230 | sub prob { | ||||||
231 | my ($self, $word, $trad) = @_; | ||||||
232 | return 0 unless exists $self->{$word}{trans}{$trad}; | ||||||
233 | return $self->{$word}{trans}{$trad}; | ||||||
234 | } | ||||||
235 | |||||||
236 | =head2 C |
||||||
237 | |||||||
238 | Returns the total number of words from the source-corpus that originated | ||||||
239 | the PTD. Basically, the sum of the C |
||||||
240 | |||||||
241 | my $size = $ptd->size; | ||||||
242 | |||||||
243 | =cut | ||||||
244 | |||||||
245 | sub size { | ||||||
246 | return $_[0]->{' size '}; # space is relevant | ||||||
247 | } | ||||||
248 | |||||||
249 | =head2 C |
||||||
250 | |||||||
251 | The C |
||||||
252 | that word. | ||||||
253 | |||||||
254 | my $count = $ptd->count("cat"); | ||||||
255 | |||||||
256 | If no argument is supplied, returns the total dictionary count (sum of | ||||||
257 | all words). | ||||||
258 | |||||||
259 | =cut | ||||||
260 | |||||||
261 | sub count { | ||||||
262 | my ($self, $word) = @_; | ||||||
263 | if (defined($word)) { | ||||||
264 | if (exists($self->{$word})) { | ||||||
265 | return $self->{$word}{count} | ||||||
266 | } else { | ||||||
267 | return 0; | ||||||
268 | } | ||||||
269 | } else { | ||||||
270 | return $self->{" count "}; | ||||||
271 | } | ||||||
272 | } | ||||||
273 | |||||||
274 | =head2 C |
||||||
275 | |||||||
276 | Computes a bunch of statistics about the PTD and returns them in an | ||||||
277 | hash reference. | ||||||
278 | |||||||
279 | =cut | ||||||
280 | |||||||
281 | sub stats { | ||||||
282 | my $self = shift; | ||||||
283 | my $stats = { | ||||||
284 | size => $self->size, | ||||||
285 | count => $self->count, | ||||||
286 | }; | ||||||
287 | |||||||
288 | $self->downtr( sub { | ||||||
289 | my ($w, $c, %t) = @_; | ||||||
290 | $c ||= 1; | ||||||
291 | $stats->{avgTransNr} += scalar(keys %t); | ||||||
292 | $stats->{occTotal} += $c; | ||||||
293 | if (!$stats->{occMin} || $stats->{occMin} > $c) { | ||||||
294 | $stats->{occMin} = $c; | ||||||
295 | $stats->{occMinWord} = $w; | ||||||
296 | } | ||||||
297 | if (!$stats->{occMax} || $stats->{occMax} < $c) { | ||||||
298 | $stats->{occMax} = $c; | ||||||
299 | $stats->{occMaxWord} = $w; | ||||||
300 | } | ||||||
301 | if (%t) { | ||||||
302 | my ($bestProb) = sort { $b <=> $a } values %t; | ||||||
303 | if (!$stats->{probMax} || $stats->{probMax} < $bestProb) { | ||||||
304 | $stats->{probMax} = $bestProb; | ||||||
305 | } | ||||||
306 | if (!$stats->{probMin} || $stats->{probMin} > $bestProb) { | ||||||
307 | $stats->{probMin} = $bestProb; | ||||||
308 | } | ||||||
309 | $stats->{avgBestTrans} += $bestProb; | ||||||
310 | } | ||||||
311 | }, | ||||||
312 | task => 'stats'); | ||||||
313 | $stats->{avgTransNr} /= $stats->{count}; | ||||||
314 | $stats->{avgBestTrans} /= $stats->{count}; | ||||||
315 | $stats->{avgOcc} = $stats->{occTotal} / $stats->{count}; | ||||||
316 | return $stats; | ||||||
317 | |||||||
318 | } | ||||||
319 | |||||||
320 | =head2 C |
||||||
321 | |||||||
322 | This method subtracts to the domain of a PTD, the elements present on | ||||||
323 | a set of elements. This set can be defines as another PTD (domain is | ||||||
324 | used), as a Perl array reference, as a Perl hash reference (domain is | ||||||
325 | used) or as a Perl array (not reference). Returns the dictionary after | ||||||
326 | domain subtraction takes place. | ||||||
327 | |||||||
328 | # removes portuguese articles from the dictionary | ||||||
329 | $ptd->subtractDomain( qw.o a os as. ); | ||||||
330 | |||||||
331 | # removes a set of stop words from the dictionary | ||||||
332 | $ptd->subtractDomain( \@stopWords ); | ||||||
333 | |||||||
334 | # removes the words present on other_ptd from ptd | ||||||
335 | $ptd->subtractDomain( $other_ptd ); | ||||||
336 | |||||||
337 | =cut | ||||||
338 | |||||||
339 | sub subtractDomain { | ||||||
340 | my ($self, $other, @more) = @_; | ||||||
341 | |||||||
342 | my @domain; | ||||||
343 | if (ref($other) =~ /Lingua::PTD/ and $other->isa("Lingua::PTD")) { | ||||||
344 | @domain = $other->words; | ||||||
345 | } | ||||||
346 | elsif (ref($other) eq "ARRAY") { | ||||||
347 | @domain = @$other | ||||||
348 | } | ||||||
349 | elsif (ref($other) eq "HASH") { | ||||||
350 | @domain = keys %$other | ||||||
351 | } | ||||||
352 | else { | ||||||
353 | @domain = ($other, @more); | ||||||
354 | } | ||||||
355 | my %domain; | ||||||
356 | @domain{@domain} = @domain; | ||||||
357 | |||||||
358 | $self -> downtr ( | ||||||
359 | sub { | ||||||
360 | my ($w,$c,%t) = @_; | ||||||
361 | return exists($domain{$w}) ? undef : toentry($w,$c,%t) | ||||||
362 | }, | ||||||
363 | filter => 1, | ||||||
364 | task => 'subtractDomain', | ||||||
365 | ); | ||||||
366 | $self->_calculate_sizes(); | ||||||
367 | return $self; | ||||||
368 | } | ||||||
369 | |||||||
370 | |||||||
371 | |||||||
372 | =head2 C |
||||||
373 | |||||||
374 | Domain restrict function: interface is similar to subtractDomain function | ||||||
375 | |||||||
376 | This method restricts the domain of a PTD to a set of elements. This | ||||||
377 | set can be defines as another PTD (domain is used), as a Perl array | ||||||
378 | reference, as a Perl hash reference (domain is used) or as a Perl | ||||||
379 | array (not reference). Returns the dictionary after domain restriction | ||||||
380 | takes place. | ||||||
381 | |||||||
382 | # restrict the dictionary to a set of words | ||||||
383 | $ptd->restrictDomain( \@someWords ); | ||||||
384 | |||||||
385 | =cut | ||||||
386 | |||||||
387 | sub restrictDomain { | ||||||
388 | my ($self, $other, @more) = @_; | ||||||
389 | |||||||
390 | my @domain; | ||||||
391 | if (ref($other) =~ /Lingua::PTD/ and $other->isa("Lingua::PTD")) { | ||||||
392 | @domain = $other->words; | ||||||
393 | } | ||||||
394 | elsif (ref($other) eq "ARRAY") { | ||||||
395 | @domain = @$other | ||||||
396 | } | ||||||
397 | elsif (ref($other) eq "HASH") { | ||||||
398 | @domain = keys %$other | ||||||
399 | } | ||||||
400 | else { | ||||||
401 | @domain = ($other, @more); | ||||||
402 | } | ||||||
403 | my %domain; | ||||||
404 | @domain{@domain} = @domain; | ||||||
405 | |||||||
406 | $self -> downtr ( | ||||||
407 | sub { | ||||||
408 | my ($w,$c,%t) = @_; | ||||||
409 | return exists($domain{$w}) ? toentry($w,$c,%t):undef | ||||||
410 | }, | ||||||
411 | filter => 1, | ||||||
412 | task => 'restrictDomain', | ||||||
413 | ); | ||||||
414 | $self->_calculate_sizes(); | ||||||
415 | return $self; | ||||||
416 | } | ||||||
417 | |||||||
418 | =head2 C |
||||||
419 | |||||||
420 | This method recalculates all probabilities accordingly with the number | ||||||
421 | of translations available. | ||||||
422 | |||||||
423 | For instance, if you have | ||||||
424 | |||||||
425 | home => casa => 25% | ||||||
426 | => lar => 25% | ||||||
427 | |||||||
428 | The resulting dictionary will have | ||||||
429 | |||||||
430 | home => casa => 50% | ||||||
431 | => lar => 50% | ||||||
432 | |||||||
433 | Note that this methods B |
||||||
434 | |||||||
435 | =cut | ||||||
436 | |||||||
437 | sub reprob { | ||||||
438 | my $self = shift; | ||||||
439 | $self->downtr( | ||||||
440 | sub { | ||||||
441 | my ($w, $c, %t) = @_; | ||||||
442 | my $actual = 0; | ||||||
443 | $actual += $t{$_} for (keys %t); | ||||||
444 | return undef unless $actual > 0.1; | ||||||
445 | $t{$_} /= $actual for (keys %t); | ||||||
446 | return toentry($w, $c, %t); | ||||||
447 | }, | ||||||
448 | filter => 1, | ||||||
449 | task => 'reprob' | ||||||
450 | ); | ||||||
451 | return $self; | ||||||
452 | } | ||||||
453 | |||||||
454 | =head2 C |
||||||
455 | |||||||
456 | This method intersects the current object with the supplied PTD. | ||||||
457 | Note that this method B |
||||||
458 | |||||||
459 | Occurrences count in the final dictionary is the minimum occurrence | ||||||
460 | value of the two dictionaries. | ||||||
461 | |||||||
462 | Only translations present on both dictionary are kept. The probability | ||||||
463 | will be the minimum on the two dictionaries. | ||||||
464 | |||||||
465 | =cut | ||||||
466 | |||||||
467 | sub intersect { | ||||||
468 | my ($self, $other) = @_; | ||||||
469 | |||||||
470 | $self->downtr | ||||||
471 | ( | ||||||
472 | sub { | ||||||
473 | my ($w, $c, %t) = @_; | ||||||
474 | if ($other->trans($w)) { | ||||||
475 | $c = _min($c, $other->count($w)); | ||||||
476 | for my $t (keys %t) { | ||||||
477 | if ($other->trans($w,$t)) { | ||||||
478 | $t{$t} = _min($t{$t}, $other->trans($w,$t)); | ||||||
479 | } | ||||||
480 | else { | ||||||
481 | delete($t{$t}); | ||||||
482 | } | ||||||
483 | } | ||||||
484 | return toentry($w, $c, %t); | ||||||
485 | } else { | ||||||
486 | return undef; | ||||||
487 | } | ||||||
488 | }, | ||||||
489 | filter => 1, | ||||||
490 | task => 'intersect', | ||||||
491 | ); | ||||||
492 | $self->_calculate_sizes(); | ||||||
493 | } | ||||||
494 | |||||||
495 | sub _set_word_translation { | ||||||
496 | my ($self, $word, $translation, $probability) = @_; | ||||||
497 | $self->{$word}{trans}{$translation} = $probability; | ||||||
498 | } | ||||||
499 | |||||||
500 | sub _delete_word_translation { | ||||||
501 | my ($self, $word, $translation) = @_; | ||||||
502 | delete($self->{$word}{trans}{$translation}); | ||||||
503 | } | ||||||
504 | |||||||
505 | sub _set_word_count { | ||||||
506 | my ($self, $word, $count) = @_; | ||||||
507 | $self->{$word}{count} = $count; | ||||||
508 | } | ||||||
509 | |||||||
510 | sub _delete_word { | ||||||
511 | my ($self, $word) = @_; | ||||||
512 | delete $self->{$word}; | ||||||
513 | } | ||||||
514 | |||||||
515 | =head2 C |
||||||
516 | |||||||
517 | This method adds the current PTD with the supplied one (first | ||||||
518 | argument). Note that this method B |
||||||
519 | |||||||
520 | =cut | ||||||
521 | |||||||
522 | sub add { | ||||||
523 | my ($self, $other, %ops) = @_; | ||||||
524 | |||||||
525 | $ops{verbose} //= $self->verbose; | ||||||
526 | |||||||
527 | my ($S1,$S2) = ($self->size, $other->size); | ||||||
528 | |||||||
529 | $other->_init_transaction; | ||||||
530 | $self->downtr(sub { | ||||||
531 | my ($w, $c, %t) = @_; | ||||||
532 | if ($other->trans($w)) { | ||||||
533 | my ($c1, $c2) = ($c, $other->count($w)); | ||||||
534 | for my $t (_uniq(keys %t, $other->trans($w))) { | ||||||
535 | my ($p1, $p2) = ($t{$t} || 0, $other->prob($w,$t)); | ||||||
536 | my ($w1, $w2) = ($c1 * $S2, $c2 * $S1); | ||||||
537 | if ($w1 + $w2) { | ||||||
538 | $t{$t} = ($w1 * $p1 + $w2 * $p2)/($w1 + $w2); | ||||||
539 | } else { | ||||||
540 | delete $t{$t}; | ||||||
541 | } | ||||||
542 | } | ||||||
543 | toentry($w, $c1+$c2, %t); | ||||||
544 | } else { | ||||||
545 | toentry($w,$c,%t); | ||||||
546 | } | ||||||
547 | }, | ||||||
548 | filter => 1, | ||||||
549 | task => 'add', | ||||||
550 | verbose => $ops{verbose}, | ||||||
551 | ); | ||||||
552 | $other->_commit; | ||||||
553 | |||||||
554 | $self->_init_transaction; | ||||||
555 | print STDERR "\tAdding new words\n" if $ops{verbose}; | ||||||
556 | $other->downtr(sub { | ||||||
557 | my ($w, $c, %t) = @_; | ||||||
558 | return if $self->trans($w); | ||||||
559 | $self->_set_word_count($w, $c); | ||||||
560 | for my $t (keys %t) { | ||||||
561 | $self->_set_word_translation($w, $t, $t{$t}); | ||||||
562 | } | ||||||
563 | }, | ||||||
564 | task => 'add', | ||||||
565 | verbose => $ops{verbose}, | ||||||
566 | ); | ||||||
567 | $self->_commit; | ||||||
568 | $self->_calculate_sizes(); | ||||||
569 | return $self; | ||||||
570 | } | ||||||
571 | |||||||
572 | sub _uniq { | ||||||
573 | my %f; | ||||||
574 | $f{$_}++ for @_; | ||||||
575 | return keys %f; | ||||||
576 | } | ||||||
577 | |||||||
578 | =head2 C |
||||||
579 | |||||||
580 | This method iterates over a dictionary and calls the function supplied | ||||||
581 | as argument. This function will receive, in each call, the word in the | ||||||
582 | source language, the number of occurrences, and the hash of | ||||||
583 | translations. | ||||||
584 | |||||||
585 | $ptd->downtr( sub { my ($w,$c,%t) = @_; | ||||||
586 | if ($w =~ /[^A-Za-z0-9]/) { | ||||||
587 | return undef; | ||||||
588 | } else { | ||||||
589 | return toentry($w,$c,%t); | ||||||
590 | } | ||||||
591 | }, | ||||||
592 | filter => 1); | ||||||
593 | |||||||
594 | Set the filter flag if your downtr function is replacing the original | ||||||
595 | dictionary. | ||||||
596 | |||||||
597 | =cut | ||||||
598 | |||||||
599 | sub _init_transaction { } | ||||||
600 | sub _commit { } | ||||||
601 | |||||||
602 | sub downtr { | ||||||
603 | my ($self, $sub, %opt) = @_; | ||||||
604 | |||||||
605 | $opt{verbose} //= $self->verbose; | ||||||
606 | $opt{task} ||= $self->{' task '} || "downtr"; | ||||||
607 | |||||||
608 | my $time = [Time::HiRes::gettimeofday]; | ||||||
609 | my $counter = 0; | ||||||
610 | $self->_init_transaction; | ||||||
611 | |||||||
612 | my @keys = $opt{sorted} ? $self->words(1) : $self->words; | ||||||
613 | for my $word (@keys) { | ||||||
614 | my $res = $sub->($word, | ||||||
615 | $self->count($word), | ||||||
616 | $self->transHash($word)); | ||||||
617 | if ($opt{filter}) { | ||||||
618 | if (!defined($res)) { | ||||||
619 | $self->_delete_word($word) | ||||||
620 | } else { | ||||||
621 | $self->_update_word($word, $res); | ||||||
622 | } | ||||||
623 | } | ||||||
624 | |||||||
625 | $counter ++; | ||||||
626 | print STDERR "\r[$opt{task}]\tProcessing ($counter entries)..." if $opt{verbose} && !($counter%100); | ||||||
627 | } | ||||||
628 | $self->_commit; | ||||||
629 | $self->_calculate_sizes() if $opt{filter}; | ||||||
630 | |||||||
631 | my $elapsed = Time::HiRes::tv_interval($time); | ||||||
632 | printf STDERR "\r[$opt{task}]\tProcessed %d entries (%.2f seconds).\n", | ||||||
633 | $counter, $elapsed if $opt{verbose}; | ||||||
634 | } | ||||||
635 | |||||||
636 | sub _update_word { | ||||||
637 | my ($self, $word, $res) = @_; | ||||||
638 | my ($k) = keys %$res; | ||||||
639 | $res = $res->{$k}; | ||||||
640 | if ($k eq $word) { | ||||||
641 | $self->{$word} = $res; | ||||||
642 | } else { | ||||||
643 | delete $self->{$word}; | ||||||
644 | $self->{$k} = $res; | ||||||
645 | } | ||||||
646 | } | ||||||
647 | |||||||
648 | # sub _trans_hash { | ||||||
649 | # my ($self, $word) = @_; | ||||||
650 | # return %{$self->{$word}{trans}}; | ||||||
651 | # } | ||||||
652 | |||||||
653 | =head2 C |
||||||
654 | |||||||
655 | This function is exported by default and creates a dictionary entry | ||||||
656 | given the word, word count, and hash of translations. Check C |
||||||
657 | for an example. | ||||||
658 | |||||||
659 | =cut | ||||||
660 | |||||||
661 | sub toentry { | ||||||
662 | ## word, count, ref(%hash) | ||||||
663 | if (ref($_[2]) eq "HASH") { | ||||||
664 | return { $_[0] => { count => $_[1], trans => $_[2] }} | ||||||
665 | } | ||||||
666 | else { | ||||||
667 | my ($w, $c, %t) = @_; | ||||||
668 | return { $w => { count => $c, trans => \%t } } | ||||||
669 | } | ||||||
670 | } | ||||||
671 | |||||||
672 | =head2 C |
||||||
673 | |||||||
674 | Method to save a PTD in another format. First argument is the name of | ||||||
675 | the format, second is the filename to be used. Supported formats are | ||||||
676 | C< |
||||||
677 | C< |
||||||
678 | database file. | ||||||
679 | |||||||
680 | Return undef if the format is not known. Returns 0 if save failed. A | ||||||
681 | true value in success. | ||||||
682 | |||||||
683 | =cut | ||||||
684 | |||||||
685 | sub saveAs { | ||||||
686 | my ($self, $type, $filename, $opts) = @_; | ||||||
687 | |||||||
688 | warn "Lingua::PTD saveAs called without all required parameteres" unless $type && $filename; | ||||||
689 | |||||||
690 | my $done = undef; | ||||||
691 | # switch | ||||||
692 | Lingua::PTD::Dumper::_save($self => $filename) and $done = 1 if $type =~ /dmp/i; | ||||||
693 | Lingua::PTD::BzDmp::_save( $self => $filename) and $done = 1 if $type =~ /bz2/i; | ||||||
694 | Lingua::PTD::XzDmp::_save( $self => $filename) and $done = 1 if $type =~ /xz/i; | ||||||
695 | Lingua::PTD::SQLite::_save($self => $filename) and $done = 1 if $type =~ /sqlite/i; | ||||||
696 | Lingua::PTD::TSV::_save($self, $filename, $opts) and $done = 1 if $type =~ /tsv/i; | ||||||
697 | Lingua::PTD::StarDict::_save($self, $filename, $opts) and $done = 1 if $type =~ /stardict/i; | ||||||
698 | # XXX - add above in the documentation. | ||||||
699 | |||||||
700 | # default | ||||||
701 | warn "Requested PTD filetype is not known" unless defined $done; | ||||||
702 | |||||||
703 | return $done; | ||||||
704 | } | ||||||
705 | |||||||
706 | =head2 C |
||||||
707 | |||||||
708 | This method replaces the dictionary, B |
||||||
709 | entries. This is specially usefull to process transation dictionaries | ||||||
710 | obtained with the C<-utf8> flag that (at the moment) does case | ||||||
711 | sensitive alignment. | ||||||
712 | |||||||
713 | $ptd->lowercase(verbose => 1); | ||||||
714 | |||||||
715 | NOTE: we are using case folding, that might no be always what you | ||||||
716 | expect, but proven to be more robust than relying on the system | ||||||
717 | lowercase implementation. | ||||||
718 | |||||||
719 | =cut | ||||||
720 | |||||||
721 | sub lowercase { | ||||||
722 | my ($self, %ops) = @_; | ||||||
723 | |||||||
724 | $ops{verbose} //= $self->verbose; | ||||||
725 | |||||||
726 | $self->downtr( | ||||||
727 | sub { | ||||||
728 | my ($w, $c, %t) = @_; | ||||||
729 | |||||||
730 | for my $k (keys %t) { | ||||||
731 | next unless $k =~ /[[:upper:]]/; | ||||||
732 | |||||||
733 | my $lk = fc $k; | ||||||
734 | $t{$lk} = exists($t{$lk}) ? $t{$lk} + $t{$k} : $t{$k}; | ||||||
735 | delete $t{$k}; | ||||||
736 | } | ||||||
737 | |||||||
738 | if ($w =~ /[[:upper:]]/) { | ||||||
739 | my $lw = fc $w; | ||||||
740 | |||||||
741 | my %ot = $self->transHash($lw); | ||||||
742 | if (%ot) { | ||||||
743 | my ($c1, $c2) = ($c, $self->count($lw)); | ||||||
744 | for my $k (_uniq(keys %t, keys %ot)) { | ||||||
745 | my ($p1, $p2) = ($t{$k} || 0, $ot{$k} || 0); | ||||||
746 | if ($c1 + $c2) { | ||||||
747 | $t{$k} = ($c1 * $p1 + $c2 * $p2)/($c1+$c2); | ||||||
748 | } else { | ||||||
749 | delete $t{$k}; | ||||||
750 | } | ||||||
751 | } | ||||||
752 | toentry($lw, $c1+$c2, %t) | ||||||
753 | } else { | ||||||
754 | toentry($lw, $c, %t) | ||||||
755 | } | ||||||
756 | } else { | ||||||
757 | toentry($w, $c, %t); | ||||||
758 | } | ||||||
759 | }, | ||||||
760 | filter => 1, | ||||||
761 | task => 'lowercase', | ||||||
762 | verbose => $ops{verbose}, | ||||||
763 | ); | ||||||
764 | } | ||||||
765 | |||||||
766 | =head2 C |
||||||
767 | |||||||
768 | Create unambiguous-concept traslation sets. | ||||||
769 | |||||||
770 | my $result = ucts($ptd1, $ptd2, m=>0.1, M=>0.8); | ||||||
771 | |||||||
772 | Available options are: | ||||||
773 | |||||||
774 | =over 4 | ||||||
775 | |||||||
776 | =item C |
||||||
777 | |||||||
778 | Mininum number of occurences of each token. Must be an | ||||||
779 | integer (default: 10). | ||||||
780 | |||||||
781 | =item C |
||||||
782 | |||||||
783 | Manixum number of occurences of each token. Must be an | ||||||
784 | integer (default: 100). | ||||||
785 | |||||||
786 | =item C
|
||||||
787 | |||||||
788 | Minimum probabilty for translation. Must be a probability | ||||||
789 | in the interval [0,1] (default: 0.2). | ||||||
790 | |||||||
791 | =item C
|
||||||
792 | |||||||
793 | Minimum probabilty for the inverse translations. Must be a | ||||||
794 | probability in the interval [0,1] (default: 0.8). | ||||||
795 | |||||||
796 | =item C |
||||||
797 | |||||||
798 | Print rank (default: 0). | ||||||
799 | |||||||
800 | =item C |
||||||
801 | |||||||
802 | Pretty print output (default: 0). | ||||||
803 | |||||||
804 | =item C | ||||||
805 | |||||||
806 | Pretty print output to file C |
||||||
807 | |||||||
808 | =back | ||||||
809 | |||||||
810 | =cut | ||||||
811 | |||||||
812 | sub ucts { | ||||||
813 | my ($fileA, $fileB, %my_opts) = @_; | ||||||
814 | |||||||
815 | my $min_occur = $my_opts{m} || 10; | ||||||
816 | my $max_occur = $my_opts{M} || 100; | ||||||
817 | my $prob = $my_opts{p} || 0.2; | ||||||
818 | my $probi = $my_opts{P} || 0.8; | ||||||
819 | my $rank = $my_opts{r} || 0; | ||||||
820 | my $pp = $my_opts{pp} || 0; | ||||||
821 | my $output = $my_opts{output} || ''; | ||||||
822 | |||||||
823 | # check files exist | ||||||
824 | unless ($fileA and $fileB) { | ||||||
825 | die "Error: need at least two PTDs given as argument."; | ||||||
826 | } | ||||||
827 | |||||||
828 | # handle output handles | ||||||
829 | $pp = 1 if $output; | ||||||
830 | open STDOUT, '>', $output if $output; | ||||||
831 | binmode(STDOUT, ':utf8'); # XXX | ||||||
832 | |||||||
833 | # load PTDs | ||||||
834 | my $ptd; | ||||||
835 | if (ref($fileA) =~ m/^Lingua::PTD/) { | ||||||
836 | $ptd = $fileA; | ||||||
837 | } | ||||||
838 | else { | ||||||
839 | if (-e $fileA) { | ||||||
840 | $ptd = Lingua::PTD->new($fileA); | ||||||
841 | } | ||||||
842 | else { | ||||||
843 | die "Error: file not found: $_"; | ||||||
844 | } | ||||||
845 | } | ||||||
846 | my $ptd_inv; | ||||||
847 | if (ref($fileB) =~ m/^Lingua::PTD/) { | ||||||
848 | $ptd_inv = $fileB; | ||||||
849 | } | ||||||
850 | else { | ||||||
851 | if (-e $fileB) { | ||||||
852 | $ptd_inv = Lingua::PTD->new($fileB); | ||||||
853 | } | ||||||
854 | else { | ||||||
855 | die "Error: file not found: $_"; | ||||||
856 | } | ||||||
857 | } | ||||||
858 | |||||||
859 | if ($pp and $fileA =~ m/.*?(\w\w)\-(\w\w)/) { # XXX | ||||||
860 | print "Langs: $1, $2\n" if $pp; | ||||||
861 | } | ||||||
862 | |||||||
863 | my (%left, %right); | ||||||
864 | |||||||
865 | # process each word in the PTD | ||||||
866 | my @words = $ptd->words; | ||||||
867 | foreach (@words) { | ||||||
868 | my $r = __build_ucts($ptd, $ptd_inv, $min_occur, $max_occur, $prob, $probi, $_); | ||||||
869 | $left{$_} = $r if $r; | ||||||
870 | } | ||||||
871 | # process each word in the inverse PTD | ||||||
872 | @words = $ptd_inv->words; | ||||||
873 | foreach (@words) { | ||||||
874 | my $r = __build_ucts($ptd_inv, $ptd, $min_occur, $max_occur, $prob, $probi, $_); | ||||||
875 | $right{$_} = $r if $r; | ||||||
876 | } | ||||||
877 | |||||||
878 | my @final = (); | ||||||
879 | foreach my $l (keys %left) { | ||||||
880 | my %ll = ($l=>1); | ||||||
881 | my %rr; | ||||||
882 | $rr{$_}++ for @{$left{$l}->{trans}}; | ||||||
883 | my $rank = $left{$l}->{rank}; | ||||||
884 | |||||||
885 | foreach (@{$left{$l}->{trans}}) { | ||||||
886 | $rr{$_}++; | ||||||
887 | if (exists($right{$_})) { | ||||||
888 | $ll{$_}++ for @{$right{$_}->{trans}}; | ||||||
889 | delete $right{$_}; | ||||||
890 | } | ||||||
891 | } | ||||||
892 | push @final, {l=>[keys %ll], r=>[keys %rr], rank=>$rank}; | ||||||
893 | } | ||||||
894 | foreach my $r (keys %right) { | ||||||
895 | my %ll; | ||||||
896 | my %rr = ($r=>1);; | ||||||
897 | $ll{$_}++ for @{$right{$r}->{trans}}; | ||||||
898 | my $rank = $right{$r}->{rank}; | ||||||
899 | |||||||
900 | push @final, {l=>[keys %ll], r=>[keys %rr], rank=>$rank}; | ||||||
901 | } | ||||||
902 | |||||||
903 | if ($pp) { | ||||||
904 | __pp_ucts($_,$rank) foreach (@final); | ||||||
905 | } | ||||||
906 | else { | ||||||
907 | return [@final]; | ||||||
908 | } | ||||||
909 | |||||||
910 | close STDOUT if $output; | ||||||
911 | } | ||||||
912 | |||||||
913 | sub __build_ucts { | ||||||
914 | my ($ptd, $ptd_inv, $min_occur, $max_occur, $prob, $probi, $word) = @_; | ||||||
915 | |||||||
916 | my $count = $ptd->count($word); ## or print STDERR "### $word\n"; | ||||||
917 | $count //= 0; | ||||||
918 | return undef unless ($min_occur <= $count and $count <= $max_occur); | ||||||
919 | |||||||
920 | my $total = 0; | ||||||
921 | my %trans = (); | ||||||
922 | my %transHash = $ptd->transHash($word); | ||||||
923 | |||||||
924 | foreach (keys %transHash) { | ||||||
925 | my $p = $transHash{$_}; | ||||||
926 | next unless ($p >= $prob); | ||||||
927 | my $p_inv = $ptd_inv->prob($_, $word); | ||||||
928 | next unless ($p_inv >= $probi); | ||||||
929 | |||||||
930 | my $counti = $ptd_inv->count($_); | ||||||
931 | if ( ($min_occur <= $counti) and ($counti <= $max_occur) ) { | ||||||
932 | if ($total) { $total = ($total+$p+$p_inv)/2; } | ||||||
933 | else { $total = $p+$p_inv; } | ||||||
934 | |||||||
935 | $trans{$_}++; | ||||||
936 | } | ||||||
937 | } | ||||||
938 | |||||||
939 | return undef unless %trans; | ||||||
940 | return {trans=>[keys %trans], rank=>$total}; | ||||||
941 | } | ||||||
942 | |||||||
943 | =head2 C |
||||||
944 | |||||||
945 | Create bi-words sets given a PTD pair. | ||||||
946 | |||||||
947 | my $result = bws($ptd1, $ptd2, m=>0.1, p=>0.4); | ||||||
948 | |||||||
949 | C<$ptd1> and C<$ptd2> can be filenames for the PTDs or already create | ||||||
950 | PTD objects. | ||||||
951 | |||||||
952 | The following options are available: | ||||||
953 | |||||||
954 | =over 4 | ||||||
955 | |||||||
956 | =item C |
||||||
957 | |||||||
958 | Mininum number of occurences of each token. Must be an integer | ||||||
959 | (default: 10). | ||||||
960 | |||||||
961 | =item C
|
||||||
962 | |||||||
963 | Minimum probabilty for translation. Must be a probability | ||||||
964 | in the interval [0,1] (default: 0.4). | ||||||
965 | |||||||
966 | =item C |
||||||
967 | |||||||
968 | Print rank (default: 0). | ||||||
969 | |||||||
970 | =item C |
||||||
971 | |||||||
972 | Pretty print output (default: 0). | ||||||
973 | |||||||
974 | =item C | ||||||
975 | |||||||
976 | Pretty print output to file C |
||||||
977 | |||||||
978 | =back | ||||||
979 | |||||||
980 | =cut | ||||||
981 | |||||||
982 | sub bws { | ||||||
983 | my ($fileA, $fileB, %my_opts) = @_; | ||||||
984 | |||||||
985 | my $min_occur = $my_opts{m} || 10; | ||||||
986 | my $prob = $my_opts{p} || 0.4; | ||||||
987 | my $rank = $my_opts{r} || 0; | ||||||
988 | my $pp = $my_opts{pp} || 0; | ||||||
989 | my $output = $my_opts{output} || ''; | ||||||
990 | |||||||
991 | my $filter = $my_opts{filter}; | ||||||
992 | |||||||
993 | #my $sorter; | ||||||
994 | #if ($my_opts{sorter} && ref($my_opts{sorter}) eq 'CODE') { | ||||||
995 | # $sorter = \&{$my_opts{sorter}}; | ||||||
996 | #} | ||||||
997 | |||||||
998 | # check files exist | ||||||
999 | unless ($fileA and $fileB) { | ||||||
1000 | die "Error: need at least two PTDs given as argument."; | ||||||
1001 | } | ||||||
1002 | |||||||
1003 | # handle output handles | ||||||
1004 | $pp = 1 if $output; | ||||||
1005 | open STDOUT, '>', $output if $output; | ||||||
1006 | binmode(STDOUT, ':utf8'); # XXX | ||||||
1007 | |||||||
1008 | # load PTDs | ||||||
1009 | my $ptd; | ||||||
1010 | if (ref($fileA) =~ m/^Lingua::PTD/) { | ||||||
1011 | $ptd = $fileA; | ||||||
1012 | } | ||||||
1013 | else { | ||||||
1014 | if (-e $fileA) { | ||||||
1015 | $ptd = Lingua::PTD->new($fileA); | ||||||
1016 | } | ||||||
1017 | else { | ||||||
1018 | die "Error: file not found: $_"; | ||||||
1019 | } | ||||||
1020 | } | ||||||
1021 | my $ptd_inv; | ||||||
1022 | if (ref($fileB) =~ m/^Lingua::PTD/) { | ||||||
1023 | $ptd_inv = $fileB; | ||||||
1024 | } | ||||||
1025 | else { | ||||||
1026 | if (-e $fileB) { | ||||||
1027 | $ptd_inv = Lingua::PTD->new($fileB); | ||||||
1028 | } | ||||||
1029 | else { | ||||||
1030 | die "Error: file not found: $_"; | ||||||
1031 | } | ||||||
1032 | } | ||||||
1033 | |||||||
1034 | if ($pp and $fileA =~ m/.*?(\w\w)\-(\w\w)/) { # XXX | ||||||
1035 | print "Langs: $1, $2\n" if $pp; | ||||||
1036 | } | ||||||
1037 | |||||||
1038 | my @final; | ||||||
1039 | |||||||
1040 | my @words = $ptd->words; | ||||||
1041 | my $total_words_l = $ptd->size(); | ||||||
1042 | my $total_words_r = $ptd_inv->size(); | ||||||
1043 | foreach my $word (@words) { | ||||||
1044 | my $count = $ptd->count($word); | ||||||
1045 | next unless ($count >= $min_occur); | ||||||
1046 | next if ($word eq "(none)"); | ||||||
1047 | |||||||
1048 | my %transHash = $ptd->transHash($word); | ||||||
1049 | foreach (keys %transHash) { | ||||||
1050 | my $p = $transHash{$_}; | ||||||
1051 | next unless ($p >= $prob); | ||||||
1052 | next if ($_ eq "(none)"); | ||||||
1053 | |||||||
1054 | __pp_ucts({l=>[$word],r=>[$_],rank=>$p}, $rank) if $pp; | ||||||
1055 | push @final, { | ||||||
1056 | l=>$word, cl=>$count, tl=>$total_words_l, | ||||||
1057 | r=>$_, cr=>$ptd_inv->count($_), tr=>$total_words_r, | ||||||
1058 | rank=>$p } unless $pp; | ||||||
1059 | } | ||||||
1060 | } | ||||||
1061 | @words = $ptd_inv->words; | ||||||
1062 | foreach my $word (@words) { | ||||||
1063 | my $count = $ptd_inv->count($word); | ||||||
1064 | next unless ($count >= $min_occur); | ||||||
1065 | next if ($word eq "(none)"); | ||||||
1066 | |||||||
1067 | my %transHash = $ptd_inv->transHash($word); | ||||||
1068 | foreach (keys %transHash) { | ||||||
1069 | my $p = $transHash{$_}; | ||||||
1070 | next unless ($p >= $prob); | ||||||
1071 | next if ($_ eq "(none)"); | ||||||
1072 | |||||||
1073 | __pp_ucts({l=>[$_],r=>[$word],rank=>$p}, $rank) if $pp; | ||||||
1074 | push @final, { | ||||||
1075 | l=>$_, cl=>$ptd->count($_), tl=>$total_words_l, | ||||||
1076 | r=>$word, cr=>$count, tr=>$total_words_r, | ||||||
1077 | rank=>$p } unless $pp; | ||||||
1078 | } | ||||||
1079 | } | ||||||
1080 | |||||||
1081 | # if only one filter, put it in an array | ||||||
1082 | $filter = [$filter] if ($filter and ref($filter) eq 'CODE'); | ||||||
1083 | # apply array of filters in order | ||||||
1084 | if ($filter and ref($filter) eq 'ARRAY'){ | ||||||
1085 | while (my $f = shift(@{$filter})) { | ||||||
1086 | @final = grep { $f->($_) } @final ; | ||||||
1087 | } | ||||||
1088 | } | ||||||
1089 | |||||||
1090 | close STDOUT if $output; | ||||||
1091 | return [@final] unless $pp; | ||||||
1092 | } | ||||||
1093 | |||||||
1094 | sub __pp_ucts { | ||||||
1095 | my ($r, $rank) = @_; | ||||||
1096 | |||||||
1097 | if ($rank) { | ||||||
1098 | printf "[%f]%s=%s\n", $r->{rank}, (join ',', @{$r->{l}}), join ',', @{$r->{r}}; | ||||||
1099 | } | ||||||
1100 | else { | ||||||
1101 | printf "%s=%s\n", (join ',', @{$r->{l}}), join ',', @{$r->{r}}; | ||||||
1102 | } | ||||||
1103 | } | ||||||
1104 | |||||||
1105 | =head1 SEE ALSO | ||||||
1106 | |||||||
1107 | NATools(3), perl(1) | ||||||
1108 | |||||||
1109 | =head1 AUTHOR | ||||||
1110 | |||||||
1111 | Alberto Manuel Brandão Simões, E |
||||||
1112 | |||||||
1113 | =head1 COPYRIGHT AND LICENSE | ||||||
1114 | |||||||
1115 | Copyright (C) 2008-2014 by Alberto Manuel Brandão Simões | ||||||
1116 | |||||||
1117 | =cut | ||||||
1118 | |||||||
1119 | sub _calculate_sizes { | ||||||
1120 | my $self = shift; | ||||||
1121 | my $total = 0; | ||||||
1122 | my $count = 0; | ||||||
1123 | $self->downtr( sub { $count++; $total += $_[1] }, verbose => 0); | ||||||
1124 | $self->{" size "} = $total; ## Private keys are kept with spaces. | ||||||
1125 | $self->{" count "} = $count; | ||||||
1126 | } | ||||||
1127 | |||||||
1128 | sub _min { $_[0] < $_[1] ? $_[0] : $_[1] } | ||||||
1129 | sub _max { $_[0] > $_[1] ? $_[0] : $_[1] } | ||||||
1130 | |||||||
1131 | sub _protect_quotes { | ||||||
1132 | my $f = shift; | ||||||
1133 | for ($f) { | ||||||
1134 | s/\\/\\\\/g; | ||||||
1135 | s/'/\\'/g; | ||||||
1136 | } | ||||||
1137 | return $f; | ||||||
1138 | } | ||||||
1139 | |||||||
1140 | |||||||
1141 | "This isn't right. This isn't even wrong."; | ||||||
1142 | __END__ |