Bug 7821 - {langcode} will be replaced with current interface language
[koha.git] / C4 / Matcher.pm
blob9d1df67e585063ece52156298821d8039664f6d5
1 package C4::Matcher;
3 # Copyright (C) 2007 LibLime
5 # This file is part of Koha.
7 # Koha is free software; you can redistribute it and/or modify it under the
8 # terms of the GNU General Public License as published by the Free Software
9 # Foundation; either version 2 of the License, or (at your option) any later
10 # version.
12 # Koha is distributed in the hope that it will be useful, but WITHOUT ANY
13 # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
14 # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along
17 # with Koha; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 use strict;
21 use warnings;
23 use C4::Context;
24 use MARC::Record;
25 use C4::Search;
26 use C4::Biblio;
28 use vars qw($VERSION);
30 BEGIN {
31 # set the version for version checking
32 $VERSION = 3.01;
35 =head1 NAME
37 C4::Matcher - find MARC records matching another one
39 =head1 SYNOPSIS
41 my @matchers = C4::Matcher::GetMatcherList();
43 my $matcher = C4::Matcher->new($record_type);
44 $matcher->threshold($threshold);
45 $matcher->code($code);
46 $matcher->description($description);
48 $matcher->add_simple_matchpoint('isbn', 1000, '020', 'a', -1, 0, '');
49 $matcher->add_simple_matchpoint('Date', 1000, '008', '', 7, 4, '');
50 $matcher->add_matchpoint('isbn', 1000, [ { tag => '020', subfields => 'a', norms => [] } ]);
52 $matcher->add_simple_required_check('245', 'a', -1, 0, '', '245', 'a', -1, 0, '');
53 $matcher->add_required_check([ { tag => '245', subfields => 'a', norms => [] } ],
54 [ { tag => '245', subfields => 'a', norms => [] } ]);
56 my @matches = $matcher->get_matches($marc_record, $max_matches);
58 foreach $match (@matches) {
60 # matches already sorted in order of
61 # decreasing score
62 print "record ID: $match->{'record_id'};
63 print "score: $match->{'score'};
67 my $matcher_description = $matcher->dump();
69 =head1 FUNCTIONS
71 =cut
73 =head2 GetMatcherList
75 my @matchers = C4::Matcher::GetMatcherList();
77 Returns an array of hashrefs list all matchers
78 present in the database. Each hashref includes:
80 * matcher_id
81 * code
82 * description
84 =cut
86 sub GetMatcherList {
87 my $dbh = C4::Context->dbh;
89 my $sth = $dbh->prepare_cached("SELECT matcher_id, code, description FROM marc_matchers ORDER BY matcher_id");
90 $sth->execute();
91 my @results = ();
92 while (my $row = $sth->fetchrow_hashref) {
93 push @results, $row;
95 return @results;
98 =head1 METHODS
100 =head2 new
102 my $matcher = C4::Matcher->new($record_type, $threshold);
104 Creates a new Matcher. C<$record_type> indicates which search
105 database to use, e.g., 'biblio' or 'authority' and defaults to
106 'biblio', while C<$threshold> is the minimum score required for a match
107 and defaults to 1000.
109 =cut
111 sub new {
112 my $class = shift;
113 my $self = {};
115 $self->{'id'} = undef;
117 if ($#_ > -1) {
118 $self->{'record_type'} = shift;
119 } else {
120 $self->{'record_type'} = 'biblio';
123 if ($#_ > -1) {
124 $self->{'threshold'} = shift;
125 } else {
126 $self->{'threshold'} = 1000;
129 $self->{'code'} = '';
130 $self->{'description'} = '';
132 $self->{'matchpoints'} = [];
133 $self->{'required_checks'} = [];
135 bless $self, $class;
136 return $self;
139 =head2 fetch
141 my $matcher = C4::Matcher->fetch($id);
143 Creates a matcher object from the version stored
144 in the database. If a matcher with the given
145 id does not exist, returns undef.
147 =cut
149 sub fetch {
150 my $class = shift;
151 my $id = shift;
152 my $dbh = C4::Context->dbh();
154 my $sth = $dbh->prepare_cached("SELECT * FROM marc_matchers WHERE matcher_id = ?");
155 $sth->execute($id);
156 my $row = $sth->fetchrow_hashref;
157 $sth->finish();
158 return undef unless defined $row;
160 my $self = {};
161 $self->{'id'} = $row->{'matcher_id'};
162 $self->{'record_type'} = $row->{'record_type'};
163 $self->{'code'} = $row->{'code'};
164 $self->{'description'} = $row->{'description'};
165 $self->{'threshold'} = int($row->{'threshold'});
166 bless $self, $class;
168 # matchpoints
169 $self->{'matchpoints'} = [];
170 $sth = $dbh->prepare_cached("SELECT * FROM matcher_matchpoints WHERE matcher_id = ? ORDER BY matchpoint_id");
171 $sth->execute($self->{'id'});
172 while (my $row = $sth->fetchrow_hashref) {
173 my $matchpoint = $self->_fetch_matchpoint($row->{'matchpoint_id'});
174 push @{ $self->{'matchpoints'} }, $matchpoint;
177 # required checks
178 $self->{'required_checks'} = [];
179 $sth = $dbh->prepare_cached("SELECT * FROM matchchecks WHERE matcher_id = ? ORDER BY matchcheck_id");
180 $sth->execute($self->{'id'});
181 while (my $row = $sth->fetchrow_hashref) {
182 my $source_matchpoint = $self->_fetch_matchpoint($row->{'source_matchpoint_id'});
183 my $target_matchpoint = $self->_fetch_matchpoint($row->{'target_matchpoint_id'});
184 my $matchcheck = {};
185 $matchcheck->{'source_matchpoint'} = $source_matchpoint;
186 $matchcheck->{'target_matchpoint'} = $target_matchpoint;
187 push @{ $self->{'required_checks'} }, $matchcheck;
190 return $self;
193 sub _fetch_matchpoint {
194 my $self = shift;
195 my $matchpoint_id = shift;
197 my $dbh = C4::Context->dbh;
198 my $sth = $dbh->prepare_cached("SELECT * FROM matchpoints WHERE matchpoint_id = ?");
199 $sth->execute($matchpoint_id);
200 my $row = $sth->fetchrow_hashref;
201 my $matchpoint = {};
202 $matchpoint->{'index'} = $row->{'search_index'};
203 $matchpoint->{'score'} = int($row->{'score'});
204 $sth->finish();
206 $matchpoint->{'components'} = [];
207 $sth = $dbh->prepare_cached("SELECT * FROM matchpoint_components WHERE matchpoint_id = ? ORDER BY sequence");
208 $sth->execute($matchpoint_id);
209 while ($row = $sth->fetchrow_hashref) {
210 my $component = {};
211 $component->{'tag'} = $row->{'tag'};
212 $component->{'subfields'} = { map { $_ => 1 } split(//, $row->{'subfields'}) };
213 $component->{'offset'} = int($row->{'offset'});
214 $component->{'length'} = int($row->{'length'});
215 $component->{'norms'} = [];
216 my $sth2 = $dbh->prepare_cached("SELECT *
217 FROM matchpoint_component_norms
218 WHERE matchpoint_component_id = ? ORDER BY sequence");
219 $sth2->execute($row->{'matchpoint_component_id'});
220 while (my $row2 = $sth2->fetchrow_hashref) {
221 push @{ $component->{'norms'} }, $row2->{'norm_routine'};
223 push @{ $matchpoint->{'components'} }, $component;
225 return $matchpoint;
228 =head2 store
230 my $id = $matcher->store();
232 Stores matcher in database. The return value is the ID
233 of the marc_matchers row. If the matcher was
234 previously retrieved from the database via the fetch()
235 method, the DB representation of the matcher
236 is replaced.
238 =cut
240 sub store {
241 my $self = shift;
243 if (defined $self->{'id'}) {
244 # update
245 $self->_del_matcher_components();
246 $self->_update_marc_matchers();
247 } else {
248 # create new
249 $self->_new_marc_matchers();
251 $self->_store_matcher_components();
252 return $self->{'id'};
255 sub _del_matcher_components {
256 my $self = shift;
258 my $dbh = C4::Context->dbh();
259 my $sth = $dbh->prepare_cached("DELETE FROM matchpoints WHERE matcher_id = ?");
260 $sth->execute($self->{'id'});
261 $sth = $dbh->prepare_cached("DELETE FROM matchchecks WHERE matcher_id = ?");
262 $sth->execute($self->{'id'});
263 # foreign key delete cascades take care of deleting relevant rows
264 # from matcher_matchpoints, matchpoint_components, and
265 # matchpoint_component_norms
268 sub _update_marc_matchers {
269 my $self = shift;
271 my $dbh = C4::Context->dbh();
272 my $sth = $dbh->prepare_cached("UPDATE marc_matchers
273 SET code = ?,
274 description = ?,
275 record_type = ?,
276 threshold = ?
277 WHERE matcher_id = ?");
278 $sth->execute($self->{'code'}, $self->{'description'}, $self->{'record_type'}, $self->{'threshold'}, $self->{'id'});
281 sub _new_marc_matchers {
282 my $self = shift;
284 my $dbh = C4::Context->dbh();
285 my $sth = $dbh->prepare_cached("INSERT INTO marc_matchers
286 (code, description, record_type, threshold)
287 VALUES (?, ?, ?, ?)");
288 $sth->execute($self->{'code'}, $self->{'description'}, $self->{'record_type'}, $self->{'threshold'});
289 $self->{'id'} = $dbh->{'mysql_insertid'};
292 sub _store_matcher_components {
293 my $self = shift;
295 my $dbh = C4::Context->dbh();
296 my $sth;
297 my $matcher_id = $self->{'id'};
298 foreach my $matchpoint (@{ $self->{'matchpoints'}}) {
299 my $matchpoint_id = $self->_store_matchpoint($matchpoint);
300 $sth = $dbh->prepare_cached("INSERT INTO matcher_matchpoints (matcher_id, matchpoint_id)
301 VALUES (?, ?)");
302 $sth->execute($matcher_id, $matchpoint_id);
304 foreach my $matchcheck (@{ $self->{'required_checks'} }) {
305 my $source_matchpoint_id = $self->_store_matchpoint($matchcheck->{'source_matchpoint'});
306 my $target_matchpoint_id = $self->_store_matchpoint($matchcheck->{'target_matchpoint'});
307 $sth = $dbh->prepare_cached("INSERT INTO matchchecks
308 (matcher_id, source_matchpoint_id, target_matchpoint_id)
309 VALUES (?, ?, ?)");
310 $sth->execute($matcher_id, $source_matchpoint_id, $target_matchpoint_id);
315 sub _store_matchpoint {
316 my $self = shift;
317 my $matchpoint = shift;
319 my $dbh = C4::Context->dbh();
320 my $sth;
321 my $matcher_id = $self->{'id'};
322 $sth = $dbh->prepare_cached("INSERT INTO matchpoints (matcher_id, search_index, score)
323 VALUES (?, ?, ?)");
324 $sth->execute($matcher_id, $matchpoint->{'index'}, $matchpoint->{'score'});
325 my $matchpoint_id = $dbh->{'mysql_insertid'};
326 my $seqnum = 0;
327 foreach my $component (@{ $matchpoint->{'components'} }) {
328 $seqnum++;
329 $sth = $dbh->prepare_cached("INSERT INTO matchpoint_components
330 (matchpoint_id, sequence, tag, subfields, offset, length)
331 VALUES (?, ?, ?, ?, ?, ?)");
332 $sth->bind_param(1, $matchpoint_id);
333 $sth->bind_param(2, $seqnum);
334 $sth->bind_param(3, $component->{'tag'});
335 $sth->bind_param(4, join "", sort keys %{ $component->{'subfields'} });
336 $sth->bind_param(5, $component->{'offset'});
337 $sth->bind_param(6, $component->{'length'});
338 $sth->execute();
339 my $matchpoint_component_id = $dbh->{'mysql_insertid'};
340 my $normseq = 0;
341 foreach my $norm (@{ $component->{'norms'} }) {
342 $normseq++;
343 $sth = $dbh->prepare_cached("INSERT INTO matchpoint_component_norms
344 (matchpoint_component_id, sequence, norm_routine)
345 VALUES (?, ?, ?)");
346 $sth->execute($matchpoint_component_id, $normseq, $norm);
349 return $matchpoint_id;
353 =head2 delete
355 C4::Matcher->delete($id);
357 Deletes the matcher of the specified ID
358 from the database.
360 =cut
362 sub delete {
363 my $class = shift;
364 my $matcher_id = shift;
366 my $dbh = C4::Context->dbh;
367 my $sth = $dbh->prepare("DELETE FROM marc_matchers WHERE matcher_id = ?");
368 $sth->execute($matcher_id); # relying on cascading deletes to clean up everything
371 =head2 threshold
373 $matcher->threshold(1000);
374 my $threshold = $matcher->threshold();
376 Accessor method.
378 =cut
380 sub threshold {
381 my $self = shift;
382 @_ ? $self->{'threshold'} = shift : $self->{'threshold'};
385 =head2 _id
387 $matcher->_id(123);
388 my $id = $matcher->_id();
390 Accessor method. Note that using this method
391 to set the DB ID of the matcher should not be
392 done outside of the editing CGI.
394 =cut
396 sub _id {
397 my $self = shift;
398 @_ ? $self->{'id'} = shift : $self->{'id'};
401 =head2 code
403 $matcher->code('ISBN');
404 my $code = $matcher->code();
406 Accessor method.
408 =cut
410 sub code {
411 my $self = shift;
412 @_ ? $self->{'code'} = shift : $self->{'code'};
415 =head2 description
417 $matcher->description('match on ISBN');
418 my $description = $matcher->description();
420 Accessor method.
422 =cut
424 sub description {
425 my $self = shift;
426 @_ ? $self->{'description'} = shift : $self->{'description'};
429 =head2 add_matchpoint
431 $matcher->add_matchpoint($index, $score, $matchcomponents);
433 Adds a matchpoint that may include multiple components. The $index
434 parameter identifies the index that will be searched, while $score
435 is the weight that will be added if a match is found.
437 $matchcomponents should be a reference to an array of matchpoint
438 compoents, each of which should be a hash containing the following
439 keys:
441 subfields
442 offset
443 length
444 norms
446 The normalization_rules value should in turn be a reference to an
447 array, each element of which should be a reference to a
448 normalization subroutine (under C4::Normalize) to be applied
449 to the source string.
451 =cut
453 sub add_matchpoint {
454 my $self = shift;
455 my ($index, $score, $matchcomponents) = @_;
457 my $matchpoint = {};
458 $matchpoint->{'index'} = $index;
459 $matchpoint->{'score'} = $score;
460 $matchpoint->{'components'} = [];
461 foreach my $input_component (@{ $matchcomponents }) {
462 push @{ $matchpoint->{'components'} }, _parse_match_component($input_component);
464 push @{ $self->{'matchpoints'} }, $matchpoint;
467 =head2 add_simple_matchpoint
469 $matcher->add_simple_matchpoint($index, $score, $source_tag,
470 $source_subfields, $source_offset,
471 $source_length, $source_normalizer);
474 Adds a simple matchpoint rule -- after composing a key based on the source tag and subfields,
475 normalized per the normalization fuction, search the index. All records retrieved
476 will receive the assigned score.
478 =cut
480 sub add_simple_matchpoint {
481 my $self = shift;
482 my ($index, $score, $source_tag, $source_subfields, $source_offset, $source_length, $source_normalizer) = @_;
484 $self->add_matchpoint($index, $score, [
485 { tag => $source_tag, subfields => $source_subfields,
486 offset => $source_offset, 'length' => $source_length,
487 norms => [ $source_normalizer ]
492 =head2 add_required_check
494 $match->add_required_check($source_matchpoint, $target_matchpoint);
496 Adds a required check definition. A required check means that in
497 order for a match to be considered valid, the key derived from the
498 source (incoming) record must match the key derived from the target
499 (already in DB) record.
501 Unlike a regular matchpoint, only the first repeat of each tag
502 in the source and target match criteria are considered.
504 A typical example of a required check would be verifying that the
505 titles and publication dates match.
507 $source_matchpoint and $target_matchpoint are each a reference to
508 an array of hashes, where each hash follows the same definition
509 as the matchpoint component specification in add_matchpoint, i.e.,
512 subfields
513 offset
514 length
515 norms
517 The normalization_rules value should in turn be a reference to an
518 array, each element of which should be a reference to a
519 normalization subroutine (under C4::Normalize) to be applied
520 to the source string.
522 =cut
524 sub add_required_check {
525 my $self = shift;
526 my ($source_matchpoint, $target_matchpoint) = @_;
528 my $matchcheck = {};
529 $matchcheck->{'source_matchpoint'}->{'index'} = '';
530 $matchcheck->{'source_matchpoint'}->{'score'} = 0;
531 $matchcheck->{'source_matchpoint'}->{'components'} = [];
532 $matchcheck->{'target_matchpoint'}->{'index'} = '';
533 $matchcheck->{'target_matchpoint'}->{'score'} = 0;
534 $matchcheck->{'target_matchpoint'}->{'components'} = [];
535 foreach my $input_component (@{ $source_matchpoint }) {
536 push @{ $matchcheck->{'source_matchpoint'}->{'components'} }, _parse_match_component($input_component);
538 foreach my $input_component (@{ $target_matchpoint }) {
539 push @{ $matchcheck->{'target_matchpoint'}->{'components'} }, _parse_match_component($input_component);
541 push @{ $self->{'required_checks'} }, $matchcheck;
544 =head2 add_simple_required_check
546 $matcher->add_simple_required_check($source_tag, $source_subfields,
547 $source_offset, $source_length, $source_normalizer,
548 $target_tag, $target_subfields, $target_offset,
549 $target_length, $target_normalizer);
551 Adds a required check, which requires that the normalized keys made from the source and targets
552 must match for a match to be considered valid.
554 =cut
556 sub add_simple_required_check {
557 my $self = shift;
558 my ($source_tag, $source_subfields, $source_offset, $source_length, $source_normalizer,
559 $target_tag, $target_subfields, $target_offset, $target_length, $target_normalizer) = @_;
561 $self->add_required_check(
562 [ { tag => $source_tag, subfields => $source_subfields, offset => $source_offset, 'length' => $source_length,
563 norms => [ $source_normalizer ] } ],
564 [ { tag => $target_tag, subfields => $target_subfields, offset => $target_offset, 'length' => $target_length,
565 norms => [ $target_normalizer ] } ]
569 =head2 find_matches
571 my @matches = $matcher->get_matches($marc_record, $max_matches);
572 foreach $match (@matches) {
573 # matches already sorted in order of
574 # decreasing score
575 print "record ID: $match->{'record_id'};
576 print "score: $match->{'score'};
579 Identifies all of the records matching the given MARC record. For a record already
580 in the database to be considered a match, it must meet the following criteria:
582 =over 2
584 =item 1. Total score from its matching field must exceed the supplied threshold.
586 =item 2. It must pass all required checks.
588 =back
590 Only the top $max_matches matches are returned. The returned array is sorted
591 in order of decreasing score, i.e., the best match is first.
593 =cut
595 sub get_matches {
596 my $self = shift;
597 my ($source_record, $max_matches) = @_;
599 my %matches = ();
601 foreach my $matchpoint (@{ $self->{'matchpoints'} }) {
602 my @source_keys = _get_match_keys($source_record, $matchpoint);
603 next if scalar(@source_keys) == 0;
604 # build query
605 my $query = join(" or ", map { "$matchpoint->{'index'}=$_" } @source_keys);
606 # FIXME only searching biblio index at the moment
607 my ($error, $searchresults, $total_hits) = SimpleSearch($query, 0, $max_matches);
609 if (defined $error ) {
610 warn "search failed ($query) $error";
611 } else {
612 foreach my $matched (@{$searchresults}) {
613 $matches{$matched} += $matchpoint->{'score'};
618 # get rid of any that don't meet the threshold
619 %matches = map { ($matches{$_} >= $self->{'threshold'}) ? ($_ => $matches{$_}) : () } keys %matches;
621 # get rid of any that don't meet the required checks
622 %matches = map { _passes_required_checks($source_record, $_, $self->{'required_checks'}) ? ($_ => $matches{$_}) : () }
623 keys %matches;
625 my @results = ();
626 foreach my $marcblob (keys %matches) {
627 my $target_record = MARC::Record->new_from_usmarc($marcblob);
628 my $result = TransformMarcToKoha(C4::Context->dbh, $target_record, '');
629 # FIXME - again, bibliospecific
630 # also, can search engine be induced to give just the number in the first place?
631 my $record_number = $result->{'biblionumber'};
632 push @results, { 'record_id' => $record_number, 'score' => $matches{$marcblob} };
634 @results = sort { $b->{'score'} cmp $a->{'score'} } @results;
635 if (scalar(@results) > $max_matches) {
636 @results = @results[0..$max_matches-1];
638 return @results;
642 =head2 dump
644 $description = $matcher->dump();
646 Returns a reference to a structure containing all of the information
647 in the matcher object. This is mainly a convenience method to
648 aid setting up a HTML editing form.
650 =cut
652 sub dump {
653 my $self = shift;
655 my $result = {};
657 $result->{'matcher_id'} = $self->{'id'};
658 $result->{'code'} = $self->{'code'};
659 $result->{'description'} = $self->{'description'};
661 $result->{'matchpoints'} = [];
662 foreach my $matchpoint (@{ $self->{'matchpoints'} }) {
663 push @{ $result->{'matchpoints'} }, $matchpoint;
665 $result->{'matchchecks'} = [];
666 foreach my $matchcheck (@{ $self->{'required_checks'} }) {
667 push @{ $result->{'matchchecks'} }, $matchcheck;
670 return $result;
673 sub _passes_required_checks {
674 my ($source_record, $target_blob, $matchchecks) = @_;
675 my $target_record = MARC::Record->new_from_usmarc($target_blob); # FIXME -- need to avoid parsing record twice
677 # no checks supplied == automatic pass
678 return 1 if $#{ $matchchecks } == -1;
680 foreach my $matchcheck (@{ $matchchecks }) {
681 my $source_key = join "", _get_match_keys($source_record, $matchcheck->{'source_matchpoint'});
682 my $target_key = join "", _get_match_keys($target_record, $matchcheck->{'target_matchpoint'});
683 return 0 unless $source_key eq $target_key;
685 return 1;
688 sub _get_match_keys {
689 my $source_record = shift;
690 my $matchpoint = shift;
691 my $check_only_first_repeat = @_ ? shift : 0;
693 # If there is more than one component to the matchpoint (e.g.,
694 # matchpoint includes both 003 and 001), any repeats
695 # of the first component's tag are identified; repeats
696 # of the subsequent components' tags are appended to
697 # each parallel key dervied from the first component,
698 # up to the number of repeats of the first component's tag.
700 # For example, if the record has one 003 and two 001s, only
701 # one key is retrieved because there is only one 003. The key
702 # will consist of the contents of the first 003 and first 001.
704 # If there are two 003s and two 001s, there will be two keys:
705 # first 003 + first 001
706 # second 003 + second 001
708 my @keys = ();
709 for (my $i = 0; $i <= $#{ $matchpoint->{'components'} }; $i++) {
710 my $component = $matchpoint->{'components'}->[$i];
711 my $j = -1;
712 FIELD: foreach my $field ($source_record->field($component->{'tag'})) {
713 $j++;
714 last FIELD if $j > 0 and $check_only_first_repeat;
715 last FIELD if $i > 0 and $j > $#keys;
716 my $key = "";
717 my $string;
718 if ($field->is_control_field()) {
719 $string=$field->data();
720 } else {
721 foreach my $subfield ($field->subfields()) {
722 if (exists $component->{'subfields'}->{$subfield->[0]}) {
723 $string .= " " . $subfield->[1];
727 if ($component->{'length'}>0) {
728 $string= substr($string, $component->{'offset'}, $component->{'length'});
729 # FIXME normalize, substr
730 } elsif ($component->{'offset'}) {
731 $string= substr($string, $component->{'offset'});
733 $key = _normalize($string);
734 if ($i == 0) {
735 push @keys, $key if $key;
736 } else {
737 $keys[$j] .= " $key" if $key;
741 return @keys;
745 sub _parse_match_component {
746 my $input_component = shift;
748 my $component = {};
749 $component->{'tag'} = $input_component->{'tag'};
750 $component->{'subfields'} = { map { $_ => 1 } split(//, $input_component->{'subfields'}) };
751 $component->{'offset'} = exists($input_component->{'offset'}) ? $input_component->{'offset'} : -1;
752 $component->{'length'} = $input_component->{'length'} ? $input_component->{'length'} : 0;
753 $component->{'norms'} = $input_component->{'norms'} ? $input_component->{'norms'} : [];
755 return $component;
758 # FIXME - default normalizer
759 sub _normalize {
760 my $value = uc shift;
761 $value =~ s/[.;:,\]\[\)\(\/'"]//g;
762 $value =~ s/^\s+//;
763 #$value =~ s/^\s+$//;
764 $value =~ s/\s+$//;
765 $value =~ s/\s+/ /g;
766 #$value =~ s/[.;,\]\[\)\(\/"']//g;
767 return $value;
771 __END__
773 =head1 AUTHOR
775 Koha Development Team <http://koha-community.org/>
777 Galen Charlton <galen.charlton@liblime.com>
779 =cut