Koha/Koha/SimpleMARC.pm
Jonathan Druart b969aa3fa3 Bug 11413: Fix field_numbers
This fix is a global fix for the MarcModificationTemplate feature.
Some unit tests were missing and some behaviors were wrong.
For instance, if you tried to update a non existent field, the script
crashed.

The following line was completely stupid:
    if $from_field ne $to_subfield

The field_number equals 1 if the user wants to update the first field
and 0 for all fields.

The field_numbers (note the s) variable contains the field numbers to
update. This array is filled if a condition exists (field exists or
field equals).

Signed-off-by: Brendan Gallagher <brendan@bywatersolutions.com>
Signed-off-by: Marcel de Rooy <m.de.rooy@rijksmuseum.nl>
Signed-off-by: Tomas Cohen Arazi <tomascohen@gmail.com>
2014-11-14 12:05:47 -03:00

604 lines
17 KiB
Perl

package Koha::SimpleMARC;
# Copyright 2009 Kyle M. Hall <kyle.m.hall@gmail.com>
use Modern::Perl;
#use MARC::Record;
require Exporter;
our @ISA = qw(Exporter);
our %EXPORT_TAGS = ( 'all' => [ qw(
) ] );
our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
our @EXPORT = qw(
read_field
update_field
copy_field
move_field
delete_field
field_exists
field_equals
);
our $VERSION = '0.01';
our $debug = 0;
=head1 NAME
SimpleMARC - Perl module for making simple MARC record alterations.
=head1 SYNOPSIS
use SimpleMARC;
=head1 DESCRIPTION
SimpleMARC is designed to make writing scripts
to modify MARC records simple and easy.
Every function in the modules requires a
MARC::Record object as its first parameter.
=head1 AUTHOR
Kyle Hall <lt>kyle.m.hall@gmail.com<gt>
=head1 COPYRIGHT AND LICENSE
Copyright (C) 2009 by Kyle Hall
This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself, either Perl version 5.8.7 or,
at your option, any later version of Perl 5 you may have available.
=head1 FUNCTIONS
=head2 copy_field
copy_field( $record, $fromFieldName, $fromSubfieldName, $toFieldName, $toSubfieldName[, $regex[, $n ] ] );
Copies a value from one field to another. If a regular expression ( $regex ) is supplied,
the value will be transformed by the given regex before being copied into the new field.
Example: $regex = { search => 'Old Text', replace => 'Replacement Text', modifiers => 'g' };
If $n is passed, copy_field will only copy the Nth field of the list of fields.
E.g. $n = 1 will only use the first field's value, $n = 2 will use only the 2nd field's value.
=cut
sub copy_field {
my ( $params ) = @_;
my $record = $params->{record};
my $fromFieldName = $params->{from_field};
my $fromSubfieldName = $params->{from_subfield};
my $toFieldName = $params->{to_field};
my $toSubfieldName = $params->{to_subfield};
my $regex = $params->{regex};
my $field_numbers = $params->{field_numbers} // [];
if ( ! ( $record && $fromFieldName && $toFieldName ) ) { return; }
if ( not $fromSubfieldName or $fromSubfieldName eq ''
or not $toSubfieldName or $toSubfieldName eq ''
) {
_copy_field({
record => $record,
from_field => $fromFieldName,
to_field => $toFieldName,
regex => $regex,
field_numbers => $field_numbers,
});
} else {
_copy_subfield({
record => $record,
from_field => $fromFieldName,
from_subfield => $fromSubfieldName,
to_field => $toFieldName,
to_subfield => $toSubfieldName,
regex => $regex,
field_numbers => $field_numbers,
});
}
}
sub _copy_field {
my ( $params ) = @_;
my $record = $params->{record};
my $fromFieldName = $params->{from_field};
my $toFieldName = $params->{to_field};
my $regex = $params->{regex};
my $field_numbers = $params->{field_numbers} // [];
_copy_move_field({
record => $record,
from_field => $fromFieldName,
to_field => $toFieldName,
regex => $regex,
field_numbers => $field_numbers,
});
}
sub _copy_subfield {
my ( $params ) = @_;
my $record = $params->{record};
my $fromFieldName = $params->{from_field};
my $fromSubfieldName = $params->{from_subfield};
my $toFieldName = $params->{to_field};
my $toSubfieldName = $params->{to_subfield};
my $regex = $params->{regex};
my $field_numbers = $params->{field_numbers} // [];
my @values = read_field({ record => $record, field => $fromFieldName, subfield => $fromSubfieldName });
if ( @$field_numbers ) {
@values = map { $_ <= @values ? $values[ $_ - 1 ] : () } @$field_numbers;
}
_modify_values({ values => \@values, regex => $regex });
update_field({ record => $record, field => $toFieldName, subfield => $toSubfieldName, values => \@values });
}
sub update_field {
my ( $params ) = @_;
my $record = $params->{record};
my $fieldName = $params->{field};
my $subfieldName = $params->{subfield};
my @values = @{ $params->{values} };
my $field_numbers = $params->{field_numbers} // [];
if ( ! ( $record && $fieldName ) ) { return; }
if ( not $subfieldName or $subfieldName eq '' ) {
# FIXME I'm not sure the actual implementation is correct.
die "This action is not implemented yet";
#_update_field({ record => $record, field => $fieldName, values => \@values });
} else {
_update_subfield({ record => $record, field => $fieldName, subfield => $subfieldName, values => \@values, field_numbers => $field_numbers });
}
}
sub _update_field {
my ( $params ) = @_;
my $record = $params->{record};
my $fieldName = $params->{field};
my @values = @{ $params->{values} };
my $i = 0;
if ( my @fields = $record->field( $fieldName ) ) {
@values = ($values[0]) x scalar( @fields )
if @values == 1;
foreach my $field ( @fields ) {
$field->update( $values[$i++] );
}
} else {
## Field does not exists, create it
if ( $fieldName < 10 ) {
foreach my $value ( @values ) {
my $field = MARC::Field->new( $fieldName, $value );
$record->append_fields( $field );
}
} else {
warn "Invalid operation, trying to add a new field without subfield";
}
}
}
sub _update_subfield {
my ( $params ) = @_;
my $record = $params->{record};
my $fieldName = $params->{field};
my $subfieldName = $params->{subfield};
my @values = @{ $params->{values} };
my $dont_erase = $params->{dont_erase};
my $field_numbers = $params->{field_numbers} // [];
my $i = 0;
my @fields = $record->field( $fieldName );
if ( @$field_numbers ) {
@fields = map { $_ <= @fields ? $fields[ $_ - 1 ] : () } @$field_numbers;
}
if ( @fields ) {
unless ( $dont_erase ) {
@values = ($values[0]) x scalar( @fields )
if @values == 1;
foreach my $field ( @fields ) {
$field->update( "$subfieldName" => $values[$i++] );
}
}
if ( $i <= scalar ( @values ) - 1 ) {
foreach my $field ( @fields ) {
foreach my $j ( $i .. scalar( @values ) - 1) {
$field->add_subfields( "$subfieldName" => $values[$j] );
}
}
}
} else {
## Field does not exist, create it.
foreach my $value ( @values ) {
my $field = MARC::Field->new( $fieldName, '', '', "$subfieldName" => $values[$i++] );
$record->append_fields( $field );
}
}
}
=head2 read_field
my @values = read_field( $record, $fieldName[, $subfieldName, [, $n ] ] );
Returns an array of field values for the given field and subfield
If $n is given, it will return only the $nth value of the array.
E.g. If $n = 1, it return the 1st value, if $n = 3, it will return the 3rd value.
=cut
sub read_field {
my ( $params ) = @_;
my $record = $params->{record};
my $fieldName = $params->{field};
my $subfieldName = $params->{subfield};
my $field_numbers = $params->{field_numbers} // [];
if ( not $subfieldName or $subfieldName eq '' ) {
_read_field({ record => $record, field => $fieldName, field_numbers => $field_numbers });
} else {
_read_subfield({ record => $record, field => $fieldName, subfield => $subfieldName, field_numbers => $field_numbers });
}
}
sub _read_field {
my ( $params ) = @_;
my $record = $params->{record};
my $fieldName = $params->{field};
my $field_numbers = $params->{field_numbers} // [];
my @fields = $record->field( $fieldName );
return unless @fields;
return map { $_->data() } @fields
if $fieldName < 10;
my @values;
if ( @$field_numbers ) {
for my $field_number ( @$field_numbers ) {
if ( $field_number <= scalar( @fields ) ) {
for my $sf ( $fields[$field_number - 1]->subfields ) {
push @values, $sf->[1];
}
}
}
} else {
foreach my $field ( @fields ) {
for my $sf ( $field->subfields ) {
push @values, $sf->[1];
}
}
}
return @values;
}
sub _read_subfield {
my ( $params ) = @_;
my $record = $params->{record};
my $fieldName = $params->{field};
my $subfieldName = $params->{subfield};
my $field_numbers = $params->{field_numbers} // [];
my @fields = $record->field( $fieldName );
return unless @fields;
my @values;
foreach my $field ( @fields ) {
my @sf = $field->subfield( $subfieldName );
push( @values, @sf );
}
if ( @values and @$field_numbers ) {
@values = map { $_ <= @values ? $values[ $_ - 1 ] : () } @$field_numbers;
}
return @values;
}
=head2 field_exists
@field_numbers = field_exists( $record, $fieldName[, $subfieldName ]);
Returns the field numbers or an empty array.
=cut
sub field_exists {
my ( $params ) = @_;
my $record = $params->{record};
my $fieldName = $params->{field};
my $subfieldName = $params->{subfield};
if ( ! $record ) { return; }
my @field_numbers = ();
my $current_field_number = 1;
for my $field ( $record->field( $fieldName ) ) {
if ( $subfieldName ) {
push @field_numbers, $current_field_number
if $field->subfield( $subfieldName );
} else {
push @field_numbers, $current_field_number;
}
$current_field_number++;
}
return \@field_numbers;
}
=head2 field_equals
$bool = field_equals( $record, $value, $fieldName[, $subfieldName[, $regex ] ]);
Returns true if the field equals the given value, false otherwise.
If a regular expression ( $regex ) is supplied, the value will be compared using
the given regex. Example: $regex = 'sought_text'
=cut
sub field_equals {
my ( $params ) = @_;
my $record = $params->{record};
my $value = $params->{value};
my $fieldName = $params->{field};
my $subfieldName = $params->{subfield};
my $is_regex = $params->{is_regex};
if ( ! $record ) { return; }
my @field_numbers = ();
my $current_field_number = 1;
FIELDS: for my $field ( $record->field( $fieldName ) ) {
my @subfield_values = $subfieldName
? $field->subfield( $subfieldName )
: map { $_->[1] } $field->subfields;
SUBFIELDS: for my $subfield_value ( @subfield_values ) {
if (
(
$is_regex and $subfield_value =~ m/$value/
) or (
$subfield_value eq $value
)
) {
push @field_numbers, $current_field_number;
last SUBFIELDS;
}
}
$current_field_number++;
}
return \@field_numbers;
}
=head2 move_field
move_field( $record, $fromFieldName, $fromSubfieldName, $toFieldName, $toSubfieldName[, $regex [, $n ] ] );
Moves a value from one field to another. If a regular expression ( $regex ) is supplied,
the value will be transformed by the given regex before being moved into the new field.
Example: $regex = 's/Old Text/Replacement Text/'
If $n is passed, only the Nth field will be moved. $n = 1
will move the first repeatable field, $n = 3 will move the third.
=cut
sub move_field {
my ( $params ) = @_;
my $record = $params->{record};
my $fromFieldName = $params->{from_field};
my $fromSubfieldName = $params->{from_subfield};
my $toFieldName = $params->{to_field};
my $toSubfieldName = $params->{to_subfield};
my $regex = $params->{regex};
my $field_numbers = $params->{field_numbers} // [];
if ( not $fromSubfieldName or $fromSubfieldName eq ''
or not $toSubfieldName or $toSubfieldName eq ''
) {
_move_field({
record => $record,
from_field => $fromFieldName,
to_field => $toFieldName,
regex => $regex,
field_numbers => $field_numbers,
});
} else {
_move_subfield({
record => $record,
from_field => $fromFieldName,
from_subfield => $fromSubfieldName,
to_field => $toFieldName,
to_subfield => $toSubfieldName,
regex => $regex,
field_numbers => $field_numbers,
});
}
}
sub _move_field {
my ( $params ) = @_;
my $record = $params->{record};
my $fromFieldName = $params->{from_field};
my $toFieldName = $params->{to_field};
my $regex = $params->{regex};
my $field_numbers = $params->{field_numbers} // [];
_copy_move_field({
record => $record,
from_field => $fromFieldName,
to_field => $toFieldName,
regex => $regex,
field_numbers => $field_numbers,
action => 'move',
});
}
sub _move_subfield {
my ( $params ) = @_;
my $record = $params->{record};
my $fromFieldName = $params->{from_field};
my $fromSubfieldName = $params->{from_subfield};
my $toFieldName = $params->{to_field};
my $toSubfieldName = $params->{to_subfield};
my $regex = $params->{regex};
my $field_numbers = $params->{field_numbers} // [];
# Copy
my @values = read_field({ record => $record, field => $fromFieldName, subfield => $fromSubfieldName });
if ( @$field_numbers ) {
@values = map { $_ <= @values ? $values[ $_ - 1 ] : () } @$field_numbers;
}
_modify_values({ values => \@values, regex => $regex });
_update_subfield({ record => $record, field => $toFieldName, subfield => $toSubfieldName, dont_erase => 1, values => \@values });
# And delete
_delete_subfield({
record => $record,
field => $fromFieldName,
subfield => $fromSubfieldName,
field_numbers => $field_numbers,
});
}
=head2 _delete_field
_delete_field( $record, $fieldName[, $subfieldName [, $n ] ] );
Deletes the given field.
If $n is passed, only the Nth field will be deleted. $n = 1
will delete the first repeatable field, $n = 3 will delete the third.
=cut
sub delete_field {
my ( $params ) = @_;
my $record = $params->{record};
my $fieldName = $params->{field};
my $subfieldName = $params->{subfield};
my $field_numbers = $params->{field_numbers} // [];
if ( not $subfieldName or $subfieldName eq '' ) {
_delete_field({ record => $record, field => $fieldName, field_numbers => $field_numbers });
} else {
_delete_subfield({ record => $record, field => $fieldName, subfield => $subfieldName, field_numbers => $field_numbers });
}
}
sub _delete_field {
my ( $params ) = @_;
my $record = $params->{record};
my $fieldName = $params->{field};
my $field_numbers = $params->{field_numbers} // [];
my @fields = $record->field( $fieldName );
if ( @$field_numbers ) {
@fields = map { $_ <= @fields ? $fields[ $_ - 1 ] : () } @$field_numbers;
}
foreach my $field ( @fields ) {
$record->delete_field( $field );
}
}
sub _delete_subfield {
my ( $params ) = @_;
my $record = $params->{record};
my $fieldName = $params->{field};
my $subfieldName = $params->{subfield};
my $field_numbers = $params->{field_numbers} // [];
my @fields = $record->field( $fieldName );
if ( @$field_numbers ) {
@fields = map { $_ <= @fields ? $fields[ $_ - 1 ] : () } @$field_numbers;
}
foreach my $field ( @fields ) {
$field->delete_subfield( code => $subfieldName );
}
}
sub _copy_move_field {
my ( $params ) = @_;
my $record = $params->{record};
my $fromFieldName = $params->{from_field};
my $toFieldName = $params->{to_field};
my $regex = $params->{regex};
my $field_numbers = $params->{field_numbers} // [];
my $action = $params->{action} || 'copy';
my @fields = $record->field( $fromFieldName );
if ( @$field_numbers ) {
@fields = map { $_ <= @fields ? $fields[ $_ - 1 ] : () } @$field_numbers;
}
for my $field ( @fields ) {
my $new_field = $field->clone;
$new_field->{_tag} = $toFieldName; # Should be replaced by set_tag, introduced by MARC::Field 2.0.4
if ( $regex and $regex->{search} ) {
for my $subfield ( $new_field->subfields ) {
my $value = $subfield->[1];
( $value ) = _modify_values({ values => [ $value ], regex => $regex });
$new_field->update( $subfield->[0], $value );
}
}
$record->append_fields( $new_field );
$record->delete_field( $field )
if $action eq 'move';
}
}
sub _modify_values {
my ( $params ) = @_;
my $values = $params->{values};
my $regex = $params->{regex};
if ( $regex and $regex->{search} ) {
$regex->{modifiers} //= q||;
my @available_modifiers = qw( i g );
my $modifiers = q||;
for my $modifier ( split //, $regex->{modifiers} ) {
$modifiers .= $modifier
if grep {/$modifier/} @available_modifiers;
}
foreach my $value ( @$values ) {
if ( $modifiers =~ m/^(ig|gi)$/ ) {
$value =~ s/$regex->{search}/$regex->{replace}/ig;
}
elsif ( $modifiers eq 'i' ) {
$value =~ s/$regex->{search}/$regex->{replace}/i;
}
elsif ( $modifiers eq 'g' ) {
$value =~ s/$regex->{search}/$regex->{replace}/g;
}
else {
$value =~ s/$regex->{search}/$regex->{replace}/;
}
}
}
return @$values;
}
1;
__END__