Grima  2018-08
Whispering into Alma's ear
ISO2709.php
Go to the documentation of this file.
1 <?php
2 
3 
4 /***********************************************************************
5 **
6 ** Methods for dealing with ISO2709 (MARC) records and files
7 */
8 
9 # TODO: should fields be in directory order or data order?
10 
11 # TODO: Missing exceptions:
12 # ISO2709
13 # 001 is non-repeatable
14 # 001 is first in data
15 # 002-009 are next in data, but in any order
16 
17 class ISO2709RecordSetIterator implements Iterator {
19  public function __construct( $input ) {
20  if( is_string( $input ) ) $input = fopen( $input, "rb" );
21  if( !is_resource( $input ) ) throw new Exception("ISO2709RecordSetIterator requires a file resource, filename, or URL to be created");
22  $this->input = $input;
23  $this->recordClass = "ISO2709Record";
24  $this->RecordTerminator = ISO2709::RecordTerminator;
25  $this->prepend = "";
26  }
27  public function valid() { return $this->valid; }
28  public function current() {
29  return $this->currentRecord;
30  }
31  public function key() {
33  }
34  public function next() {
35  if( $this->currentRecord === null ) $this->currentRecord = new $this->recordClass();
36  $this->currentPosition++;
37  $string = stream_get_line( $this->input, 99999, $this->RecordTerminator );
38  $this->valid = true;
39  if( $string === false ) $this->valid = false;
40  else $this->currentRecord->loadFromString( $this->prepend . $string );
41  $this->prepend = "";
42  }
43  public function rewind() {
44  if($this->currentPosition !== null) rewind( $this->input );
45  $this->currentPosition = -1;
46  $this->currentRecord = new $this->recordClass();
47  $this->next();
48  }
49 }
50 
51 class ISO2709RecordSetArray implements ArrayAccess {
52  protected $input, $records, $numRecords, $cache, $recordOffsets, $numCache, $recordClass;
53 
54  function __construct( $input, $numCache = 10 ) {
55  if( is_string( $input ) ) $input = fopen( $input, "rb" );
56  if( !is_resource( $input ) ) throw new Exception("ISO2709RecordSetArray requires a file resource, filename, or URL to be created");
57  $this->input = $input;
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";
64  }
65 
66  public function offsetExists( $offset ) {
67  if(!is_int($offset) or $offset < 0 or ($this->numRecords !== null and $this->numRecords <= $offset))
68  return false;
69  return $this->offsetGet( $offset ) === false;
70  }
71 
72  public function offsetGet ( $offset ) {
73  # Update cache
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] );
80  }
81  # Now get the record
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;
86  }
87  fseek( $this->input, $this->recordOffsets[$i], SEEK_SET );
88  while( $i < $offset ) {
89  stream_get_line( $this->input, 99999, ISO2709::RecordTerminator );
90  $i++;
91  $this->recordOffsets[ $i ] = ftell( $this->input );
92  }
93  } else {
94  fseek( $this->input, $this->recordOffsets[$offset], SEEK_SET );
95  }
96  $this->records[ $offset ] = new $this->recordClass();
97  $this->records[ $offset ]->loadFromBinaryStream( $this->input );
98  if(feof($this->input)) {
99  $this->numRecords = $offset+1;
100  } else {
101  $this->recordOffsets[$offset+1] = ftell( $this->input );
102  }
103  }
104  return $this->records[ $offset ];
105  }
106 
107  public function offsetSet ( $offset, $value ) {
108  throw new Exception( "This type of record set is read-only, sorry" );
109  }
110 
111  public function offsetUnset ( $offset ) {
112  throw new Exception( "This type of record set is read-only, sorry" );
113  }
114 }
115 
116 class ISO2709 {
117  /* A record must be terminated with 0x1D,
118  the directory and each field must be terminated with 0x1E,
119  and each subfield begins with an identifier which must in turn begin
120  with 0x1F */
121  const RecordTerminator = "\035";
122  const FieldTerminator = "\036";
123  const SubfieldInitiator = "\037";
124 
125 };
126 
127 class ISO2709Exception extends Exception { };
128 
130  public $leader; // string form of the leader (mnemonic marc format)
131  public $fields; // array of ISO2709 fields
132  public $exceptions; // an array of standards violations as strings
133  public $raw;
134  protected $identifierLength, $indicatorLength;
135 
136  protected $defaults;
137 
138  protected function set_default( $name, $value ) {
139  if( !isset( $this->defaults ) ) $this->defaults=array();
140  if( !isset( $this->defaults[$name] ) ) $this->defaults[$name] = $value;
141  }
142 
143  function __construct( ) {
144  $this->set_default( "identifierLength", 0 ); // required, so if not available disable subfields
145  $this->set_default( "indicatorLength", 0 ); // advisory only
146  $this->set_default( "lengthOfLengthOfField", 4 ); // required, but no real default; use MARC21
147  $this->set_default( "lengthOfStartingCharacterPosition", 5 ); // required, but no real default; use MARC21
148  $this->set_default( "lengthOfImplementationDefined", 0 ); // required, it seems like 0 is the only sensible value; use MARC21
149  $this->leader = "";
150  $this->fields = array();
151  $this->exceptions = array();
152  }
153 
154  ############################################################
155  ##
156  ## Standards conformance
157  ##
158 
159  // ISO2709 4.5.1
160  function isTagValid( $tag ) {
161  return ($tag !== "000") && is_string( $tag ) && ( preg_match('/^[0-9A-Z]{3}$/',$tag) || preg_match('/^[0-9a-z]{3}$/',$tag) );
162  }
163 
164  function assertTagValid( $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.");
167  return false;
168  }
169  if( !$this->isTagValid( $tag ) ) {
170  $this->exception("Unknown: Invalid tag '$tag'");
171  return false;
172  }
173  return true;
174  }
175 
176  // ISO2709 4.1
177  function isTagRepeatable( $tag ) {
178  return ($tag !== "001");
179  }
180 
181  function assertTagRepeatable( $tag ) {
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)");
184  return false;
185  }
186  if( !$this->isTagRepeatable( $tag ) ) {
187  $this->exception("Unknown: Tag '$tag' is not repeatable.");
188  return false;
189  }
190  return true;
191  }
192 
193  // ISO2709 4.1
194  // -1 if $a must come before $b, +1 if $a must come after $b, 0 if unspecified
195  // ISO2709 ordering is simple 001, then 00*, then ***
196  function tagOrder( $a, $b ) {
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;
206  return 0;
207  }
208 
209  function assertTagOrder( $a, $b ) {
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'");
214  }
215 
216  function exception( $string ) {
217  $this->exceptions[] = $string;
218  }
219 
220  ############################################################
221  ##
222  ## Parsing a binary string
223  ##
224 
225  public function loadFromBinaryStream( $filehandle ) {
226  $peek = stream_get_line( $filehandle, 99999, ISO2709::RecordTerminator );
227  if( $peek ) $peek .= ISO2709::RecordTerminator;
228  else return false;
229  /*
230  $peek = fread( $filehandle, 5 );
231  if( $peek === false ) return false;
232  if( ! ctype_digit( $peek ) ) return false;
233  $peek .= fread( $filehandle, intval( $peek ) - 5 );
234  */
235  return $this->loadFromBinaryString( $peek );
236  }
237 
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 )
242  $this->exception(
243  "ISO2709: Value '$string' for \"$name\" does not match computed value '$trueValue'" );
244  } else {
245  if( !ctype_digit( $string ) )
246  $this->exception(
247  "ISO2709: Value '$string' for \"$name\" is not numeric" );
248  }
249  return ctype_digit($string)?intval( $string ):$default;
250  }
251 
252  public function setLeader( $string ) {
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"] );
256  }
257 
258  public function loadFromBinaryString( $string, $fuzzy = true ) {
259  $this->leader = substr( $string, 0, 24 );
260  $this->fields = array();
261  $this->exceptions = array();
262  $this->raw = $string;
263 
264  // We now ignore the leader as much as possible
265  $recordLength = strpos( $string, ISO2709::RecordTerminator ) + 1;
266  $baseAddressOfData = strpos( $string, ISO2709::FieldTerminator )+1;
267 
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;
280  }
281 
282  $this->indicatorLength = $indicatorLength; // advisory only
283  $this->identifierLength = $identifierLength; // required
284 
285  $lengthOfDirectoryEntry = 3 + $lengthOfLengthOfField + $lengthOfStartingCharacterPosition + $lengthOfImplementationDefined;
286 
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)" );
292 
293  // now read the directory
294  $fieldTypeMode = 0;
295  $lastTag = false;
296  for( $i = 24 ; $i+$lengthOfDirectoryEntry+1 <= $baseAddressOfData ; $i+= $lengthOfDirectoryEntry ) {
297  $tag = substr( $string, $i, 3 );
298 
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." );
303  continue;
304  }
305  if( $string[ $baseAddressOfData + $off - 1 ] !== ISO2709::FieldTerminator ) {
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;
308  }
309  $len = strpos( $string, ISO2709::FieldTerminator, $baseAddressOfData + $off ) - ( $baseAddressOfData + $off - 1);
310  $imp = substr( $string, $i + 3 + $lengthOfLengthOfField + $lengthOfStartingCharacterPosition, $lengthOfImplementationDefined );
311 
312  $maxFieldLen = intval( "1" .str_repeat( $lengthOfLengthOfField, "0" ) ) - 1;
313  $calclen = 0;
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;
321  break;
322  }
323  }
324  $this->checkString( "Length of field '$tag'", $len-$calclen, $string, $i+3, $lengthOfLengthOfField );
325 
326 
327  // read in the field
328  $data = substr( $string, $baseAddressOfData + $off, $len - 1 );
329  $this->AppendFieldBinary( $tag, $data, array( $tag, $len, $off, $imp ) );
330  }
331  return true;
332  }
333 
334  function DataNotInDirectory( ) {
335  $string = $this->raw;
336  $recordLength = strpos( $string, ISO2709::RecordTerminator ) + 1;
337  $baseAddressOfData = strpos( $string, ISO2709::FieldTerminator )+1;
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;
342 
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;
348 
349  $calclen = 0;
350  $maxFieldLen = intval( "1" .str_repeat( $lengthOfLengthOfField, "0" ) ) - 1;
351  while( substr( $string, $i+3, $lengthOfLengthOfField ) === "000" ) {
352  $i += $lengthOfDirectoryEntry;
353  $calclen += $maxFieldLen;
354  }
355  $len = $calclen + intval( substr( $string, $i+3, $lengthOfLengthOfField ) );
356 
357  for( $j = 0 ; $j < $len ; $j++ ) {
358  $map[$baseAddressOfData+$off+$j] = " ";
359  }
360  $map = substr_replace( $map, $tag, $baseAddressOfData+$off );
361  $map[$baseAddressOfData+$off+$len-1] = "t";
362  }
363  for( $i = 0 ; $i < $recordLength ; $i++ ) {
364  if( $i < 24 ) $map[$i] = "l";
365  else if( $i+1 < $baseAddressOfData ) $map[$i] = "d";
366  else if( $string[$i] == ISO2709::FieldTerminator ) {
367  if($map[$i] != "t") $map[$i]="#";
368  else $map[$i] = "T";
369  }
370  }
371  print $map."\n";
372  }
373 
374  function loadFromString( $string ) {
375  return $this->loadFromBinaryString( $string );
376  }
377 
378 
379  ############################################################
380  ##
381  ## Append a field to a MARC record
382  ##
383 
384  function ParseBinaryField( $tag, $data, $directoryEntry ) {
385  # Check for valid tag
386  $this->assertTagValid($tag);
387 
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
391  if( false !== ( $subfieldStart = strpos( $data, ISO2709::SubfieldInitiator ) ) ) {
392  $indicators = substr( $data, 0, $subfieldStart );
393  $subfields = explode( ISO2709::SubfieldInitiator, substr( $data, $subfieldStart+1 ) );
394  foreach( $subfields as $k => $v ) {
395  $identifier = substr( $v, 0, $this->identifierLength - 1);
396  $subdata = substr( $v, $this->identifierLength-1 );
397  $subfields[$k] = new ISO2709Subfield( $identifier, $subdata );
398  }
399  $data = "";
400  } else {
401  if( substr( $tag, 0, 2 ) !== "00" ) {
402  $indicators = substr( $data, 0, $this->indicatorLength );
403  $data = substr( $data, $this->indicatorLength );
404  } else {
405  $indicators = "";
406  }
407  $subfields = array();
408  }
409  return new ISO2709Field( $tag, $data, $indicators, $subfields, $directoryEntry, $this );
410  }
411 
412  function AppendFieldBinary( $tag, $data, $directoryEntry = array(), $reorder = false ) {
413  $field = $this->ParseBinaryField( $tag, $data, $directoryEntry );
414  $this->AppendField( $field, $reorder );
415  }
416 
417  function CheckField( $field ) {
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");
423  } else {
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.");
429  }
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");
432  }
433  }
434 
435  function AppendField( $field, $reorder = false ) {
436  $this->checkField( $field );
437  $field->record = $this;
438 
439  # Check for valid ordering
440  $numTags = count( $this->fields );
441  if( $numTags > 0 ) {
442  if( $reorder === false ) {
443  $lastTag = $this->fields[ $numTags - 1 ]->tag;
444  #$this->assertTagOrder( $lastTag, $field->tag );
445  $appendAfter = $numTags;
446  } else {
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";
451  break;
452  }
453  #print "Do not {$field->tag} after $lastTag \n";
454  }
455  $appendAfter = $i;
456  }
457  } else { $appendAfter = -1; }
458 
459  array_splice( $this->fields, $appendAfter+1, 0, array( $field ) );
460  }
461 
462  function removeField( $field ) {
463  foreach( $this->fields as $k => $v ) {
464  if( $v === $field ) unset( $this->fields[$k] );
465  }
466  $this->fields = array_values( $this->fields );
467  }
468 
469  function delFields( $tag ) {
470  foreach( $this->fields as $k => $v ) {
471  if( $v->tag === $tag ) unset( $this->fields[$k] );
472  }
473  $this->fields = array_values( $this->fields );
474  }
475 
476  function getFields( $tag ) {
477  $ret = array();
478  foreach( $this->fields as $v ) {
479  if( $v->tag === $tag ) $ret[] = $v;
480  }
481  return $ret;
482  }
483 
484  function getTagPattern( $patt ) {
485  $ret = array();
486  foreach( $this->fields as $v ) {
487  if( preg_match("/$patt/", $v->tag ) ) $ret[] = $v;
488  }
489  return $ret;
490  }
491 
492  ############################################################
493  ##
494  ## Export as binary
495  ##
496 
497  public function AsBinaryString( ) {
498  // We now ignore the leader as much as possible
499  $indicatorLength = $this->indicatorLength; // advisory, we will write corrupt files if requested
500  $identifierLength = $this->identifierLength; // advisory
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;
505 
506  $directory = "";
507  $data = "";
508  $baseAddressOfData = 24 + $lengthOfDirectoryEntry * count( $this->fields ) + 1;
509  foreach( $this->fields as $v ) {
510  $tag = $v->tag;
511  $start = strlen( $data );
512  $data .= $v->indicators;
513  foreach( $v->subfields as $vv ) {
514  $data .= ISO2709::SubfieldInitiator . $vv->identifier . $vv->data;
515  }
516  $data .= $v->data;
517  $data .= ISO2709::FieldTerminator;
518  $length = strlen($data) - $start;
519  $impdef = $v->directoryEntry[3];
520  $directory .= sprintf(
521  "%3.3s" .
522  "%0{$lengthOfLengthOfField}.{$lengthOfLengthOfField}d" .
523  "%0{$lengthOfStartingCharacterPosition}.{$lengthOfStartingCharacterPosition}d" .
524  "%{$lengthOfImplementationDefined}.{$lengthOfImplementationDefined}s",
525  $tag, $length, $start, $impdef );
526  }
527  $directory .= ISO2709::FieldTerminator;
528  $data .= ISO2709::RecordTerminator;
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 );
541  return $ret;
542  }
543 
544  ############################################################
545  ##
546  ## Export as Mnemonic string
547  ##
548 
549 
550  public function AsMnemonicString(
551  $leader_tag = "LDR",
552  $field_initiator = "=",
553  $tag_terminator = " ",
554  $identifier_initiator = "\$",
555  $field_terminator = "\r\n",
556  $record_terminator = "\r\n",
557  $space_replacer = " "
558  ) {
559  $ret = array();
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 );
564  }
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 );
568  }
569  return implode( $ret ) . $record_terminator;
570  }
571 }
572 
574  public $tag, $data, $indicators, $subfields, $directoryEntry;
575  function __construct( $tag, $data, $indicators = "", $subfields = array(), $directoryEntry = array(), $record = null ) {
576  $this->tag=$tag;
577  $this->data=$data;
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 ); }
585  }
586  function remove() {
587  return $this->record->removeField( $this );
588  }
589  function delete() {
590  return $this->record->removeField( $this );
591  }
592  function appendSubfield( $subfield, $arg2 = null ) {
593  if( is_a( $subfield, "ISO2709Subfield" ) ) {
594  $subfield->field = $this;
595  $this->subfields[] = $subfield;
596  } else {
597  $this->appendSubfield( new ISO2709Subfield( $subfield, $arg2 ) );
598  }
599  }
600 
601  # maybe title more specific
602  function insertSubfieldBefore( $identifier, $subfield, $arg3 = null) {
603  if( is_a( $subfield, "ISO2709Subfield" ) ) {
604  $found = false;
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);
609  $found = true;
610  if( $k > 0 ) {
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];
614  }
615  }
616  break;
617  }
618  }
619  return $found;
620  } else {
621  return $this->insertSubfieldBefore( $identifier, new ISO2709Subfield( $subfield, $arg3 ) );
622  }
623  }
624 
625  function removeSubfield( \ISO2709Subfield $subfield ) {
626  foreach( $this->subfields as $k => $v ) {
627  if( $v === $subfield ) unset( $this->subfields[$k] );
628  }
629  $this->subfields = array_values( $this->subfields );
630  }
631  function getOneSubfield( $identifier ) {
632  foreach( $this->subfields as $v ) {
633  if( $v->identifier === $identifier ) return $v;
634  }
635  return false;
636  }
637  function getSubfields( $identifier ) {
638  $ret = array();
639  foreach( $this->subfields as $v ) {
640  if( $v->identifier === $identifier ) $ret[] = $v;
641  }
642  return $ret;
643  }
644 
645  public function AsMnemonicString(
646  $field_initiator = "=",
647  $tag_terminator = " ",
648  $identifier_initiator = "\$",
649  $field_terminator = "\r\n",
650  $space_replacer = " "
651  ) {
652  $sub = "";
653  foreach( $this->subfields as $vv ) {
654  $sub .= $identifier_initiator . $vv->identifier . $vv->data;
655  }
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 );
660  }
661 }
662 
664  public $identifier, $data, $field;
665  function __construct( $identifier, $data ) {
666  $this->identifier = $identifier;
667  $this->data = $data;
668  }
669  function remove() {
670  return $this->field->removeSubfield( $this );
671  }
672  function delete() {
673  return $this->field->removeSubfield( $this );
674  }
675 
676  function prev() {
677  $prev = null;
678  foreach( $this->field->subfields as $vv ) {
679  if ($vv == $this) {
680  return $prev;
681  } else {
682  $prev = $vv;
683  }
684  }
685  }
686 
687 }
DataNotInDirectory()
Definition: ISO2709.php:334
removeSubfield(\ISO2709Subfield $subfield)
Definition: ISO2709.php:625
Definition: ISO2709.php:116
offsetSet( $offset, $value)
Definition: ISO2709.php:107
getTagPattern( $patt)
Definition: ISO2709.php:484
appendSubfield( $subfield, $arg2=null)
Definition: ISO2709.php:592
isTagValid( $tag)
Definition: ISO2709.php:160
loadFromBinaryStream( $filehandle)
Definition: ISO2709.php:225
getFields( $tag)
Definition: ISO2709.php:476
offsetUnset( $offset)
Definition: ISO2709.php:111
const RecordTerminator
Definition: ISO2709.php:121
assertTagRepeatable( $tag)
Definition: ISO2709.php:181
loadFromBinaryString( $string, $fuzzy=true)
Definition: ISO2709.php:258
insertSubfieldBefore( $identifier, $subfield, $arg3=null)
Definition: ISO2709.php:602
assertTagOrder( $a, $b)
Definition: ISO2709.php:209
__construct()
Definition: ISO2709.php:143
const FieldTerminator
Definition: ISO2709.php:122
checkString( $name, $trueValue, $string, $start=0, $length=false, $default=false)
Definition: ISO2709.php:238
getOneSubfield( $identifier)
Definition: ISO2709.php:631
const SubfieldInitiator
Definition: ISO2709.php:123
$indicatorLength
Definition: ISO2709.php:134
__construct( $input)
Definition: ISO2709.php:19
getSubfields( $identifier)
Definition: ISO2709.php:637
__construct( $identifier, $data)
Definition: ISO2709.php:665
assertTagValid( $tag)
Definition: ISO2709.php:164
AsBinaryString()
Definition: ISO2709.php:497
AppendFieldBinary( $tag, $data, $directoryEntry=array(), $reorder=false)
Definition: ISO2709.php:412
exception( $string)
Definition: ISO2709.php:216
__construct( $tag, $data, $indicators="", $subfields=array(), $directoryEntry=array(), $record=null)
Definition: ISO2709.php:575
setLeader( $string)
Definition: ISO2709.php:252
__construct( $input, $numCache=10)
Definition: ISO2709.php:54
AsMnemonicString( $leader_tag="LDR", $field_initiator="=", $tag_terminator=" ", $identifier_initiator="\, $field_terminator="\\", $record_terminator="\\", $space_replacer=" ")
Definition: ISO2709.php:550
AsMnemonicString( $field_initiator="=", $tag_terminator=" ", $identifier_initiator="\, $field_terminator="\\", $space_replacer=" ")
Definition: ISO2709.php:645
$exceptions
Definition: ISO2709.php:132
set_default( $name, $value)
Definition: ISO2709.php:138
offsetGet( $offset)
Definition: ISO2709.php:72
tagOrder( $a, $b)
Definition: ISO2709.php:196
CheckField( $field)
Definition: ISO2709.php:417
removeField( $field)
Definition: ISO2709.php:462
loadFromString( $string)
Definition: ISO2709.php:374
AppendField( $field, $reorder=false)
Definition: ISO2709.php:435
delFields( $tag)
Definition: ISO2709.php:469
ParseBinaryField( $tag, $data, $directoryEntry)
Definition: ISO2709.php:384
offsetExists( $offset)
Definition: ISO2709.php:66
isTagRepeatable( $tag)
Definition: ISO2709.php:177