push version to 0.24
[Biblio-Isis] / lib / Biblio / Isis.pm
1 package Biblio::Isis;
2 use strict;
3
4 use Carp;
5 use File::Glob qw(:globally :nocase);
6
7 BEGIN {
8         use Exporter ();
9         use vars qw ($VERSION @ISA @EXPORT @EXPORT_OK %EXPORT_TAGS);
10         $VERSION     = 0.24;
11         @ISA         = qw (Exporter);
12         #Give a hoot don't pollute, do not export more than needed by default
13         @EXPORT      = qw ();
14         @EXPORT_OK   = qw ();
15         %EXPORT_TAGS = ();
16
17 }
18
19 =head1 NAME
20
21 Biblio::Isis - Read CDS/ISIS, WinISIS and IsisMarc database
22
23 =head1 SYNOPSIS
24
25   use Biblio::Isis;
26
27   my $isis = new Biblio::Isis(
28         isisdb => './cds/cds',
29   );
30
31   for(my $mfn = 1; $mfn <= $isis->count; $mfn++) {
32         print $isis->to_ascii($mfn),"\n";
33   }
34
35 =head1 DESCRIPTION
36
37 This module will read ISIS databases created by DOS CDS/ISIS, WinIsis or
38 IsisMarc. It can be used as perl-only alternative to OpenIsis module which
39 seems to depriciate it's old C<XS> bindings for perl.
40
41 It can create hash values from data in ISIS database (using C<to_hash>),
42 ASCII dump (using C<to_ascii>) or just hash with field names and packed
43 values (like C<^asomething^belse>).
44
45 Unique feature of this module is ability to C<include_deleted> records.
46 It will also skip zero sized fields (OpenIsis has a bug in XS bindings, so
47 fields which are zero sized will be filled with random junk from memory).
48
49 It also has support for identifiers (only if ISIS database is created by
50 IsisMarc), see C<to_hash>.
51
52 This module will always be slower than OpenIsis module which use C
53 library. However, since it's written in perl, it's platform independent (so
54 you don't need C compiler), and can be easily modified. I hope that it
55 creates data structures which are easier to use than ones created by
56 OpenIsis, so reduced time in other parts of the code should compensate for
57 slower performance of this module (speed of reading ISIS database is
58 rarely an issue).
59
60 =head1 METHODS
61
62 =cut
63
64 #  my $ORDN;            # Nodes Order
65 #  my $ORDF;            # Leafs Order
66 #  my $N;               # Number of Memory buffers for nodes
67 #  my $K;               # Number of buffers for first level index
68 #  my $LIV;             # Current number of Index Levels
69 #  my $POSRX;           # Pointer to Root Record in N0x
70 #  my $NMAXPOS;         # Next Available position in N0x
71 #  my $FMAXPOS;         # Next available position in L0x
72 #  my $ABNORMAL;        # Formal BTree normality indicator
73
74 #
75 # some binary reads
76 #
77
78 =head2 new
79
80 Open ISIS database
81
82  my $isis = new Biblio::Isis(
83         isisdb => './cds/cds',
84         read_fdt => 1,
85         include_deleted => 1,
86         hash_filter => sub {
87                 my ($v,$field_number) = @_;
88                 $v =~ s#foo#bar#g;
89         },
90         debug => 1,
91         join_subfields_with => ' ; ',
92  );
93
94 Options are described below:
95
96 =over 5
97
98 =item isisdb
99
100 This is full or relative path to ISIS database files which include
101 common prefix of C<.MST>, and C<.XRF> and optionally C<.FDT> (if using
102 C<read_fdt> option) files.
103
104 In this example it uses C<./cds/cds.MST> and related files.
105
106 =item read_fdt
107
108 Boolean flag to specify if field definition table should be read. It's off
109 by default.
110
111 =item include_deleted
112
113 Don't skip logically deleted records in ISIS.
114
115 =item hash_filter
116
117 Filter code ref which will be used before data is converted to hash. It will
118 receive two arguments, whole line from current field (in C<< $_[0] >>) and
119 field number (in C<< $_[1] >>).
120
121 =item debug
122
123 Dump a B<lot> of debugging output even at level 1. For even more increase level.
124
125 =item join_subfields_with
126
127 Define delimiter which will be used to join repeatable subfields. This
128 option is included to support lagacy application written against version
129 older than 0.21 of this module. By default, it disabled. See L</to_hash>.
130
131 =item ignore_empty_subfields
132
133 Remove all empty subfields while reading from ISIS file.
134
135 =back
136
137 =cut
138
139 sub new {
140         my $class = shift;
141         my $self = {};
142         bless($self, $class);
143
144         croak "new needs database name (isisdb) as argument!" unless ({@_}->{isisdb});
145
146         foreach my $v (qw{isisdb debug include_deleted hash_filter join_subfields_with ignore_empty_subfields}) {
147                 $self->{$v} = {@_}->{$v} if defined({@_}->{$v});
148         }
149
150         my @isis_files = grep(/\.(FDT|MST|XRF|CNT)$/i,glob($self->{isisdb}."*"));
151
152         foreach my $f (@isis_files) {
153                 my $ext = $1 if ($f =~ m/\.(\w\w\w)$/);
154                 $self->{lc($ext)."_file"} = $f;
155         }
156
157         my @must_exist = qw(mst xrf);
158         push @must_exist, "fdt" if ($self->{read_fdt});
159
160         foreach my $ext (@must_exist) {
161                 unless ($self->{$ext."_file"}) {
162                         carp "missing ",uc($ext)," file in ",$self->{isisdb};
163                         return;
164                 }
165         }
166
167         if ($self->{debug}) {
168                 print STDERR "## using files: ",join(" ",@isis_files),"\n";
169                 eval "use Data::Dump";
170
171                 if (! $@) {
172                         *Dumper = *Data::Dump::dump;
173                 } else {
174                         use Data::Dumper;
175                 }
176         }
177
178         # if you want to read .FDT file use read_fdt argument when creating class!
179         if ($self->{read_fdt} && -e $self->{fdt_file}) {
180
181                 # read the $db.FDT file for tags
182                 my $fieldzone=0;
183
184                 open(my $fileFDT, $self->{fdt_file}) || croak "can't read '$self->{fdt_file}': $!";
185                 binmode($fileFDT);
186
187                 while (<$fileFDT>) {
188                         chomp;
189                         if ($fieldzone) {
190                                 my $name=substr($_,0,30);
191                                 my $tag=substr($_,50,3);
192
193                                 $name =~ s/\s+$//;
194                                 $tag =~ s/\s+$//;
195
196                                 $self->{'TagName'}->{$tag}=$name;  
197                         }
198
199                         if (/^\*\*\*/) {
200                                 $fieldzone=1;
201                         }
202                 }
203                 
204                 close($fileFDT);
205         }
206
207         # Get the Maximum MFN from $db.MST
208
209         open($self->{'fileMST'}, $self->{mst_file}) || croak "can't open '$self->{mst_file}': $!";
210         binmode($self->{'fileMST'});
211
212         # MST format:   (* = 32 bit signed)
213         # CTLMFN*       always 0
214         # NXTMFN*       MFN to be assigned to the next record created
215         # NXTMFB*       last block allocated to master file
216         # NXTMFP        offset to next available position in last block
217         # MFTYPE        always 0 for user db file (1 for system)
218         seek($self->{'fileMST'},4,0) || croak "can't seek to offset 0 in MST: $!";
219
220         my $buff;
221
222         read($self->{'fileMST'}, $buff, 4) || croak "can't read NXTMFN from MST: $!";
223         $self->{'NXTMFN'}=unpack("V",$buff) || croak "NXTNFN is zero";
224
225         print STDERR "## self ",Dumper($self),"\n" if ($self->{debug});
226
227         # open files for later
228         open($self->{'fileXRF'}, $self->{xrf_file}) || croak "can't open '$self->{xrf_file}': $!";
229         binmode($self->{'fileXRF'});
230
231         $self ? return $self : return undef;
232 }
233
234 =head2 count
235
236 Return number of records in database
237
238   print $isis->count;
239
240 =cut
241
242 sub count {
243         my $self = shift;
244         return $self->{'NXTMFN'} - 1;
245 }
246
247 =head2 fetch
248
249 Read record with selected MFN
250
251   my $rec = $isis->fetch(55);
252
253 Returns hash with keys which are field names and values are unpacked values
254 for that field like this:
255
256   $rec = {
257     '210' => [ '^aNew York^cNew York University press^dcop. 1988' ],
258     '990' => [ '2140', '88', 'HAY' ],
259   };
260
261 =cut
262
263 sub fetch {
264         my $self = shift;
265
266         my $mfn = shift || croak "fetch needs MFN as argument!";
267
268         # is mfn allready in memory?
269         my $old_mfn = $self->{'current_mfn'} || -1;
270         return $self->{record} if ($mfn == $old_mfn);
271
272         print STDERR "## fetch: $mfn\n" if ($self->{debug});
273
274         # XXX check this?
275         my $mfnpos=($mfn+int(($mfn-1)/127))*4;
276
277         print STDERR "## seeking to $mfnpos in file '$self->{xrf_file}'\n" if ($self->{debug});
278         seek($self->{'fileXRF'},$mfnpos,0);
279
280         my $buff;
281
282         # delete old record
283         delete $self->{record};
284
285         # read XRFMFB abd XRFMFP
286         read($self->{'fileXRF'}, $buff, 4);
287         my $pointer=unpack("V",$buff);
288         if (! $pointer) {
289                 if ($self->{include_deleted}) {
290                         return;
291                 } else {
292                         warn "pointer for MFN $mfn is null\n";
293                         return;
294                 }
295         }
296
297         # check for logically deleted record
298         if ($pointer & 0x80000000) {
299                 print STDERR "## record $mfn is logically deleted\n" if ($self->{debug});
300                 $self->{deleted} = $mfn;
301
302                 return unless $self->{include_deleted};
303
304                 # abs
305                 $pointer = ($pointer ^ 0xffffffff) + 1;
306         }
307
308         my $XRFMFB = int($pointer/2048);
309         my $XRFMFP = $pointer - ($XRFMFB*2048);
310
311         # (XRFMFB - 1) * 512 + XRFMFP
312         # why do i have to do XRFMFP % 1024 ?
313
314         my $blk_off = (($XRFMFB - 1) * 512) + ($XRFMFP % 512);
315
316         print STDERR "## pointer: $pointer XRFMFB: $XRFMFB XRFMFP: $XRFMFP offset: $blk_off\n" if ($self->{'debug'});
317
318         # Get Record Information
319
320         seek($self->{'fileMST'},$blk_off,0) || croak "can't seek to $blk_off: $!";
321
322         read($self->{'fileMST'}, $buff, 4) || croak "can't read 4 bytes at offset $blk_off from MST file: $!";
323         my $value=unpack("V",$buff);
324
325         print STDERR "## offset for rowid $value is $blk_off (blk $XRFMFB off $XRFMFP)\n" if ($self->{debug});
326
327         if ($value!=$mfn) {
328                 if ($value == 0) {
329                         print STDERR "## record $mfn is physically deleted\n" if ($self->{debug});
330                         $self->{deleted} = $mfn;
331                         return;
332                 }
333
334                 carp "Error: MFN ".$mfn." not found in MST file, found $value";    
335                 return;
336         }
337
338         read($self->{'fileMST'}, $buff, 14);
339
340         my ($MFRL,$MFBWB,$MFBWP,$BASE,$NVF,$STATUS) = unpack("vVvvvv", $buff);
341
342         print STDERR "## MFRL: $MFRL MFBWB: $MFBWB MFBWP: $MFBWP BASE: $BASE NVF: $NVF STATUS: $STATUS\n" if ($self->{debug});
343
344         warn "MFRL $MFRL is not even number" unless ($MFRL % 2 == 0);
345
346         warn "BASE is not 18+6*NVF" unless ($BASE == 18 + 6 * $NVF);
347
348         # Get Directory Format
349
350         my @FieldPOS;
351         my @FieldLEN;
352         my @FieldTAG;
353
354         read($self->{'fileMST'}, $buff, 6 * $NVF);
355
356         my $rec_len = 0;
357
358         for (my $i = 0 ; $i < $NVF ; $i++) {
359
360                 my ($TAG,$POS,$LEN) = unpack("vvv", substr($buff,$i * 6, 6));
361
362                 print STDERR "## TAG: $TAG POS: $POS LEN: $LEN\n" if ($self->{debug});
363
364                 # The TAG does not exists in .FDT so we set it to 0.
365                 #
366                 # XXX This is removed from perl version; .FDT file is updated manually, so
367                 # you will often have fields in .MST file which aren't in .FDT. On the other
368                 # hand, IsisMarc doesn't use .FDT files at all!
369
370                 #if (! $self->{TagName}->{$TAG}) {
371                 #       $TAG=0;
372                 #}
373
374                 push @FieldTAG,$TAG;
375                 push @FieldPOS,$POS;
376                 push @FieldLEN,$LEN;
377
378                 $rec_len += $LEN;
379         }
380
381         # Get Variable Fields
382
383         read($self->{'fileMST'},$buff,$rec_len);
384
385         print STDERR "## rec_len: $rec_len poc: ",tell($self->{'fileMST'})."\n" if ($self->{debug});
386
387         for (my $i = 0 ; $i < $NVF ; $i++) {
388                 # skip zero-sized fields
389                 next if ($FieldLEN[$i] == 0);
390
391                 my $v = substr($buff,$FieldPOS[$i],$FieldLEN[$i]);
392
393                 if ( $self->{ignore_empty_subfields} ) {
394                         $v =~ s/(\^\w)+(\^\w)/$2/g;
395                         $v =~ s/\^\w$//;                        # last on line?
396                         next if ($v eq '');
397                 }
398
399                 push @{$self->{record}->{$FieldTAG[$i]}}, $v;
400         }
401
402         $self->{'current_mfn'} = $mfn;
403
404         print STDERR Dumper($self),"\n" if ($self->{debug});
405
406         return $self->{'record'};
407 }
408
409 =head2 mfn
410
411 Returns current MFN position
412
413   my $mfn = $isis->mfn;
414
415 =cut
416
417 # This function should be simple return $self->{current_mfn},
418 # but if new is called with _hack_mfn it becomes setter.
419 # It's useful in tests when setting $isis->{record} directly
420
421 sub mfn {
422         my $self = shift;
423         return $self->{current_mfn};
424 };
425
426
427 =head2 to_ascii
428
429 Returns ASCII output of record with specified MFN
430
431   print $isis->to_ascii(42);
432
433 This outputs something like this:
434
435   210   ^aNew York^cNew York University press^dcop. 1988
436   990   2140
437   990   88
438   990   HAY
439
440 If C<read_fdt> is specified when calling C<new> it will display field names
441 from C<.FDT> file instead of numeric tags.
442
443 =cut
444
445 sub to_ascii {
446         my $self = shift;
447
448         my $mfn = shift || croak "need MFN";
449
450         my $rec = $self->fetch($mfn) || return;
451
452         my $out = "0\t$mfn";
453
454         foreach my $f (sort keys %{$rec}) {
455                 my $fn = $self->tag_name($f);
456                 $out .= "\n$fn\t".join("\n$fn\t",@{$self->{record}->{$f}});
457         }
458
459         $out .= "\n";
460
461         return $out;
462 }
463
464 =head2 to_hash
465
466 Read record with specified MFN and convert it to hash
467
468   my $hash = $isis->to_hash($mfn);
469
470 It has ability to convert characters (using C<hash_filter>) from ISIS
471 database before creating structures enabling character re-mapping or quick
472 fix-up of data.
473
474 This function returns hash which is like this:
475
476   $hash = {
477     '210' => [
478                {
479                  'c' => 'New York University press',
480                  'a' => 'New York',
481                  'd' => 'cop. 1988'
482                }
483              ],
484     '990' => [
485                '2140',
486                '88',
487                'HAY'
488              ],
489   };
490
491 You can later use that hash to produce any output from ISIS data.
492
493 If database is created using IsisMarc, it will also have to special fields
494 which will be used for identifiers, C<i1> and C<i2> like this:
495
496   '200' => [
497              {
498                'i1' => '1',
499                'i2' => ' '
500                'a' => 'Goa',
501                'f' => 'Valdo D\'Arienzo',
502                'e' => 'tipografie e tipografi nel XVI secolo',
503              }
504            ],
505
506 In case there are repeatable subfields in record, this will create
507 following structure:
508
509   '900' => [ {
510         'a' => [ 'foo', 'bar', 'baz' ],
511   }]
512
513 Or in more complex example of
514
515   902   ^aa1^aa2^aa3^bb1^aa4^bb2^cc1^aa5
516
517 it will create
518
519   902   => [
520         { a => ["a1", "a2", "a3", "a4", "a5"], b => ["b1", "b2"], c => "c1" },
521   ],
522
523 This behaviour can be changed using C<join_subfields_with> option to L</new>,
524 in which case C<to_hash> will always create single value for each subfield.
525 This will change result to:
526
527
528
529 This method will also create additional field C<000> with MFN.
530
531 There is also more elaborative way to call C<to_hash> like this:
532
533   my $hash = $isis->to_hash({
534         mfn => 42,
535         include_subfields => 1,
536   });
537
538 Each option controll creation of hash:
539
540 =over 4
541
542 =item mfn
543
544 Specify MFN number of record
545
546 =item include_subfields
547
548 This option will create additional key in hash called C<subfields> which will
549 have original record subfield order and index to that subfield like this:
550
551   902   => [ {
552         a => ["a1", "a2", "a3", "a4", "a5"],
553         b => ["b1", "b2"],
554         c => "c1",
555         subfields => ["a", 0, "a", 1, "a", 2, "b", 0, "a", 3, "b", 1, "c", 0, "a", 4],
556   } ],
557
558 =item join_subfields_with
559
560 Define delimiter which will be used to join repeatable subfields. You can
561 specify option here instead in L</new> if you want to have per-record control.
562
563 =item hash_filter
564
565 You can override C<hash_filter> defined in L</new> using this option.
566
567 =back
568
569 =cut
570
571 sub to_hash {
572         my $self = shift;
573
574
575         my $mfn = shift || confess "need mfn!";
576         my $arg;
577
578         my $hash_filter = $self->{hash_filter};
579
580         if (ref($mfn) eq 'HASH') {
581                 $arg = $mfn;
582                 $mfn = $arg->{mfn} || confess "need mfn in arguments";
583                 $hash_filter = $arg->{hash_filter} if ($arg->{hash_filter});
584         }
585
586         # init record to include MFN as field 000
587         my $rec = { '000' => [ $mfn ] };
588
589         my $row = $self->fetch($mfn) || return;
590
591         my $j_rs = $arg->{join_subfields_with} || $self->{join_subfields_with};
592         $j_rs = $self->{join_subfields_with} unless(defined($j_rs));
593         my $i_sf = $arg->{include_subfields};
594
595         foreach my $f_nr (keys %{$row}) {
596                 foreach my $l (@{$row->{$f_nr}}) {
597
598                         # filter output
599                         $l = $hash_filter->($l, $f_nr) if ($hash_filter);
600                         next unless defined($l);
601
602                         my $val;
603                         my $r_sf;       # repeatable subfields in this record
604
605                         # has identifiers?
606                         ($val->{'i1'},$val->{'i2'}) = ($1,$2) if ($l =~ s/^([01 #])([01 #])\^/\^/);
607
608                         # has subfields?
609                         if ($l =~ m/\^/) {
610                                 foreach my $t (split(/\^/,$l)) {
611                                         next if (! $t);
612                                         my ($sf,$v) = (substr($t,0,1), substr($t,1));
613                                         # XXX this might be option, but why?
614                                         next unless (defined($v) && $v ne '');
615 #                                       warn "### $f_nr^$sf:$v",$/ if ($self->{debug} > 1);
616
617                                         if (ref( $val->{$sf} ) eq 'ARRAY') {
618
619                                                 push @{ $val->{$sf} }, $v;
620
621                                                 # record repeatable subfield it it's offset
622                                                 push @{ $val->{subfields} }, ( $sf, $#{ $val->{$sf} } ) if (! $j_rs && $i_sf);
623                                                 $r_sf->{$sf}++;
624
625                                         } elsif (defined( $val->{$sf} )) {
626
627                                                 # convert scalar field to array
628                                                 $val->{$sf} = [ $val->{$sf}, $v ];
629
630                                                 push @{ $val->{subfields} }, ( $sf, 1 ) if (! $j_rs && $i_sf);
631                                                 $r_sf->{$sf}++;
632
633                                         } else {
634                                                 $val->{$sf} = $v;
635                                                 push @{ $val->{subfields} }, ( $sf, 0 ) if ($i_sf);
636                                         }
637                                 }
638                         } else {
639                                 $val = $l;
640                         }
641
642                         if ($j_rs) {
643                                 map {
644                                         $val->{$_} = join($j_rs, @{ $val->{$_} });
645                                 } keys %$r_sf
646                         }
647
648                         push @{$rec->{$f_nr}}, $val;
649                 }
650         }
651
652         return $rec;
653 }
654
655 =head2 tag_name
656
657 Return name of selected tag
658
659  print $isis->tag_name('200');
660
661 =cut
662
663 sub tag_name {
664         my $self = shift;
665         my $tag = shift || return;
666         return $self->{'TagName'}->{$tag} || $tag;
667 }
668
669
670 =head2 read_cnt
671
672 Read content of C<.CNT> file and return hash containing it.
673
674   print Dumper($isis->read_cnt);
675
676 This function is not used by module (C<.CNT> files are not required for this
677 module to work), but it can be useful to examine your index (while debugging
678 for example).
679
680 =cut
681
682 sub read_cnt  {
683         my $self = shift;
684
685         croak "missing CNT file in ",$self->{isisdb} unless ($self->{cnt_file});
686
687         # Get the index information from $db.CNT
688    
689         open(my $fileCNT, $self->{cnt_file}) || croak "can't read '$self->{cnt_file}': $!";
690         binmode($fileCNT);
691
692         my $buff;
693
694         read($fileCNT, $buff, 26) || croak "can't read first table from CNT: $!";
695         $self->unpack_cnt($buff);
696
697         read($fileCNT, $buff, 26) || croak "can't read second table from CNT: $!";
698         $self->unpack_cnt($buff);
699
700         close($fileCNT);
701
702         return $self->{cnt};
703 }
704
705 =head2 unpack_cnt
706
707 Unpack one of two 26 bytes fixed length record in C<.CNT> file.
708
709 Here is definition of record:
710
711  off key        description                             size
712   0: IDTYPE     BTree type                              s
713   2: ORDN       Nodes Order                             s
714   4: ORDF       Leafs Order                             s
715   6: N          Number of Memory buffers for nodes      s
716   8: K          Number of buffers for first level index s
717  10: LIV        Current number of Index Levels          s
718  12: POSRX      Pointer to Root Record in N0x           l
719  16: NMAXPOS    Next Available position in N0x          l
720  20: FMAXPOS    Next available position in L0x          l
721  24: ABNORMAL   Formal BTree normality indicator        s
722  length: 26 bytes
723
724 This will fill C<$self> object under C<cnt> with hash. It's used by C<read_cnt>.
725
726 =cut
727
728 sub unpack_cnt {
729         my $self = shift;
730
731         my @flds = qw(ORDN ORDF N K LIV POSRX NMAXPOS FMAXPOS ABNORMAL);
732
733         my $buff = shift || return;
734         my @arr = unpack("vvvvvvVVVv", $buff);
735
736         print STDERR "unpack_cnt: ",join(" ",@arr),"\n" if ($self->{'debug'});
737
738         my $IDTYPE = shift @arr;
739         foreach (@flds) {
740                 $self->{cnt}->{$IDTYPE}->{$_} = abs(shift @arr);
741         }
742 }
743
744 1;
745
746 =head1 BUGS
747
748 Some parts of CDS/ISIS documentation are not detailed enough to exmplain
749 some variations in input databases which has been tested with this module.
750 When I was in doubt, I assumed that OpenIsis's implementation was right
751 (except for obvious bugs).
752
753 However, every effort has been made to test this module with as much
754 databases (and programs that create them) as possible.
755
756 I would be very greatful for success or failure reports about usage of this
757 module with databases from programs other than WinIsis and IsisMarc. I had
758 tested this against ouput of one C<isis.dll>-based application, but I don't
759 know any details about it's version.
760
761 =head1 VERSIONS
762
763 As this is young module, new features are added in subsequent version. It's
764 a good idea to specify version when using this module like this:
765
766   use Biblio::Isis 0.23
767
768 Below is list of changes in specific version of module (so you can target
769 older versions if you really have to):
770
771 =over 8 
772
773 =item 0.24
774
775 Added C<ignore_empty_subfields>
776
777 =item 0.23
778
779 Added C<hash_filter> to L</to_hash>
780
781 Fixed bug with documented C<join_subfields_with> in L</new> which wasn't
782 implemented
783
784 =item 0.22
785
786 Added field number when calling C<hash_filter>
787
788 =item 0.21
789
790 Added C<join_subfields_with> to L</new> and L</to_hash>.
791
792 Added C<include_subfields> to L</to_hash>.
793
794 =item 0.20
795
796 Added C<< $isis->mfn >>, support for repeatable subfields and
797 C<< $isis->to_hash({ mfn => 42, ... }) >> calling convention
798
799 =back
800
801 =head1 AUTHOR
802
803         Dobrica Pavlinusic
804         CPAN ID: DPAVLIN
805         dpavlin@rot13.org
806         http://www.rot13.org/~dpavlin/
807
808 This module is based heavily on code from C<LIBISIS.PHP> library to read ISIS files V0.1.1
809 written in php and (c) 2000 Franck Martin <franck@sopac.org> and released under LGPL.
810
811 =head1 COPYRIGHT
812
813 This program is free software; you can redistribute
814 it and/or modify it under the same terms as Perl itself.
815
816 The full text of the license can be found in the
817 LICENSE file included with this module.
818
819
820 =head1 SEE ALSO
821
822 L<Biblio::Isis::Manual> for CDS/ISIS manual appendix F, G and H which describe file format
823
824 OpenIsis web site L<http://www.openisis.org>
825
826 perl4lib site L<http://perl4lib.perl.org>
827