Bug 16011: $VERSION - Remove use vars qw();
[koha.git] / C4 / Matcher.pm
1 package C4::Matcher;
2
3 # Copyright (C) 2007 LibLime, 2012 C & P Bibliography Services
4 #
5 # This file is part of Koha.
6 #
7 # Koha is free software; you can redistribute it and/or modify it
8 # under the terms of the GNU General Public License as published by
9 # the Free Software Foundation; either version 3 of the License, or
10 # (at your option) any later version.
11 #
12 # Koha is distributed in the hope that it will be useful, but
13 # WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License
18 # along with Koha; if not, see <http://www.gnu.org/licenses>.
19
20 use strict;
21 use warnings;
22
23 use C4::Context;
24 use MARC::Record;
25
26
27 BEGIN {
28 }
29
30 =head1 NAME
31
32 C4::Matcher - find MARC records matching another one
33
34 =head1 SYNOPSIS
35
36   my @matchers = C4::Matcher::GetMatcherList();
37
38   my $matcher = C4::Matcher->new($record_type);
39   $matcher->threshold($threshold);
40   $matcher->code($code);
41   $matcher->description($description);
42
43   $matcher->add_simple_matchpoint('isbn', 1000, '020', 'a', -1, 0, '');
44   $matcher->add_simple_matchpoint('Date', 1000, '008', '', 7, 4, '');
45   $matcher->add_matchpoint('isbn', 1000, [ { tag => '020', subfields => 'a', norms => [] } ]);
46
47   $matcher->add_simple_required_check('245', 'a', -1, 0, '', '245', 'a', -1, 0, '');
48   $matcher->add_required_check([ { tag => '245', subfields => 'a', norms => [] } ], 
49                                [ { tag => '245', subfields => 'a', norms => [] } ]);
50
51   my @matches = $matcher->get_matches($marc_record, $max_matches);
52
53   foreach $match (@matches) {
54
55       # matches already sorted in order of
56       # decreasing score
57       print "record ID: $match->{'record_id'};
58       print "score:     $match->{'score'};
59
60   }
61
62   my $matcher_description = $matcher->dump();
63
64 =head1 FUNCTIONS
65
66 =cut
67
68 =head2 GetMatcherList
69
70   my @matchers = C4::Matcher::GetMatcherList();
71
72 Returns an array of hashrefs list all matchers
73 present in the database.  Each hashref includes:
74
75  * matcher_id
76  * code
77  * description
78
79 =cut
80
81 sub GetMatcherList {
82     my $dbh = C4::Context->dbh;
83     
84     my $sth = $dbh->prepare_cached("SELECT matcher_id, code, description FROM marc_matchers ORDER BY matcher_id");
85     $sth->execute();
86     my @results = ();
87     while (my $row = $sth->fetchrow_hashref) {
88         push @results, $row;
89     } 
90     return @results;
91 }
92
93 =head2 GetMatcherId
94
95   my $matcher_id = C4::Matcher::GetMatcherId($code);
96
97 Returns the matcher_id of a code.
98
99 =cut
100
101 sub GetMatcherId {
102     my ($code) = @_;
103     my $dbh = C4::Context->dbh;
104
105     my $matcher_id = $dbh->selectrow_array("SELECT matcher_id FROM marc_matchers WHERE code = ?", undef, $code);
106     return $matcher_id;
107 }
108
109 =head1 METHODS
110
111 =head2 new
112
113   my $matcher = C4::Matcher->new($record_type, $threshold);
114
115 Creates a new Matcher.  C<$record_type> indicates which search
116 database to use, e.g., 'biblio' or 'authority' and defaults to
117 'biblio', while C<$threshold> is the minimum score required for a match
118 and defaults to 1000.
119
120 =cut
121
122 sub new {
123     my $class = shift;
124     my $self = {};
125
126     $self->{'id'} = undef;
127
128     if ($#_ > -1) {
129         $self->{'record_type'} = shift;
130     } else {
131         $self->{'record_type'} = 'biblio';
132     }
133
134     if ($#_ > -1) {
135         $self->{'threshold'} = shift;
136     } else {
137         $self->{'threshold'} = 1000;
138     }
139
140     $self->{'code'} = '';
141     $self->{'description'} = '';
142
143     $self->{'matchpoints'} = [];
144     $self->{'required_checks'} = [];
145
146     bless $self, $class;
147     return $self;
148 }
149
150 =head2 fetch
151
152   my $matcher = C4::Matcher->fetch($id);
153
154 Creates a matcher object from the version stored
155 in the database.  If a matcher with the given
156 id does not exist, returns undef.
157
158 =cut
159
160 sub fetch {
161     my $class = shift;
162     my $id = shift;
163     my $dbh = C4::Context->dbh();
164
165     my $sth = $dbh->prepare_cached("SELECT * FROM marc_matchers WHERE matcher_id = ?");
166     $sth->execute($id);
167     my $row = $sth->fetchrow_hashref;
168     $sth->finish();
169     return undef unless defined $row;
170
171     my $self = {};
172     $self->{'id'} = $row->{'matcher_id'};
173     $self->{'record_type'} = $row->{'record_type'};
174     $self->{'code'} = $row->{'code'};
175     $self->{'description'} = $row->{'description'};
176     $self->{'threshold'} = int($row->{'threshold'});
177     bless $self, $class;
178
179     # matchpoints
180     $self->{'matchpoints'} = [];
181     $sth = $dbh->prepare_cached("SELECT * FROM matcher_matchpoints WHERE matcher_id = ? ORDER BY matchpoint_id");
182     $sth->execute($self->{'id'});
183     while (my $row = $sth->fetchrow_hashref) {
184         my $matchpoint = $self->_fetch_matchpoint($row->{'matchpoint_id'});
185         push @{ $self->{'matchpoints'} }, $matchpoint;
186     }
187
188     # required checks
189     $self->{'required_checks'} = [];
190     $sth = $dbh->prepare_cached("SELECT * FROM matchchecks WHERE matcher_id = ? ORDER BY matchcheck_id");
191     $sth->execute($self->{'id'});
192     while (my $row = $sth->fetchrow_hashref) {
193         my $source_matchpoint = $self->_fetch_matchpoint($row->{'source_matchpoint_id'});
194         my $target_matchpoint = $self->_fetch_matchpoint($row->{'target_matchpoint_id'});
195         my $matchcheck = {};
196         $matchcheck->{'source_matchpoint'} = $source_matchpoint;
197         $matchcheck->{'target_matchpoint'} = $target_matchpoint;
198         push @{ $self->{'required_checks'} }, $matchcheck;
199     }
200
201     return $self;
202 }
203
204 sub _fetch_matchpoint {
205     my $self = shift;
206     my $matchpoint_id = shift;
207     
208     my $dbh = C4::Context->dbh;
209     my $sth = $dbh->prepare_cached("SELECT * FROM matchpoints WHERE matchpoint_id = ?");
210     $sth->execute($matchpoint_id);
211     my $row = $sth->fetchrow_hashref;
212     my $matchpoint = {};
213     $matchpoint->{'index'} = $row->{'search_index'};
214     $matchpoint->{'score'} = int($row->{'score'});
215     $sth->finish();
216
217     $matchpoint->{'components'} = [];
218     $sth = $dbh->prepare_cached("SELECT * FROM matchpoint_components WHERE matchpoint_id = ? ORDER BY sequence");
219     $sth->execute($matchpoint_id);
220     while ($row = $sth->fetchrow_hashref) {
221         my $component = {};
222         $component->{'tag'} = $row->{'tag'};
223         $component->{'subfields'} = { map { $_ => 1 } split(//,  $row->{'subfields'}) };
224         $component->{'offset'} = int($row->{'offset'});
225         $component->{'length'} = int($row->{'length'});
226         $component->{'norms'} = [];
227         my $sth2 = $dbh->prepare_cached("SELECT * 
228                                          FROM matchpoint_component_norms 
229                                          WHERE matchpoint_component_id = ? ORDER BY sequence");
230         $sth2->execute($row->{'matchpoint_component_id'});
231         while (my $row2 = $sth2->fetchrow_hashref) {
232             push @{ $component->{'norms'} }, $row2->{'norm_routine'};
233         }
234         push @{ $matchpoint->{'components'} }, $component;
235     }
236     return $matchpoint;
237 }
238
239 =head2 store
240
241   my $id = $matcher->store();
242
243 Stores matcher in database.  The return value is the ID 
244 of the marc_matchers row.  If the matcher was 
245 previously retrieved from the database via the fetch()
246 method, the DB representation of the matcher
247 is replaced.
248
249 =cut
250
251 sub store {
252     my $self = shift;
253
254     if (defined $self->{'id'}) {
255         # update
256         $self->_del_matcher_components();
257         $self->_update_marc_matchers();
258     } else {
259         # create new
260         $self->_new_marc_matchers();
261     }
262     $self->_store_matcher_components();
263     return $self->{'id'};
264 }
265
266 sub _del_matcher_components {
267     my $self = shift;
268
269     my $dbh = C4::Context->dbh();
270     my $sth = $dbh->prepare_cached("DELETE FROM matchpoints WHERE matcher_id = ?");
271     $sth->execute($self->{'id'});
272     $sth = $dbh->prepare_cached("DELETE FROM matchchecks WHERE matcher_id = ?");
273     $sth->execute($self->{'id'});
274     # foreign key delete cascades take care of deleting relevant rows
275     # from matcher_matchpoints, matchpoint_components, and
276     # matchpoint_component_norms
277 }
278
279 sub _update_marc_matchers {
280     my $self = shift;
281
282     my $dbh = C4::Context->dbh();
283     my $sth = $dbh->prepare_cached("UPDATE marc_matchers 
284                                     SET code = ?,
285                                         description = ?,
286                                         record_type = ?,
287                                         threshold = ?
288                                     WHERE matcher_id = ?");
289     $sth->execute($self->{'code'}, $self->{'description'}, $self->{'record_type'}, $self->{'threshold'}, $self->{'id'});
290 }
291
292 sub _new_marc_matchers {
293     my $self = shift;
294
295     my $dbh = C4::Context->dbh();
296     my $sth = $dbh->prepare_cached("INSERT INTO marc_matchers
297                                     (code, description, record_type, threshold)
298                                     VALUES (?, ?, ?, ?)");
299     $sth->execute($self->{'code'}, $self->{'description'}, $self->{'record_type'}, $self->{'threshold'});
300     $self->{'id'} = $dbh->{'mysql_insertid'};
301 }
302
303 sub _store_matcher_components {
304     my $self = shift;
305
306     my $dbh = C4::Context->dbh();
307     my $sth;
308     my $matcher_id = $self->{'id'};
309     foreach my $matchpoint (@{ $self->{'matchpoints'}}) {
310         my $matchpoint_id = $self->_store_matchpoint($matchpoint);
311         $sth = $dbh->prepare_cached("INSERT INTO matcher_matchpoints (matcher_id, matchpoint_id)
312                                      VALUES (?, ?)");
313         $sth->execute($matcher_id, $matchpoint_id);
314     }
315     foreach my $matchcheck (@{ $self->{'required_checks'} }) {
316         my $source_matchpoint_id = $self->_store_matchpoint($matchcheck->{'source_matchpoint'});
317         my $target_matchpoint_id = $self->_store_matchpoint($matchcheck->{'target_matchpoint'});
318         $sth = $dbh->prepare_cached("INSERT INTO matchchecks
319                                      (matcher_id, source_matchpoint_id, target_matchpoint_id)
320                                      VALUES (?, ?, ?)");
321         $sth->execute($matcher_id, $source_matchpoint_id,  $target_matchpoint_id);
322     }
323
324 }
325
326 sub _store_matchpoint {
327     my $self = shift;
328     my $matchpoint = shift;
329
330     my $dbh = C4::Context->dbh();
331     my $sth;
332     my $matcher_id = $self->{'id'};
333     $sth = $dbh->prepare_cached("INSERT INTO matchpoints (matcher_id, search_index, score)
334                                  VALUES (?, ?, ?)");
335     $sth->execute($matcher_id, $matchpoint->{'index'}, $matchpoint->{'score'});
336     my $matchpoint_id = $dbh->{'mysql_insertid'};
337     my $seqnum = 0;
338     foreach my $component (@{ $matchpoint->{'components'} }) {
339         $seqnum++;
340         $sth = $dbh->prepare_cached("INSERT INTO matchpoint_components 
341                                      (matchpoint_id, sequence, tag, subfields, offset, length)
342                                      VALUES (?, ?, ?, ?, ?, ?)");
343         $sth->bind_param(1, $matchpoint_id);
344         $sth->bind_param(2, $seqnum);
345         $sth->bind_param(3, $component->{'tag'});
346         $sth->bind_param(4, join "", sort keys %{ $component->{'subfields'} });
347         $sth->bind_param(5, $component->{'offset'});
348         $sth->bind_param(6, $component->{'length'});
349         $sth->execute();
350         my $matchpoint_component_id = $dbh->{'mysql_insertid'};
351         my $normseq = 0;
352         foreach my $norm (@{ $component->{'norms'} }) {
353             $normseq++;
354             $sth = $dbh->prepare_cached("INSERT INTO matchpoint_component_norms
355                                          (matchpoint_component_id, sequence, norm_routine)
356                                          VALUES (?, ?, ?)");
357             $sth->execute($matchpoint_component_id, $normseq, $norm);
358         }
359     }
360     return $matchpoint_id;
361 }
362
363
364 =head2 delete
365
366   C4::Matcher->delete($id);
367
368 Deletes the matcher of the specified ID
369 from the database.
370
371 =cut
372
373 sub delete {
374     my $class = shift;
375     my $matcher_id = shift;
376
377     my $dbh = C4::Context->dbh;
378     my $sth = $dbh->prepare("DELETE FROM marc_matchers WHERE matcher_id = ?");
379     $sth->execute($matcher_id); # relying on cascading deletes to clean up everything
380 }
381
382 =head2 record_type
383
384   $matcher->record_type('biblio');
385   my $record_type = $matcher->record_type();
386
387 Accessor method.
388
389 =cut
390
391 sub record_type {
392     my $self = shift;
393     @_ ? $self->{'record_type'} = shift : $self->{'record_type'};
394 }
395
396 =head2 threshold
397
398   $matcher->threshold(1000);
399   my $threshold = $matcher->threshold();
400
401 Accessor method.
402
403 =cut
404
405 sub threshold {
406     my $self = shift;
407     @_ ? $self->{'threshold'} = shift : $self->{'threshold'};
408 }
409
410 =head2 _id
411
412   $matcher->_id(123);
413   my $id = $matcher->_id();
414
415 Accessor method.  Note that using this method
416 to set the DB ID of the matcher should not be
417 done outside of the editing CGI.
418
419 =cut
420
421 sub _id {
422     my $self = shift;
423     @_ ? $self->{'id'} = shift : $self->{'id'};
424 }
425
426 =head2 code
427
428   $matcher->code('ISBN');
429   my $code = $matcher->code();
430
431 Accessor method.
432
433 =cut
434
435 sub code {
436     my $self = shift;
437     @_ ? $self->{'code'} = shift : $self->{'code'};
438 }
439
440 =head2 description
441
442   $matcher->description('match on ISBN');
443   my $description = $matcher->description();
444
445 Accessor method.
446
447 =cut
448
449 sub description {
450     my $self = shift;
451     @_ ? $self->{'description'} = shift : $self->{'description'};
452 }
453
454 =head2 add_matchpoint
455
456   $matcher->add_matchpoint($index, $score, $matchcomponents);
457
458 Adds a matchpoint that may include multiple components.  The $index
459 parameter identifies the index that will be searched, while $score
460 is the weight that will be added if a match is found.
461
462 $matchcomponents should be a reference to an array of matchpoint
463 compoents, each of which should be a hash containing the following 
464 keys:
465     tag
466     subfields
467     offset
468     length
469     norms
470
471 The normalization_rules value should in turn be a reference to an
472 array, each element of which should be a reference to a 
473 normalization subroutine (under C4::Normalize) to be applied
474 to the source string.
475
476 =cut
477     
478 sub add_matchpoint {
479     my $self = shift;
480     my ($index, $score, $matchcomponents) = @_;
481
482     my $matchpoint = {};
483     $matchpoint->{'index'} = $index;
484     $matchpoint->{'score'} = $score;
485     $matchpoint->{'components'} = [];
486     foreach my $input_component (@{ $matchcomponents }) {
487         push @{ $matchpoint->{'components'} }, _parse_match_component($input_component);
488     }
489     push @{ $self->{'matchpoints'} }, $matchpoint;
490 }
491
492 =head2 add_simple_matchpoint
493
494   $matcher->add_simple_matchpoint($index, $score, $source_tag,
495                             $source_subfields, $source_offset, 
496                             $source_length, $source_normalizer);
497
498
499 Adds a simple matchpoint rule -- after composing a key based on the source tag and subfields,
500 normalized per the normalization fuction, search the index.  All records retrieved
501 will receive the assigned score.
502
503 =cut
504
505 sub add_simple_matchpoint {
506     my $self = shift;
507     my ($index, $score, $source_tag, $source_subfields, $source_offset, $source_length, $source_normalizer) = @_;
508
509     $self->add_matchpoint($index, $score, [
510                           { tag => $source_tag, subfields => $source_subfields,
511                             offset => $source_offset, 'length' => $source_length,
512                             norms => [ $source_normalizer ]
513                           }
514                          ]);
515 }
516
517 =head2 add_required_check
518
519   $match->add_required_check($source_matchpoint, $target_matchpoint);
520
521 Adds a required check definition.  A required check means that in 
522 order for a match to be considered valid, the key derived from the
523 source (incoming) record must match the key derived from the target
524 (already in DB) record.
525
526 Unlike a regular matchpoint, only the first repeat of each tag 
527 in the source and target match criteria are considered.
528
529 A typical example of a required check would be verifying that the
530 titles and publication dates match.
531
532 $source_matchpoint and $target_matchpoint are each a reference to
533 an array of hashes, where each hash follows the same definition
534 as the matchpoint component specification in add_matchpoint, i.e.,
535
536     tag
537     subfields
538     offset
539     length
540     norms
541
542 The normalization_rules value should in turn be a reference to an
543 array, each element of which should be a reference to a 
544 normalization subroutine (under C4::Normalize) to be applied
545 to the source string.
546
547 =cut
548
549 sub add_required_check {
550     my $self = shift;
551     my ($source_matchpoint, $target_matchpoint) = @_;
552
553     my $matchcheck = {};
554     $matchcheck->{'source_matchpoint'}->{'index'} = '';
555     $matchcheck->{'source_matchpoint'}->{'score'} = 0;
556     $matchcheck->{'source_matchpoint'}->{'components'} = [];
557     $matchcheck->{'target_matchpoint'}->{'index'} = '';
558     $matchcheck->{'target_matchpoint'}->{'score'} = 0;
559     $matchcheck->{'target_matchpoint'}->{'components'} = [];
560     foreach my $input_component (@{ $source_matchpoint }) {
561         push @{ $matchcheck->{'source_matchpoint'}->{'components'} }, _parse_match_component($input_component);
562     }
563     foreach my $input_component (@{ $target_matchpoint }) {
564         push @{ $matchcheck->{'target_matchpoint'}->{'components'} }, _parse_match_component($input_component);
565     }
566     push @{ $self->{'required_checks'} }, $matchcheck;
567 }
568
569 =head2 add_simple_required_check
570
571   $matcher->add_simple_required_check($source_tag, $source_subfields,
572                 $source_offset, $source_length, $source_normalizer, 
573                 $target_tag, $target_subfields, $target_offset, 
574                 $target_length, $target_normalizer);
575
576 Adds a required check, which requires that the normalized keys made from the source and targets
577 must match for a match to be considered valid.
578
579 =cut
580
581 sub add_simple_required_check {
582     my $self = shift;
583     my ($source_tag, $source_subfields, $source_offset, $source_length, $source_normalizer,
584         $target_tag, $target_subfields, $target_offset, $target_length, $target_normalizer) = @_;
585
586     $self->add_required_check(
587       [ { tag => $source_tag, subfields => $source_subfields, offset => $source_offset, 'length' => $source_length,
588           norms => [ $source_normalizer ] } ],
589       [ { tag => $target_tag, subfields => $target_subfields, offset => $target_offset, 'length' => $target_length,
590           norms => [ $target_normalizer ] } ]
591     );
592 }
593
594 =head2 get_matches
595
596   my @matches = $matcher->get_matches($marc_record, $max_matches);
597   foreach $match (@matches) {
598       # matches already sorted in order of
599       # decreasing score
600       print "record ID: $match->{'record_id'};
601       print "score:     $match->{'score'};
602   }
603
604 Identifies all of the records matching the given MARC record.  For a record already 
605 in the database to be considered a match, it must meet the following criteria:
606
607 =over 2
608
609 =item 1. Total score from its matching field must exceed the supplied threshold.
610
611 =item 2. It must pass all required checks.
612
613 =back
614
615 Only the top $max_matches matches are returned.  The returned array is sorted
616 in order of decreasing score, i.e., the best match is first.
617
618 =cut
619
620 sub get_matches {
621     my $self = shift;
622     my ($source_record, $max_matches) = @_;
623
624     my %matches = ();
625
626     my $QParser;
627     $QParser = C4::Context->queryparser if (C4::Context->preference('UseQueryParser'));
628     foreach my $matchpoint ( @{ $self->{'matchpoints'} } ) {
629         my @source_keys = _get_match_keys( $source_record, $matchpoint );
630
631         next if scalar(@source_keys) == 0;
632
633         # FIXME - because of a bug in QueryParser, an expression ofthe
634         # format 'isbn:"isbn1" || isbn:"isbn2" || isbn"isbn3"...'
635         # does not get parsed correctly, so we will not
636         # do AggressiveMatchOnISBN if UseQueryParser is on
637         @source_keys = C4::Koha::GetVariationsOfISBNs(@source_keys)
638           if ( $matchpoint->{index} =~ /^isbn$/i
639             && C4::Context->preference('AggressiveMatchOnISBN') )
640             && !C4::Context->preference('UseQueryParser');
641
642         # build query
643         my $query;
644         my $error;
645         my $searchresults;
646         my $total_hits;
647         if ( $self->{'record_type'} eq 'biblio' ) {
648
649             if ($QParser) {
650                 $query = join( " || ",
651                     map { "$matchpoint->{'index'}:$_" } @source_keys );
652             }
653             else {
654                 my $phr = C4::Context->preference('AggressiveMatchOnISBN') ? ',phr' : q{};
655                 $query = join( " or ",
656                     map { "$matchpoint->{'index'}$phr=$_" } @source_keys );
657             }
658
659             require C4::Search;
660
661             ( $error, $searchresults, $total_hits ) =
662               C4::Search::SimpleSearch( $query, 0, $max_matches );
663         }
664         elsif ( $self->{'record_type'} eq 'authority' ) {
665             my $authresults;
666             my @marclist;
667             my @and_or;
668             my @excluding = [];
669             my @operator;
670             my @value;
671             foreach my $key (@source_keys) {
672                 push @marclist, $matchpoint->{'index'};
673                 push @and_or,   'or';
674                 push @operator, 'exact';
675                 push @value,    $key;
676             }
677             require C4::AuthoritiesMarc;
678             ( $authresults, $total_hits ) =
679               C4::AuthoritiesMarc::SearchAuthorities(
680                 \@marclist,  \@and_or, \@excluding, \@operator,
681                 \@value,     0,        20,          undef,
682                 'AuthidAsc', 1
683               );
684             foreach my $result (@$authresults) {
685                 push @$searchresults, $result->{'authid'};
686             }
687         }
688
689         if ( defined $error ) {
690             warn "search failed ($query) $error";
691         }
692         else {
693             foreach my $matched ( @{$searchresults} ) {
694                 $matches{$matched} += $matchpoint->{'score'};
695             }
696         }
697     }
698
699     # get rid of any that don't meet the threshold
700     %matches = map { ($matches{$_} >= $self->{'threshold'}) ? ($_ => $matches{$_}) : () } keys %matches;
701
702     # get rid of any that don't meet the required checks
703     %matches = map { _passes_required_checks($source_record, $_, $self->{'required_checks'}) ?  ($_ => $matches{$_}) : () } 
704                 keys %matches unless ($self->{'record_type'} eq 'auth');
705
706     my @results = ();
707     if ($self->{'record_type'} eq 'biblio') {
708         require C4::Biblio;
709         foreach my $marcblob (keys %matches) {
710             my $target_record = C4::Search::new_record_from_zebra('biblioserver',$marcblob);
711             my $record_number;
712             my $result = C4::Biblio::TransformMarcToKoha(C4::Context->dbh, $target_record, '');
713             $record_number = $result->{'biblionumber'};
714             push @results, { 'record_id' => $record_number, 'score' => $matches{$marcblob} };
715         }
716     } elsif ($self->{'record_type'} eq 'authority') {
717         require C4::AuthoritiesMarc;
718         foreach my $authid (keys %matches) {
719             push @results, { 'record_id' => $authid, 'score' => $matches{$authid} };
720         }
721     }
722     @results = sort { $b->{'score'} cmp $a->{'score'} } @results;
723     if (scalar(@results) > $max_matches) {
724         @results = @results[0..$max_matches-1];
725     }
726     return @results;
727
728 }
729
730 =head2 dump
731
732   $description = $matcher->dump();
733
734 Returns a reference to a structure containing all of the information
735 in the matcher object.  This is mainly a convenience method to
736 aid setting up a HTML editing form.
737
738 =cut
739
740 sub dump {
741     my $self = shift;
742    
743     my $result = {};
744
745     $result->{'matcher_id'} = $self->{'id'};
746     $result->{'code'} = $self->{'code'};
747     $result->{'description'} = $self->{'description'};
748     $result->{'record_type'} = $self->{'record_type'};
749
750     $result->{'matchpoints'} = [];
751     foreach my $matchpoint (@{ $self->{'matchpoints'} }) {
752         push @{  $result->{'matchpoints'} }, $matchpoint;
753     }
754     $result->{'matchchecks'} = [];
755     foreach my $matchcheck (@{ $self->{'required_checks'} }) {
756         push @{  $result->{'matchchecks'} }, $matchcheck;
757     }
758
759     return $result;
760 }
761
762 sub _passes_required_checks {
763     my ($source_record, $target_blob, $matchchecks) = @_;
764     my $target_record = MARC::Record->new_from_usmarc($target_blob); # FIXME -- need to avoid parsing record twice
765
766     # no checks supplied == automatic pass
767     return 1 if $#{ $matchchecks } == -1;
768
769     foreach my $matchcheck (@{ $matchchecks }) {
770         my $source_key = join "", _get_match_keys($source_record, $matchcheck->{'source_matchpoint'});
771         my $target_key = join "", _get_match_keys($target_record, $matchcheck->{'target_matchpoint'});
772         return 0 unless $source_key eq $target_key;
773     }
774     return 1;
775 }
776
777 sub _get_match_keys {
778     my $source_record = shift;
779     my $matchpoint = shift;
780     my $check_only_first_repeat = @_ ? shift : 0;
781
782     # If there is more than one component to the matchpoint (e.g.,
783     # matchpoint includes both 003 and 001), any repeats
784     # of the first component's tag are identified; repeats
785     # of the subsequent components' tags are appended to
786     # each parallel key dervied from the first component,
787     # up to the number of repeats of the first component's tag.
788     #
789     # For example, if the record has one 003 and two 001s, only
790     # one key is retrieved because there is only one 003.  The key
791     # will consist of the contents of the first 003 and first 001.
792     #
793     # If there are two 003s and two 001s, there will be two keys:
794     #    first 003 + first 001
795     #    second 003 + second 001
796     
797     my @keys = ();
798     for (my $i = 0; $i <= $#{ $matchpoint->{'components'} }; $i++) {
799         my $component = $matchpoint->{'components'}->[$i];
800         my $j = -1;
801         FIELD: foreach my $field ($source_record->field($component->{'tag'})) {
802             $j++;
803             last FIELD if $j > 0 and $check_only_first_repeat;
804             last FIELD if $i > 0 and $j > $#keys;
805             my $key = "";
806                         my $string;
807             if ($field->is_control_field()) {
808                                 $string=$field->data();
809             } else {
810                 foreach my $subfield ($field->subfields()) {
811                     if (exists $component->{'subfields'}->{$subfield->[0]}) {
812                         $string .= " " . $subfield->[1];
813                     }
814                 }
815                         }
816             if ($component->{'length'}>0) {
817                     $string= substr($string, $component->{'offset'}, $component->{'length'});
818                             # FIXME normalize, substr
819             } elsif ($component->{'offset'}) {
820                     $string= substr($string, $component->{'offset'});
821             }
822             $key = _normalize($string);
823             if ($i == 0) {
824                 push @keys, $key if $key;
825             } else {
826                 $keys[$j] .= " $key" if $key;
827             }
828         }
829     }
830     return @keys;
831 }
832
833
834 sub _parse_match_component {
835     my $input_component = shift;
836
837     my $component = {};
838     $component->{'tag'} = $input_component->{'tag'};
839     $component->{'subfields'} = { map { $_ => 1 } split(//, $input_component->{'subfields'}) };
840     $component->{'offset'} = exists($input_component->{'offset'}) ? $input_component->{'offset'} : -1;
841     $component->{'length'} = $input_component->{'length'} ? $input_component->{'length'} : 0;
842     $component->{'norms'} =  $input_component->{'norms'} ? $input_component->{'norms'} : [];
843
844     return $component;
845 }
846
847 # FIXME - default normalizer
848 sub _normalize {
849     my $value = uc shift;
850     $value =~ s/[.;:,\]\[\)\(\/'"]//g;
851     $value =~ s/^\s+//;
852     #$value =~ s/^\s+$//;
853     $value =~ s/\s+$//;
854     $value =~ s/\s+/ /g;
855     #$value =~ s/[.;,\]\[\)\(\/"']//g;
856     return $value;
857 }
858
859 1;
860 __END__
861
862 =head1 AUTHOR
863
864 Koha Development Team <http://koha-community.org/>
865
866 Galen Charlton <galen.charlton@liblime.com>
867
868 =cut