9 # TODO: should fields be in directory order or data order?
11 # TODO: Missing exceptions:
13 # 001 is non-repeatable
14 # 001 is first in data
15 # 002-009 are next in data, but in any order
21 if( !is_resource(
$input ) )
throw new Exception(
"ISO2709RecordSetIterator requires a file resource, filename, or URL to be created");
23 $this->recordClass =
"ISO2709Record";
31 public function key() {
35 if( $this->currentRecord === null ) $this->currentRecord =
new $this->recordClass();
36 $this->currentPosition++;
37 $string = stream_get_line( $this->input, 99999, $this->RecordTerminator );
39 if( $string ===
false ) $this->
valid =
false;
40 else $this->currentRecord->loadFromString( $this->prepend . $string );
44 if($this->currentPosition !== null)
rewind( $this->input );
45 $this->currentPosition = -1;
46 $this->currentRecord =
new $this->recordClass();
52 protected $input,
$records, $numRecords, $cache, $recordOffsets, $numCache, $recordClass;
56 if( !is_resource(
$input ) )
throw new Exception(
"ISO2709RecordSetArray requires a file resource, filename, or URL to be created");
58 $this->records = array();
59 $this->cache = array();
60 $this->recordOffsets = array();
61 $this->recordOffsets[0] = 0;
62 $this->numCache = $numCache;
63 $this->recordClass =
"ISO2709Record";
67 if(!is_int($offset) or $offset < 0 or ($this->numRecords !== null and $this->numRecords <= $offset))
69 return $this->offsetGet( $offset ) ===
false;
74 foreach( array_keys( $this->cache, $offset ) as $k ) { unset($this->cache[$k]); }
75 $this->cache = array_values( $this->cache );
76 array_push( $this->cache, $offset );
77 while( count( $this->cache ) > $this->numCache ) {
78 $k = array_shift( $this->cache );
79 unset( $this->records[$k] );
82 if( !isset( $this->records[ $offset ] ) ) {
83 if( !isset( $this->recordOffsets[$offset] ) ) {
84 for( $i = $offset - 1 ; $i > 0 ; $i-- ) {
85 if( isset( $this->recordOffsets[$i] ) )
break;
87 fseek( $this->input, $this->recordOffsets[$i], SEEK_SET );
88 while( $i < $offset ) {
91 $this->recordOffsets[ $i ] = ftell( $this->input );
94 fseek( $this->input, $this->recordOffsets[$offset], SEEK_SET );
96 $this->records[ $offset ] =
new $this->recordClass();
97 $this->records[ $offset ]->loadFromBinaryStream( $this->input );
98 if(feof($this->input)) {
99 $this->numRecords = $offset+1;
101 $this->recordOffsets[$offset+1] = ftell( $this->input );
104 return $this->records[ $offset ];
108 throw new Exception(
"This type of record set is read-only, sorry" );
112 throw new Exception(
"This type of record set is read-only, sorry" );
121 const RecordTerminator =
"\035";
122 const FieldTerminator =
"\036";
123 const SubfieldInitiator =
"\037";
139 if( !isset( $this->defaults ) ) $this->defaults=array();
140 if( !isset( $this->defaults[$name] ) ) $this->defaults[$name] = $value;
144 $this->set_default(
"identifierLength", 0 );
145 $this->set_default(
"indicatorLength", 0 );
146 $this->set_default(
"lengthOfLengthOfField", 4 );
147 $this->set_default(
"lengthOfStartingCharacterPosition", 5 );
148 $this->set_default(
"lengthOfImplementationDefined", 0 );
150 $this->fields = array();
151 $this->exceptions = array();
154 ############################################################
156 ## Standards conformance
161 return ($tag !==
"000") && is_string( $tag ) && ( preg_match(
'/^[0-9A-Z]{3}$/',$tag) || preg_match(
'/^[0-9a-z]{3}$/',$tag) );
165 if( !self::isTagValid( $tag ) ) {
166 $this->exception(
"ISO2709 4.5.1: Tags must be alphanumeric, using only one case, and must be 3 bytes long. Thus '$tag' is not allowed.");
169 if( !$this->isTagValid( $tag ) ) {
170 $this->exception(
"Unknown: Invalid tag '$tag'");
178 return ($tag !==
"001");
182 if( !self::isTagRepeatable( $tag ) ) {
183 $this->exception(
"ISO2709 4.5.1: 001 refers to the record identifier field, which is not repeatable (4.1c)");
186 if( !$this->isTagRepeatable( $tag ) ) {
187 $this->exception(
"Unknown: Tag '$tag' is not repeatable.");
197 if(!$this->isTagValid($a) || !$this->isTagValid($b))
return 0;
198 if( $a === $b )
return 0;
199 if( $a ===
"001" )
return -1;
200 if( $b ===
"001" )
return +1;
201 $a = substr( $a, 0, 2 );
202 $b = substr( $b, 0, 2 );
203 if( $a === $b )
return 0;
204 if( $a ===
"00" )
return -1;
205 if( $b ===
"00" )
return +1;
210 if( self::tagOrder( $a, $b ) > 0 )
211 $this->exception(
"ISO2709 4.1: Field 001 is first, followed by 00* in any order, followed by all other tags; hence '$a' cannot precede '$b'");
212 else if( $this->tagOrder( $a, $b ) > 0 )
213 $this->exception(
"Unknown: '$a' is not supposed to precede '$b'");
217 $this->exceptions[] = $string;
220 ############################################################
222 ## Parsing a binary string
235 return $this->loadFromBinaryString( $peek );
238 private function checkString( $name, $trueValue, $string, $start = 0, $length =
false, $default =
false ) {
239 $string = ( $length === false ) ? substr( $string, $start ) : substr( $string, $start, $length );
240 if( is_int( $trueValue ) ) {
241 if( !ctype_digit( $string ) or intval( $string ) !== $trueValue )
243 "ISO2709: Value '$string' for \"$name\" does not match computed value '$trueValue'" );
245 if( !ctype_digit( $string ) )
247 "ISO2709: Value '$string' for \"$name\" is not numeric" );
249 return ctype_digit($string)?intval( $string ):$default;
253 $this->leader = $string;
254 $this->indicatorLength = $this->checkString(
"Indicator Length",
false, $this->leader, 10, 1, $this->defaults[
"indicatorLength"] );
255 $this->identifierLength = $this->checkString(
"Identifier Length",
false, $this->leader, 11, 1, $this->defaults[
"identifierLength"] );
259 $this->leader = substr( $string, 0, 24 );
260 $this->fields = array();
261 $this->exceptions = array();
262 $this->raw = $string;
268 $this->checkString(
"Record Length", $recordLength, $this->leader, 0, 5 );
269 $indicatorLength = $this->checkString(
"Indicator Length",
false, $this->leader, 10, 1, $this->defaults[
"indicatorLength"] );
270 $identifierLength = $this->checkString(
"Identifier Length",
false, $this->leader, 11, 1, $this->defaults[
"identifierLength"] );
271 $this->checkString(
"Base Address of Data", $baseAddressOfData, $this->leader, 12, 5 );
272 $lengthOfLengthOfField = $this->checkString(
"Length of Length-Of-Field portion of directory entry",
false, $this->leader, 20, 1, $this->defaults[
"lengthOfLengthOfField"] );
273 $lengthOfStartingCharacterPosition = $this->checkString(
"Length of Starting-Character-Position portion of directory entry",
false, $this->leader, 21, 1, $this->defaults[
"lengthOfStartingCharacterPosition"] );
274 $lengthOfImplementationDefined = $this->checkString(
"Length of Implementation-Defined portion of directory entry",
false, $this->leader, 22, 1, $this->defaults[
"lengthOfImplementationDefined"] );
275 if(
"$lengthOfLengthOfField$lengthOfStartingCharacterPosition$lengthOfImplementationDefined" !==
"450" ) {
276 $this->exception(
"MARC21: $lengthOfLengthOfField$lengthOfStartingCharacterPosition$lengthOfImplementationDefined != 450" );
277 $lengthOfLengthOfField = 4;
278 $lengthOfStartingCharacterPosition = 5;
279 $lengthOfImplementationDefined = 0;
282 $this->indicatorLength = $indicatorLength;
283 $this->identifierLength = $identifierLength;
285 $lengthOfDirectoryEntry = 3 + $lengthOfLengthOfField + $lengthOfStartingCharacterPosition + $lengthOfImplementationDefined;
287 if( 0 != ( ( $baseAddressOfData-25) % $lengthOfDirectoryEntry ) )
288 $this->exception(
"ISO2709 4.4.1: Directory does not end on directory entry boundary ".
289 "(Directory is ". ($baseAddressOfData-25).
" bytes long, " .
290 "each entry is 3 + $lengthOfLengthOfField + $lengthOfStartingCharacterPosition + $lengthOfImplementationDefined = $lengthOfDirectoryEntry bytes long, " .
291 "leaving " . (($baseAddressOfData-25)%$lengthOfDirectoryEntry) .
" bytes leftover)" );
296 for( $i = 24 ; $i+$lengthOfDirectoryEntry+1 <= $baseAddressOfData ; $i+= $lengthOfDirectoryEntry ) {
297 $tag = substr( $string, $i, 3 );
299 $off = $this->checkString(
"Start of field '$tag'",
false, $string, $i+3+$lengthOfLengthOfField, $lengthOfStartingCharacterPosition );
300 if( $baseAddressOfData + $off > strlen($string) ) {
301 $off = substr( $string, $i+3+$lengthOfLengthOfField, $lengthOfStartingCharacterPosition );
302 $this->exception(
"ISO2709: Offset '$off' of field '$tag' is beyond end of string. Losing this field." );
306 $this->exception(
"Jack: Data for field '$tag' does not immediately follow a field terminator" );
307 $off = strrpos( substr( $string, 0, $baseAddressOfData + $off ),
ISO2709::FieldTerminator ) - $baseAddressOfData + 1;
310 $imp = substr( $string, $i + 3 + $lengthOfLengthOfField + $lengthOfStartingCharacterPosition, $lengthOfImplementationDefined );
312 $maxFieldLen = intval(
"1" .str_repeat( $lengthOfLengthOfField,
"0" ) ) - 1;
314 while( substr( $string, $i+3, $lengthOfLengthOfField ) ===
"000" ) {
315 $calclen += $maxFieldLen;
316 $i += $lengthOfDirectoryEntry;
317 $newTag = substr( $string, $i, 3 );
318 if( $tag !== $newTag ) {
319 $this->exception(
"ISO2709 4.4.4: When the recorded field length is 0, each following directory entry refers to the same field. However, we went from '$tag' to '$newTag'.");
320 $i -= $lengthOfDirectoryEntry;
324 $this->checkString(
"Length of field '$tag'", $len-$calclen, $string, $i+3, $lengthOfLengthOfField );
328 $data = substr( $string, $baseAddressOfData + $off, $len - 1 );
329 $this->AppendFieldBinary( $tag, $data, array( $tag, $len, $off, $imp ) );
335 $string = $this->raw;
338 $lengthOfLengthOfField = $this->checkString(
"Length of Length-Of-Field portion of directory entry",
false, $this->leader, 20, 1, $this->defaults[
"lengthOfLengthOfField"] );
339 $lengthOfStartingCharacterPosition = $this->checkString(
"Length of Starting-Character-Position portion of directory entry",
false, $this->leader, 21, 1, $this->defaults[
"lengthOfStartingCharacterPosition"] );
340 $lengthOfImplementationDefined = $this->checkString(
"Length of Implementation-Defined portion of directory entry",
false, $this->leader, 22, 1, $this->defaults[
"lengthOfImplementationDefined"] );
341 $lengthOfDirectoryEntry = 3 + $lengthOfLengthOfField + $lengthOfStartingCharacterPosition + $lengthOfImplementationDefined;
343 $map = str_repeat(
"?", $recordLength );
344 for( $i = 24 ; $i+$lengthOfDirectoryEntry+1 <= $baseAddressOfData ; $i+= $lengthOfDirectoryEntry ) {
345 $tag = substr( $string, $i, 3 );
346 $off = intval( substr( $string, $i+3+$lengthOfLengthOfField, $lengthOfStartingCharacterPosition ) );
347 $off = strrpos( substr( $string, 0, $baseAddressOfData + $off ),
ISO2709::FieldTerminator ) - $baseAddressOfData + 1;
350 $maxFieldLen = intval(
"1" .str_repeat( $lengthOfLengthOfField,
"0" ) ) - 1;
351 while( substr( $string, $i+3, $lengthOfLengthOfField ) ===
"000" ) {
352 $i += $lengthOfDirectoryEntry;
353 $calclen += $maxFieldLen;
355 $len = $calclen + intval( substr( $string, $i+3, $lengthOfLengthOfField ) );
357 for( $j = 0 ; $j < $len ; $j++ ) {
358 $map[$baseAddressOfData+$off+$j] =
" ";
360 $map = substr_replace( $map, $tag, $baseAddressOfData+$off );
361 $map[$baseAddressOfData+$off+$len-1] =
"t";
363 for( $i = 0 ; $i < $recordLength ; $i++ ) {
364 if( $i < 24 ) $map[$i] =
"l";
365 else if( $i+1 < $baseAddressOfData ) $map[$i] =
"d";
367 if($map[$i] !=
"t") $map[$i]=
"#";
375 return $this->loadFromBinaryString( $string );
379 ############################################################
381 ## Append a field to a MARC record
385 # Check for valid tag
386 $this->assertTagValid($tag);
388 # Strip out indicators and subfields, handling bad data
389 # If there is a subfield identifier initiator, then everything before it is the indicators
390 # Otherwise the indicators are precisely the first indicatorLength bytes
392 $indicators = substr( $data, 0, $subfieldStart );
394 foreach( $subfields as $k => $v ) {
395 $identifier = substr( $v, 0, $this->identifierLength - 1);
396 $subdata = substr( $v, $this->identifierLength-1 );
401 if( substr( $tag, 0, 2 ) !==
"00" ) {
402 $indicators = substr( $data, 0, $this->indicatorLength );
403 $data = substr( $data, $this->indicatorLength );
407 $subfields = array();
409 return new ISO2709Field( $tag, $data, $indicators, $subfields, $directoryEntry, $this );
413 $field = $this->ParseBinaryField( $tag, $data, $directoryEntry );
414 $this->AppendField( $field, $reorder );
418 if( substr( $field->tag, 0, 2 ) ===
"00" ) {
419 if( $field->indicators !==
"" )
420 $this->exception(
"ISO2709 4.5.4: Control fields have no indicators, but field '{$field->tag}' has indicators '{$field->indicators}'");
421 if( count( $field->subfields ) > 0 )
422 $this->exception(
"ISO2709 4.5.4: Control fields have no subfields, but field '{$field->tag}' has " . count( $field->subfields) .
" subfields");
424 if( strlen( $field->indicators ) != $this->indicatorLength )
425 $this->exception(
"ISO2709 4.5.4d: Field '{$field->tag}' has indicators '{$field->indicators}' of the wrong length (specified length is {$this->indicatorLength})");
426 foreach( $field->subfields as $subfield ) {
427 if( strlen( $subfield->identifier ) + 1 != $this->identifierLength )
428 $this->exception(
"ISO2709: Field '{$field->tag}' has subfield '\${$subfield->identifier}' but that is not the right length of a subfield identifier.");
430 if( $field->data !==
"" and $this->indicatorLength > 0 )
431 $this->exception(
"ISO2709 4.5.4: Field '{$field->tag}' has data, but should only have subfields");
436 $this->checkField( $field );
437 $field->record = $this;
439 # Check for valid ordering
440 $numTags = count( $this->fields );
442 if( $reorder ===
false ) {
443 $lastTag = $this->fields[ $numTags - 1 ]->tag;
444 #$this->assertTagOrder( $lastTag, $field->tag );
445 $appendAfter = $numTags;
447 for( $i = $numTags - 1 ; $i >= 0 ; $i-- ) {
448 $lastTag = $this->fields[ $i ]->tag;
449 if( $this->tagOrder( $lastTag, $field->tag ) <= 0 ) {
450 #print "Append {$field->tag} after $lastTag \n";
453 #print "Do not {$field->tag} after $lastTag \n";
457 }
else { $appendAfter = -1; }
459 array_splice( $this->fields, $appendAfter+1, 0, array( $field ) );
463 foreach( $this->fields as $k => $v ) {
464 if( $v === $field ) unset( $this->fields[$k] );
466 $this->fields = array_values( $this->fields );
470 foreach( $this->fields as $k => $v ) {
471 if( $v->tag === $tag ) unset( $this->fields[$k] );
473 $this->fields = array_values( $this->fields );
478 foreach( $this->fields as $v ) {
479 if( $v->tag === $tag ) $ret[] = $v;
486 foreach( $this->fields as $v ) {
487 if( preg_match(
"/$patt/", $v->tag ) ) $ret[] = $v;
492 ############################################################
499 $indicatorLength = $this->indicatorLength;
500 $identifierLength = $this->identifierLength;
501 $lengthOfLengthOfField = $this->checkString(
"Length of Length-Of-Field portion of directory entry",
false, $this->leader, 20, 1, $this->defaults[
"lengthOfLengthOfField"] );
502 $lengthOfStartingCharacterPosition = $this->checkString(
"Length of Starting-Character-Position portion of directory entry",
false, $this->leader, 21, 1, $this->defaults[
"lengthOfStartingCharacterPosition"] );
503 $lengthOfImplementationDefined = $this->checkString(
"Length of Implementation-Defined portion of directory entry",
false, $this->leader, 22, 1, $this->defaults[
"lengthOfImplementationDefined"] );
504 $lengthOfDirectoryEntry = 3 + $lengthOfLengthOfField + $lengthOfStartingCharacterPosition + $lengthOfImplementationDefined;
508 $baseAddressOfData = 24 + $lengthOfDirectoryEntry * count( $this->fields ) + 1;
509 foreach( $this->fields as $v ) {
511 $start = strlen( $data );
512 $data .= $v->indicators;
513 foreach( $v->subfields as $vv ) {
518 $length = strlen($data) - $start;
519 $impdef = $v->directoryEntry[3];
520 $directory .= sprintf(
522 "%0{$lengthOfLengthOfField}.{$lengthOfLengthOfField}d" .
523 "%0{$lengthOfStartingCharacterPosition}.{$lengthOfStartingCharacterPosition}d" .
524 "%{$lengthOfImplementationDefined}.{$lengthOfImplementationDefined}s",
525 $tag, $length, $start, $impdef );
529 $recordLength = $baseAddressOfData + strlen($data);
530 $leader = sprintf(
"%05.5d%1.1s%4.4s%1.1d%1.1d%05.5d%3.3s%1.1d%1.1d%1.1d%1.1s",
531 $recordLength, substr( $this->leader, 5, 1 ),
532 substr( $this->leader, 6, 4), $indicatorLength,
533 $identifierLength, $baseAddressOfData, substr( $this->leader, 17, 3 ),
534 $lengthOfLengthOfField,
535 $lengthOfStartingCharacterPosition,
536 $lengthOfImplementationDefined, substr( $this->leader, 23, 1 ) );
537 $this->leader = $leader;
538 $ret = $leader . $directory . $data;
539 assert( strlen( $leader ) + strlen( $directory ) === $baseAddressOfData );
540 assert( strlen( $ret ) === $recordLength );
544 ############################################################
546 ## Export as Mnemonic string
552 $field_initiator =
"=",
553 $tag_terminator =
" ",
554 $identifier_initiator =
"\$",
555 $field_terminator =
"\r\n",
556 $record_terminator =
"\r\n",
557 $space_replacer =
" "
560 $ret[] = sprintf(
"%s%3.3s%s%s%s%s%s", $field_initiator, $leader_tag,
561 $tag_terminator,
"",
"", $this->leader, $field_terminator );
562 foreach( $this->fields as $v ) {
563 $ret[] = $v->AsMnemonicString( $field_initiator,$tag_terminator,$identifier_initiator,$field_terminator, $space_replacer );
565 foreach( $this->exceptions as $v ) {
566 $ret[] = sprintf(
"%s%3.3s%s%s%s%s%s", $field_initiator,
"XXX",
567 $tag_terminator,
"xx",
"\$x$v",
"", $field_terminator );
569 return implode( $ret ) . $record_terminator;
574 public $tag, $data, $indicators, $subfields, $directoryEntry;
575 function __construct( $tag, $data, $indicators =
"", $subfields = array(), $directoryEntry = array(), $record = null ) {
578 $this->indicators = $indicators;
579 $this->directoryEntry = $directoryEntry;
580 if( !isset( $this->directoryEntry[0] ) ) $this->directoryEntry[0] = $tag;
581 if( !isset( $this->directoryEntry[3] ) ) $this->directoryEntry[3] =
"";
582 $this->record = $record;
583 $this->subfields = array();
584 foreach( $subfields as $v ) { $this->appendSubfield( $v ); }
587 return $this->record->removeField( $this );
590 return $this->record->removeField( $this );
593 if( is_a( $subfield,
"ISO2709Subfield" ) ) {
594 $subfield->field = $this;
595 $this->subfields[] = $subfield;
601 # maybe title more specific
603 if( is_a( $subfield,
"ISO2709Subfield" ) ) {
605 foreach( $this->subfields as $k => $v ) {
606 if( $v->identifier === $identifier ) {
607 array_splice($this->subfields,$k,0,array($subfield));
608 $this->subfields = array_values($this->subfields);
611 if (preg_match(
"/(.*?)( *[\:\/=] *)$/",$this->subfields[$k-1]->data,$m)) {
612 $this->subfields[$k-1]->data = $m[1];
613 $this->subfields[$k]->data .= $m[2];
621 return $this->insertSubfieldBefore( $identifier,
new ISO2709Subfield( $subfield, $arg3 ) );
626 foreach( $this->subfields as $k => $v ) {
627 if( $v === $subfield ) unset( $this->subfields[$k] );
629 $this->subfields = array_values( $this->subfields );
632 foreach( $this->subfields as $v ) {
633 if( $v->identifier === $identifier )
return $v;
639 foreach( $this->subfields as $v ) {
640 if( $v->identifier === $identifier ) $ret[] = $v;
646 $field_initiator =
"=",
647 $tag_terminator =
" ",
648 $identifier_initiator =
"\$",
649 $field_terminator =
"\r\n",
650 $space_replacer =
" "
653 foreach( $this->subfields as $vv ) {
654 $sub .= $identifier_initiator . $vv->identifier . $vv->data;
656 $indicators = str_replace(
" ",$space_replacer, $this->indicators );
657 $data = str_replace(
" ",$space_replacer, $this->data );
658 return sprintf(
"%s%3.3s%s%s%s%s%s", $field_initiator, $this->tag,
659 $tag_terminator, $indicators, $sub, $data, $field_terminator );
666 $this->identifier = $identifier;
670 return $this->field->removeSubfield( $this );
673 return $this->field->removeSubfield( $this );
678 foreach( $this->field->subfields as $vv ) {
removeSubfield(\ISO2709Subfield $subfield)
offsetSet( $offset, $value)
appendSubfield( $subfield, $arg2=null)
loadFromBinaryStream( $filehandle)
assertTagRepeatable( $tag)
loadFromBinaryString( $string, $fuzzy=true)
insertSubfieldBefore( $identifier, $subfield, $arg3=null)
checkString( $name, $trueValue, $string, $start=0, $length=false, $default=false)
getOneSubfield( $identifier)
getSubfields( $identifier)
__construct( $identifier, $data)
AppendFieldBinary( $tag, $data, $directoryEntry=array(), $reorder=false)
__construct( $tag, $data, $indicators="", $subfields=array(), $directoryEntry=array(), $record=null)
__construct( $input, $numCache=10)
AsMnemonicString( $leader_tag="LDR", $field_initiator="=", $tag_terminator=" ", $identifier_initiator="\, $field_terminator="\\", $record_terminator="\\", $space_replacer=" ")
AsMnemonicString( $field_initiator="=", $tag_terminator=" ", $identifier_initiator="\, $field_terminator="\\", $space_replacer=" ")
set_default( $name, $value)
AppendField( $field, $reorder=false)
ParseBinaryField( $tag, $data, $directoryEntry)