-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgenerateAuthors.php
More file actions
executable file
·2086 lines (1773 loc) · 65.8 KB
/
Copy pathgenerateAuthors.php
File metadata and controls
executable file
·2086 lines (1773 loc) · 65.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<?php
# Class to generate the complex author fields
/* Mutations can happen as follows:
First round: looks at the very first entity (person/company/conference) where person is author/editor/etc.
For this item:
Start, assuming it is a 100
Obtain the value
|
|---> Value that has been found can stay as 100 then Add to record; end further processing of first round
|
|---> A mutatation into 110 may be detected; at this point any value is thrown away; then 110 processing starts
|---> 110 routine generates a value; then Add to record; end further processing of first round
|---> OR value can mutate into 710 then Add to record; end further processing of first round
|
|---> A mutatation into 111 may be detected; at this point any value is thrown away; then 111 processing starts
|---> 710 routine generates a value; then Add to record; end further processing of first round
|---> OR value can mutate into 711 then Add to record; end further processing of first round
|
|---> Value that has been found can become 700 then Add to record; end further processing of first round
Second round: looks each each other entity
For each one:
Start, assuming it is a 700
Obtain the value
|
|---> Value can stay as 700 then Add to record; go to next in loop
|
|---> Value can mutate into 710 then Add to record; go to next in loop
|
|---> Value can mutate into 711 then Add to record; go to next in loop
If there are four or more authors (i.e. with no relator term), excluding those pulled in from *kg host, convert the 100 to 700, e.g. /records/1648/ (test #895, #896)
*/
/* Key checks:
- The first *art/*ag/*a field should map to the 1XX field e.g. /records/7195/ (test #59)
- Any further *art/*ag/*a fields should map to 7XX fields e.g. /records/8249/ (test #60)
- Any *art/*ag/*al fields should map to 7XX fields e.g. /records/1963/ (test #61)
- Any *art/*e/*n fields should map to 7XX fields e.g. /records/5126/ (test #62)
- Four or more authors have their 100 turned to 700 e.g. /records/1648/ (test #895, #896)
However...
- Any *art/*in/*ag/*a fields should NOT map to a 1XX or 7XX field e.g. /records/1902/ (test #64)
- Any *art/*in/*ag/*al fields should NOT map to a 7XX field e.g. /records/3427/ (test #65)
- Any *art/*in/*e/*n fields should NOT map to a 7XX field [No cases] (checked again in Jan 2017, including *ee instead of *e in case of error)
*/
class generateAuthors
{
# Define subfields that are capable of being transliterated
private $transliterableSubfields = array (
100 => 'aqc',
110 => 'ab',
111 => 'anc',
700 => 'aqct',
710 => 'abt',
711 => 'anct',
);
# Constructor
public function __construct ($marcConversion, $languageModes)
{
# Create class property handles to the parent class
$this->marcConversion = $marcConversion;
$this->databaseConnection = $marcConversion->databaseConnection;
$this->settings = $marcConversion->settings;
$this->languageModes = $languageModes;
# Load lookups
$this->lookups = array (
'namesInDirectOrder' => $this->namesInDirectOrder (),
'surnameOnly' => $this->surnameOnly (),
'prefixes' => $this->prefixes (),
'suffixes' => $this->suffixes (),
'betweenN1AndN2' => $this->betweenN1AndN2 (),
'relatorTerms' => $this->relatorTerms (),
'miscList' => $this->miscList (),
'affiliationList' => $this->affiliationList (),
'dateList' => $this->dateList (),
'fullStopExceptionsList' => $this->fullStopExceptionsList (),
);
# Define unicode symbols
$this->doubleDagger = chr(0xe2).chr(0x80).chr(0xa1);
}
# Main entry point
public function createAuthorsFields ($mainRecordXml)
{
# Initalise a values list
$this->values = array ();
# Create a handle to the XML
$this->mainRecordXml = $mainRecordXml;
# Determine the language of the record; this uses only the first language, e.g. /records/83587/ (test #1028) which is enforced by macro_authorsField instantiation anyway; NB /records/1220/ , /records/8690/ have multiple languages (see also test #66)
$recordLanguage = $this->marcConversion->xPathValue ($mainRecordXml, '(//lang)[1]', false);
# Determine if *nt={BGNRus|LOCRus}, e.g. /records/102036/ (test #728) which is a Yakut record with *nt=BGNRus sections
$supportedNtTokensPresent = $this->marcConversion->supportedNtTokensPresent ($mainRecordXml);
# Process both normal and transliterated modes
foreach ($this->languageModes as $languageMode) {
# Initialise all fields, creating an empty array for each, into which lines can be registered
$fields = array (100, 110, 111, 700, 710, 711);
foreach ($fields as $field) {
$this->values[$languageMode][$field] = array ();
}
# For the non-default language mode, if the current language mode does not match a language of the record (and has no supported *nt tokens present), skip processing, e.g. /records/178029/ (test #875), and /records/5255/ (test #876)
$this->recordLanguageTransliterable = ($languageMode == $recordLanguage);
if ($languageMode != 'default') {
if (!$this->recordLanguageTransliterable && !$supportedNtTokensPresent) {
continue;
}
}
# Launch the two main entry points; each may include a mutation to a different field number
$this->generateFirstEntity ($languageMode); // Round one: first entity
$this->generateOtherEntities ($languageMode); // Round two: all other entities
}
# If there are four or more authors (i.e. with no relator term), excluding those pulled in from *kg host, convert the 100 to 700, e.g. /records/1648/ (test #895, #896)
$this->fourOrMoreAuthorsAdjustment ();
# Return the values
return $this->values;
}
# First entity generation entry point, which assumes 100 but may become 110/111/700/710/711
/*
* This is basically the first author.
* It may end up switching to 110/111/700/710/711 instead.
* Everyone else involved in the production ends put in 7xx fields.
*
*/
private function generateFirstEntity ($languageMode)
{
# Assume 100 by default
$this->field = 100;
# Set the language mode
$this->languageMode = $languageMode;
# Look at the first or only *doc/*ag/*a OR *art/*ag/*a
# *ser like /records/1062/ would not match (test #67)
# *doc like /records/1392/ will match (test #68)
# *art/in like /records/4179/ will match (test #69); its /art/in/ag/a will be ignored (test #70)
# *art/in with /art/in/ag/ but not /art/ag like /records/45318/ will not match (same as test #70)
# *art/j like /records/1109/ will match (test #71)
if (!$a = $this->marcConversion->xPathValue ($this->mainRecordXml, '//ag[parent::doc|parent::art]/a')) {
return false; // The entry in $this->values for this field will be left as when initialised, i.e. an empty array
}
# Do the classification; NB this may return false, e.g. /records/6883/, which is necessary to ensure that the 880 handling correctly receives a multiline where there are >1 lines being processed, e.g. /records/6883/ (test #872)
$xPath = '//ag[parent::doc|parent::art][1]/a[1]';
$line = $this->main ($this->mainRecordXml, $xPath, 100, $languageMode);
# Subfield ‡u, if present, needs to go before subfield ‡e (test #90)
$line = $this->shiftSubfieldU ($line);
# Pass through the transliterator if required; e.g. /records/6653/ (test #107), /records/23186/ (test #108); *nt handling: BGNRus example at /records/154475/ (test #880), and negative case at /records/102036/ (test #874)
$line = $this->transliterateNtAware ($line, $this->transliterableSubfields[$this->field], $languageMode, $xPath);
# Ensure the line ends with punctuation; e.g. /records/1218/ (test #72), /records/1221/ (test #73)
$line = $this->marcConversion->macro_dotEnd ($line, $extendedCharacterList = true);
# Write the value into the values registry
$this->values[$this->languageMode][$this->field][] = $line;
}
# Wrapper function to run the line through the transliterator, with *nt token support
private function transliterateNtAware ($line, $transliterableSubfields, $languageMode, $xPath)
{
# Perform transliteration
$line = $this->marcConversion->macro_transliterateSubfields ($line, $transliterableSubfields, $errorString_ignored, $languageMode);
# If in non-default language mode because of *nt tokens present (rather than the main record language being transliterable), determine whether to cancel the transliteration
if ($languageMode != 'default') {
if (!$this->recordLanguageTransliterable) {
# Obtain the *nt value for this path
$ntValue = $this->marcConversion->xPathValue ($this->mainRecordXml, $xPath . '/nt');
# If a supported token, use the transliteration, i.e. take no further action having done the transliteration, e.g. /records/102036/ (test #728) for BGNRus example, /records/160653/ (tests #877 and #878) for LOCRus example; if not (no *nt, or any other *nt value) cancel the line, e.g. 700 in /records/160653/ (test #879)
$supportedNtTokens = array ('BGNRus', 'LOCRus');
if (!in_array ($ntValue, $supportedNtTokens)) {
return false;
}
}
}
# Return the line, e.g. /records/23186/ (test #108) which is a Russian record with ordinary transliteration handling, or the *nt support examples as noted above, e.g. /records/102036/ (test #728)
return $line;
}
# Other entities generation entry point, which assumes 700 but may become 710/711; see: https://www.loc.gov/marc/bibliographic/bd700.html
/*
* This is basically all the people involved in the book except the first author, which if present is covered in 100/110/111. (tests #74, #75)
* It includes people in the analytic (child) records, but limited to the first of them for each such child record
* This creates multiple 700 lines, the lines being created as the outcome of the loop below
* Each "contributor block" referenced below refers to the author components, which are basically the 'classify' functions elsewhere in this class
*
* - Check there is *doc/*ag or *art/*ag (i.e. *ser records will be ignored)
* - Loop through each *ag
* - Within each *ag, for each *a and *al add the contributor block
* - In the case of each *ag/*al, the "*al Detail" block (and ", ‡g (alternative name)", once only) is added
* - Loop through each *e
* - Within each *e, for each *n add the contributor block
* - In the case of each *e/*n, *role, with Relator Term lookup substitution, is incorporated
* - When considering the *e/*n, there is a guard clause to skip cases of 'the author' as the 100 field would have already pulled in that person (e.g. the 100 field could create "<name> $eIllustrator" indicating the author <name> is also the illustrator)
* - Check for a *ke which is a flag indicating that there are analytic (child) records, e.g. as present in /records/7463/
* - Look up the records whose *kg matches, e.g. /records/9375/ has *kg=7463, so this indicates that 9375 (which will be an *art) is a child of 7463
* - For each *kg's *art (i.e. child *art record): take the first *art/*ag/*a/ (only the first) in that record within the *ag block, i.e. /records/9375/ /art/ag/a "contributor block" (test #76), and also add the title (i.e. *art/*tg/*t) (test #77); the second indicator is set to '2' to indicate that this 700 line is an 'Analytical entry' (test #78)
* - Every 700 has a fixed string ", ‡5 UkCU-P." at the end (representing the Institution to which field applies)
*
* Handling of multiple entries:
* - However many entries there are, each line is registered against the field number; the client code can then implode these into a multiline
* - After each line is registered, the field number is reset to 700 to ensure that a switch to say 710 is not leaky into the next entry
* - The ordering of 7xx fields ends up in numerical order, e.g. authorA then authorB may become 700 authorB then 710 authorA
*/
private function generateOtherEntities ($languageMode)
{
# Assume 700 by default
$this->field = 700;
# Set the language mode
$this->languageMode = $languageMode;
# Generate the 700 line values
$lines = $this->generateOtherEntitiesLines ($languageMode);
# End if no lines
if (!$lines) {
return false; // The entry in $this->values[$this->languageMode] for this field will be left as when initialised, i.e. an empty array
}
# Subfield ‡u, if present, needs to go before subfield ‡e (test #90)
foreach ($lines as $index => $line) {
$lines[$index]['line'] = $this->shiftSubfieldU ($line['line']);
}
# Pass each line through the transliterator if required (test #108, #109); *nt handling: BGNRus example at /records/102036/ (test #728), and negative case at /records/160653/ (test #879)
foreach ($lines as $index => $line) {
$fieldNumber = $line['field'];
$lines[$index]['line'] = $this->transliterateNtAware ($line['line'], $this->transliterableSubfields[$fieldNumber], $languageMode, $line['xPath']);
}
# Ensure each line ends with punctuation; e.g. /records/7463/ (tests #81 and #82)
foreach ($lines as $index => $line) {
$lines[$index]['line'] = $this->marcConversion->macro_dotEnd ($line['line'], $extendedCharacterList = true);
}
# Write the values, into the values registry; the field number is not part of the line itself, e.g. /records/1127/ (test #111)
foreach ($lines as $line) {
$fieldNumber = $line['field'];
$this->values[$this->languageMode][$fieldNumber][] = $line['line'];
}
}
# Function to adjust 100 to 700 when there are four or more authors (i.e. with no relator term), excluding those pulled in from *kg host, e.g. /records/1648/ (test #895, #896)
private function fourOrMoreAuthorsAdjustment ()
{
# Count pure authors, i.e. those without a relator term, and without a *kg host -derived title, also excluding alternative name entries
# This count is done in the default language mode, because Russian can have *nt=BGNRus handling causing empty slots to avoid 880 mismatches, e.g. /records/151048/ (test #898)
$pureAuthors = array ();
$fields = array (100, 700);
foreach ($fields as $field) {
foreach ($this->values['default'][$field] as $entry) {
# Skip those with relator term (editors, illustrators, compilers, etc.) (which is always $e for 100 not $j), e.g. /records/154475/ (test #900)
if (substr_count ($entry, "{$this->doubleDagger}e")) {continue;}
# Skip those with pulled in from *kg host, which have a title, e.g. /records/109111/ (test #899)
if (substr_count ($entry, "{$this->doubleDagger}t")) {continue;}
# Exclude alternative name entries from consideration, as these are not real authors, e.g. /records/7624/ (test #902)
if (substr_count ($entry, "{$this->doubleDagger}g(alternative name)")) {continue;}
# Add to the count, registering the field
$pureAuthors[] = $field;
}
}
# End if not four more or authors, e.g. /records/154475/ (test #901)
if (count ($pureAuthors) < 4) {return;}
# End if there is no 100 amongst the pure authors, e.g. /records/121744/ (test #903, though not properly testable as the effect is to create an invalid record with a 700 field but no line, which the marcParser routine used by the test system cannot recognise)
if (!in_array (100, $pureAuthors)) {return;}
# Shift the 100 author (there can only ever be one) to the start of the 700 and reindex, for each language mode, e.g. /records/1648/ (test #895, #896), transliterated example at /records/9995/ (test #897)
foreach ($this->languageModes as $languageMode) {
$new700Entry = array_pop ($this->values[$languageMode][100]);
array_unshift ($this->values[$languageMode][700], $new700Entry);
}
}
# Inner function for generateOtherEntities, covering everything except the final compilation of lines into a single string
private function generateOtherEntitiesLines ($languageMode)
{
# Assume 700 by default
$this->field = 700;
# Start a list of 700 line values; these are tuples as array(field,line,nt); these are indexed numerically, rather than associatively as field=>line, as there may be more than one field instance
# NB The client code (not this registry) then handles multiline, so that e.g. a 710 doesn't get stuck amongst 700s, e.g. /records/16272/ (test #755)
# It is considered acceptable that the order therefore does not necessary stay the same, i.e. authorA then authorB may become 700 authorB then 710 authorA; this is probably more logical anyway in that the MARC field order then remains correct rather than e.g. 700 710 700; this is not defined at https://www.loc.gov/marc/specifications/specrecstruc.html#varifields
$lines = array ();
# If there is already a 700 field arising from generateFirstEntity, which will be a standard scalar string, register this first by transfering it into the lines format and resetting the 700 registry, e.g. /records/23186/ (test #109)
if ($this->values[$this->languageMode][700]) {
foreach ($this->values[$this->languageMode][700] as $line) {
$lines[] = array ('field' => 700, 'line' => $line, 'xPath' => NULL);
}
$this->values[$this->languageMode][700] = array (); // Reset
}
# Check it is *doc/*ag or *art/*ag (i.e. ignore *ser records), e.g. /records/107192/ (test #110)
# After this point, the only looping is through top-level *a* fields, e.g. /*/ag but not /*/in/ag
if ($this->marcConversion->xPathValue ($this->mainRecordXml, '/ser')) {
return $lines;
}
# Loop through each *ag
$agIndex = 1;
while ($this->marcConversion->xPathValue ($this->mainRecordXml, "/*/ag[$agIndex]")) {
# Loop through each *a (author) in this *ag (author group)
$aIndex = 1; // XPaths are indexed from 1, not 0
while ($this->marcConversion->xPathValue ($this->mainRecordXml, "/*/ag[$agIndex]/a[{$aIndex}]")) {
# Skip the first /*ag/*a, e.g. /records/1127/ (tests #112, #113)
if ($agIndex == 1 && $aIndex == 1) {
$aIndex++;
continue;
}
# Obtain the value
$xPath = "/*/ag[$agIndex]/a[{$aIndex}]";
$line = $this->main ($this->mainRecordXml, $xPath, 700, $languageMode);
# Register the line, setting the field code, which may have been modified in main(), e.g. /records/1127/ (test #114); no examples found for *nt handling as per `SELECT * FROM catalogue_processed WHERE field = 'nt' AND xPath LIKE '%/ag/a/%' AND xPathWithIndex LIKE '%[2]%' AND recordLanguage != 'Russian';`
$lines[] = array ('field' => $this->field, 'line' => $line, 'xPath' => $xPath);
# Next *a, e.g. /records/132356/ (test #115)
$aIndex++;
}
# Loop through each *al (author) in this *ag (author group), e.g. /records/1565/ (test #116), and *nt handling example: /records/40263/ (test #881)
$alIndex = 1; // XPaths are indexed from 1, not 0
while ($this->marcConversion->xPathValue ($this->mainRecordXml, "/*/ag[$agIndex]/al[{$alIndex}]")) {
# Obtain the value
$xPath = "/*/ag[$agIndex]/al[{$alIndex}]";
$line = $this->main ($this->mainRecordXml, $xPath, 700, $languageMode);
# The "*al Detail" block (and ", ‡g (alternative name)", once only) is added, e.g. /records/29234/ (test #118)
if ($line) {
if (!substr_count ($line, "{$this->doubleDagger}g")) { // No actual cases found, so this block will always be entered
$line = $this->marcConversion->macro_dotEnd ($line, $extendedCharacterList = '.?!'); // e.g. /records/2787/ ; "700: Subfield g must be preceded by a full stop, question mark or exclamation mark." (test #83)
$line .= "{$this->doubleDagger}g" . '(alternative name)';
}
}
# Register the line, setting the field code, which may have been modified in main()
$lines[] = array ('field' => $this->field, 'line' => $line, 'xPath' => $xPath);
# Next *al, e.g. /records/29234/ (test #117)
$alIndex++;
}
# Next *ag
$agIndex++;
}
# Loop through each *e; *nt elsewhere handling negative example in /records/154475/ (test #882), but no positive cases available as per `SELECT * FROM catalogue_processed WHERE field = 'nt' AND xPath LIKE '%/e/%' AND recordLanguage != 'Russian'`
$eIndex = 1;
while ($this->marcConversion->xPathValue ($this->mainRecordXml, "/*/e[$eIndex]")) {
# Within each *e, for each *n add the contributor block, e.g. /records/1247/ (test #119)
$nIndex = 1; // XPaths are indexed from 1, not 0
while ($this->marcConversion->xPathValue ($this->mainRecordXml, "/*/e[$eIndex]/n[{$nIndex}]")) {
# When considering the *e/*n, there is a guard clause to skip cases of 'the author' as the 100 field would have already pulled in that person (e.g. the 100 field could create "<name> $eIllustrator" indicating the author <name> is also the illustrator); e.g. /records/147053/ (test #84), /records/23965/ (test #1026), /records/23161/ (test #1027)
$n1 = $this->marcConversion->xPathValue ($this->mainRecordXml, "/*/e[$eIndex]/n[{$nIndex}]/n1");
if ($n1 == 'the author') {
$nIndex++;
continue;
}
# Obtain the value
# In the case of each *e/*n, *role, with Relator Term lookup substitution, is incorporated; e.g. /records/47079/ (test #85) ; this is done inside classifyAdField ()
$xPath = "/*/e[$eIndex]/n[{$nIndex}]";
$line = $this->main ($this->mainRecordXml, $xPath, 700, $languageMode);
# Register the line, if it has resulted in a line, setting the field code, which may have been modified in main()
if ($line) { // E.g. /records/8988/ which has "others" should not result in a line for /*/e[1]/n[2] due to classifyN1Field having "return false" (test #86)
$lines[] = array ('field' => $this->field, 'line' => $line, 'xPath' => $xPath);
}
# Next *n, e.g. /records/2295/ (test #120)
$nIndex++;
}
# Next *e
$eIndex++;
}
# Check for a *ke which is a flag indicating that there are analytic (child) records; e.g. /records/7463/, /records/1895/ (test #1123)
if ($this->marcConversion->xPathValue ($this->mainRecordXml, '//ke')) { // Is just a flag, not a useful value (test #87); e.g. /records/1221/ contains "\<b> Analytics \<b(l) ~l 1000/"ME1221"/ ~>" which creates a button in the Muscat GUI
# Look up the records whose *kg matches, e.g. /records/9375/ has *kg=7463, so this indicates that 9375 (which will be an *art) is a child of 7463 (tests #76 and #77)
$currentRecordId = $this->marcConversion->xPathValue ($this->mainRecordXml, '/q0');
if ($children = $this->getAnalyticChildren ($currentRecordId)) { // Returns records as array (id=>xmlObject, ...)
# Loop through each *kg's *art (i.e. child *art record)
foreach ($children as $id => $childRecordXml) {
# Take the first *art/*ag/*a/ (only the first (test #88)) in that record within the *ag block, i.e. /records/9375/ /art/ag/a "contributor block" (test #76); the second indicator is set to '2' to indicate that this 700 line is an 'Analytical entry' (test #78)
$xPath = "/*/ag[1]/a[1]";
$line = $this->main ($childRecordXml, $xPath, 700, $languageMode, '2', $omitAnon = false); // Omit anon disabled, so that the $a gets generated, e.g. /records/2070/ (test #1049)
# Add the title (i.e. *art/*tg/*t)
if ($line) {
$extendedCharacterList = ($this->field == 710 ? '.' : '?.-)'); // 710 doesn't allow ) but e.g. 700 does; e.g. /records/215849/ (tests #778, #779) ; "700: Subfield _t must be preceded by a question mark, full stop, hyphen or closing parenthesis."
$line = $this->marcConversion->macro_dotEnd ($line, $extendedCharacterList); // (test #89) e.g. /records/9843/ , /records/13620/
$line .= "{$this->doubleDagger}t" . $this->marcConversion->xPathValue ($childRecordXml, '/*/tg/t');
}
# Register the line, setting the field code, which may have been modified in main()
$lines[] = array ('field' => $this->field, 'line' => $line, 'xPath' => NULL); // No cases of *nt handling (which would be buggy as xPath needs to be for the child not the current record), as shown with `SELECT * FROM catalogue_processed WHERE field = 'kg' AND value IN(SELECT recordId FROM `catalogue_processed` WHERE field = 'nt' and xPath LIKE '%/ag/a/%' and recordLanguage = 'Russian' and value IN('BGNRus', 'LOCRus'));`
}
}
}
# Return the lines
return $lines;
}
# Function to shift subfield ‡u, if present, to go before subfield ‡e (test #90); e.g. /records/127378/ , /records/134669/ , /records/135235/
private function shiftSubfieldU ($line)
{
# Take no action unless both $u and $e are present
if (!substr_count ($line, "{$this->doubleDagger}u") || !substr_count ($line, "{$this->doubleDagger}e")) {
return $line;
}
# Move $u block to just before $e, leaving all others in place
$line = preg_replace ("/^(.*)(,{$this->doubleDagger}e.*)(, {$this->doubleDagger}u[^{$this->doubleDagger}]+)(.*\.)$/u", '\1\3\2\4', $line);
# Return the result
return $line;
}
# Function to obtain the analytic children
private function getAnalyticChildren ($parentId)
{
# Get the children
# NB This works correctly when a child has two parents (see also lookupHostRecord ()), i.e. *k2[2]/kg is present in the child, e.g. /records/1896/ (test #763) which joins to /records/11625/ and /records/1895/ (test #1123)
$childIds = $this->databaseConnection->selectPairs ($this->settings['database'], 'catalogue_processed', array ('field' => 'kg', 'value' => $parentId), array ('recordId'));
# Load the XML records for the children
$children = $this->databaseConnection->selectPairs ($this->settings['database'], 'catalogue_xml', array ('id' => $childIds), array ('id', 'xml'));
# Convert each XML record string to an XML object
$childrenRecords = array ();
foreach ($children as $id => $record) {
$childrenRecords[$id] = $this->marcConversion->loadXmlRecord ($record);
}
# Return the records
return $childrenRecords;
}
# Function providing an entry point into the main classification block, which switches between the name format
private function main ($xml, $path, $defaultFieldCode, $languageMode, $secondIndicator = '#', $omitAnon = true)
{
# Start the value
$value = '';
# Set (or reset) the field code so that every processing is guaranteed to have a clean start
$this->field = $defaultFieldCode;
# Create a handle to the context1xx flag
$this->context1xx = (mb_substr ($defaultFieldCode, 0, 1) == 1); // i.e. true if 1xx but not 7xx
# Create a handle to the XML for this field
$this->xml = $xml;
# Create a handle to the second indicator
$this->secondIndicator = $secondIndicator;
# If *nt=None, which should disable transliteration, set a flag so that the eventual value is wiped below but the rest of the processing, which may result in field number mutation occuring, continues, e.g. /records/6883/ (test #872)
$transliterationDisabledNt = $this->transliterationDisabledNt ($path, $languageMode);
# Does the *a contain a *n2?
$n2 = $this->marcConversion->xPathValue ($this->xml, $path . '/n2');
$n1 = $this->marcConversion->xPathValue ($this->xml, $path . '/n1');
if (strlen ($n2)) {
# Add to 100 field: 1, second indicator, ‡a <*a/*n1>, ; no space between $a and name, e.g. /records/1103/ (test #795)
$value .= "1{$this->secondIndicator} {$this->doubleDagger}a{$n1}, ";
# Classify *n2 field
$value = $this->classifyN2Field ($path, $value, $n2);
} else {
# Classify *n1 field
$value = $this->classifyN1Field ($path, $value, $n1, $omitAnon);
}
# If the line has been set to be wiped, due to *nt=None, erase its value; the calling code will retain the slot for this line (ensuring that 880 handling correctly receives a multiline where there are >1 lines being processed, thus avoiding 880 mismatches) but the line will be empty, e.g. /records/6883/ (test #872)
if ($transliterationDisabledNt) {return false;}
# Return the value
return $value;
}
# Function to determine any *nt modifier; values available are None, BGNRus, LOCRus
private function transliterationDisabledNt ($path, $languageMode)
{
# In default mode, *nt is not relevant
if ($languageMode == 'default') {
return NULL;
}
# Get the *nt value, if present
$nt = $this->marcConversion->xPathValue ($this->mainRecordXml, $path . '/nt');
# Return whether transliteration is disabled, i.e. by the presence of *nt=None
$isTransliterationDisabled = ($nt == 'None');
return $isTransliterationDisabled;
}
# Function to classify *n1 field
private function classifyN1Field ($path, $value, $n1, $omitAnon)
{
# Start the value for this section
$value = '';
# Is the *n1 exactly equal to a set of specific strings?
$strings = array (
'other members of the expedition',
'others', // E.g. /records/8988/ (test #86)
);
if (application::iin_array ($n1, $strings)) {
# If yes, no 100 field (or any 1XX field) required
return false; // Resets $value
}
# Is the *n1 exactly equal to a set of specific strings? E.g. /records/68219/ (test #123)
$strings = array (
'-',
'Anon',
'Anon.',
'[Anon.]',
'[n.p.]',
'Unknown',
);
if (application::iin_array ($n1, $strings)) {
# Add to 100 field
$value .= "0{$this->secondIndicator} {$this->doubleDagger}aAnonymous";
# GO TO: Classify *ad Field
$value = $this->classifyAdField ($path, $value, $hasAd /* returned by reference */);
# If no *ad is present, omit Anon entirely (if enabled), so that no 1xx/7xx line is generated, e.g. /records/213264/ (test #1047), /records/124316/ (test #1048) which pulls in record 123915 that has Anon. handling removing 100 line "$aAnonymous.", so ends up using the 245 version
if (!$hasAd) {
if ($omitAnon) { // Not enabled for 700 *ke handling, e.g. /records/2070/ (test #1049)
return false;
}
}
# Return the value, under the *ad present scenario (or $omitAnon), e.g. /records/39875/ (test #1046)
return $value;
}
# Is the *n1 exactly equal to any of the names in the 'Name in direct order' tab? E.g. /records/181460/ (test #124)
$strings = $this->entitiesToUtf8List ($this->lookups['namesInDirectOrder']);
if (in_array ($n1, $strings)) {
# Add to 100 field
$value .= "0{$this->secondIndicator} {$this->doubleDagger}a" . $this->spaceOutInitials ($n1); // Spacing-out needed in e.g. /records/213499/ (test #125)
# Classify *nd Field
$value = $this->classifyNdField ($path, $value);
# End
return $value;
}
# Is the *n1 exactly equal to any of the names in the 'Surname only' tab? E.g. /records/111558/ (test #126), /records/3904/ (test #127) which has HTML entities
$surnameOnly = $this->entitiesToUtf8List ($this->lookups['surnameOnly']);
if (in_array ($n1, $surnameOnly)) {
# Add to 100 field
$value .= "1{$this->secondIndicator} {$this->doubleDagger}a{$n1}";
# Classify *nd Field
$value = $this->classifyNdField ($path, $value);
# End
return $value;
}
# Explicitly throw away the so-far generated value
$value = false;
# Is the *n1 a conference? E.g. /records/50035/ (test #128)
if ($this->isConference ($n1)) {
# Mutate to 111/711 field instead of 100/700 field; e.g. /records/88204/ (test #792)
$value = $this->generateX11 ($path);
} else {
# Mutate to 110/710 field instead of 100/700 field
$value = $this->generateX10 ($path);
}
# Return the overwritten value
return $value;
}
# Helper function to determine if an *n1 is conference-like, e.g. /records/50035/ (test #128)
private function isConference ($n1)
{
# Does the *n1 contain any of the following specific strings?
$strings = array (
'colloque',
'colloquy',
'conference', // /records/50035/ (test #128)
'congrés',
'congrès', // /records/8728/ (test #129)
'congreso',
'congress', // but NOT 'United States' - see below, including tests
'konferent', // Originally 'konferentsiya' but that is the pre-transliteration value; checked that this does not create mistaken hits; e.g. /records/32818/ (test #130)
'konferenzen',
'inqua',
'polartech',
'symposium', // /records/88204/ (test #792)
'tagung',
);
$strings = $this->entitiesToUtf8List ($strings);
# Search for a match
foreach ($strings as $string) {
if (substr_count (strtolower ($n1), strtolower ($string))) {
if (($string == 'congress') && (substr_count (strtolower ($n1), strtolower ('United States')))) {continue;} // Whitelist this one; e.g. /records/55763/ (test #131) and /records/1912/ (test #132)
# Match is found
return true;
}
}
# No match found
return false;
}
# Function to generate a 110/710 field
private function generateX10 ($path)
{
# Assume 110/710 by default
$this->field += 10; // 100->110, 700->710
# Start the value for this section
$value = '';
# Look at the first or only *doc/*ag/*a OR *art/*ag/*a
$n1 = $this->marcConversion->xPathValue ($this->xml, $path . '/n1');
# Does the *a/*n1 contain '. ' (i.e. full stop followed by a space)?
# Is the *n1 exactly equal to one of the names listed in the 'Full Stop Space Exceptions' tab? (test #94)
if (substr_count ($n1, '. ') && !in_array ($n1, $this->lookups['fullStopExceptionsList'])) {
# Add to 110 field: 2# ‡a <*a/*n1 [portion up to and including first full stop]> ‡b <*a/*n1 [everything after first full stop]>; e.g. /records/12195/ (test #93); e.g. /records/127474/ (test #94), /records/1261/
$n1Components = explode ('.', $n1, 2);
$value .= "2# {$this->doubleDagger}a{$n1Components[0]}.{$this->doubleDagger}b{$n1Components[1]}";
} else {
# Add to 110 field: 2# ‡a <*a/*n1>; e.g. /records/127474/ (test #94)
$value .= "2# {$this->doubleDagger}a{$n1}";
}
# GO TO: Classify *nd Field
$value = $this->classifyNdField ($path, $value);
# Return the value
return $value;
}
# Function to generate a 111/711 field; e.g. only 711 in /records/88204/ (test #792)
# 711 should be rare, as would be a conference as the second/third/etc. name entry
private function generateX11 ($path)
{
# Assume 111/711 by default
$this->field += 11; // 100->111, 700->711
# Start the value for this section
$value = '';
# Look at the first or only *doc/*ag/*a OR *art/*ag/*a
$n1 = $this->marcConversion->xPathValue ($this->xml, $path . '/n1');
# Parse the conference name
$value = $this->parseConferenceTitle ($n1);
# Classify *nd field
$value = $this->classifyNdField ($path, $value);
# Return the value
return $value;
}
# Function to parse a conference title; see: https://www.loc.gov/marc/bibliographic/bd111.html
private function parseConferenceTitle ($n1)
{
# Convert separator used in the data from , to ;
$n1 = str_replace (', ', '; ', $n1);
# Revert real commas that are not separators
$whitelistStrings = array (
// Present in meeting name:
'Aerosols, Condensation',
'Mass-Balance, Fluctuations',
// Present in Location of meeting:
'Washington, D.C.',
'Edmonton, Alberta', // /records/55264/ (test #137)
'Yakutsk, Siberia, U.S.S.R',
);
$replacements = array ();
foreach ($whitelistStrings as $whitelistString) {
$find = str_replace (', ', '; ', $whitelistString);
$replacements[$find] = $whitelistString; // e.g. 'Washington; D.C.' => 'Washington, D.C.'
}
$n1 = strtr ($n1, $replacements);
# Explode the components
$conferenceAttributes = explode ('; ', $n1);
# Start the value for this section, which is $a<conferencename>
$value = "2# {$this->doubleDagger}a" . $conferenceAttributes[0];
# Assemble according to number of parts
$totalParts = count ($conferenceAttributes);
switch ($totalParts) {
# Simple conference name; e.g. 'Arctic Science Conference'
case 1:
// No addition; e.g. /records/173340/ (test #133)
break;
# Conference and date; e.g. 'Symposium on Antarctic Resources, 1978' /records/57564/ (test #134)
case 2:
$value .= "{$this->doubleDagger}d({$conferenceAttributes[1]})"; // Note no space before $d, e.g. /records/57564/ (test #560)
break;
# Conference, date and location; e.g. 'Conference on Antarctica, Washington, D.C., 1959' /records/32965/ (test #135)
case 3:
$value .= "{$this->doubleDagger}d({$conferenceAttributes[2]} :{$this->doubleDagger}c{$conferenceAttributes[1]})";
break;
# Conference, number, date and location; e.g. 'International Conference on Permafrost, 2nd, Yakutsk, Siberia, U.S.S.R, 1973' /records/51434/ (test #136)
case 4:
$value .= " {$this->doubleDagger}n({$conferenceAttributes[1]} :{$this->doubleDagger}d{$conferenceAttributes[3]} :{$this->doubleDagger}c{$conferenceAttributes[2]})";
break;
}
# Return the value
return $value;
}
# Function to classify *n2 field
private function classifyN2Field ($path, $value, $n2)
{
# Is the *n2 exactly equal to a set of specific names?
$names = array (
'David B. (David Bruce)', // /records/170179/ (test #139)
'H. (Hervé)', // /records/4366/ (test #140)
'H. (Hippolyte)',
'K.V. (Konstantin Viktorovich)',
'L. (Letterio)',
'M. M. (Marilyn M.)',
'O. (Osmund)',
'R.D. (Reginald D.)',
'R.D. (Robert D.)',
'V. C. (Vanessa C.)',
);
$names = $this->entitiesToUtf8List ($names);
if (in_array ($n2, $names)) {
# Add to 100 field: <*a/*n2 [portion before brackets]> ‡q<*a/*n2 [portion in brackets, including brackets]>
preg_match ('/^(.+) (\(.+\))$/', $n2, $matches);
$n2FieldValue = $matches[1];
$n2FieldValue .= " {$this->doubleDagger}q" . $matches[2];
} else {
# Add to 100 field: <*a/*n2>; e.g. /records/1296/ (test #138)
$n2FieldValue = $n2;
}
# Any initials in the $a subfield should be separated by a space (test #91); e.g. /records/1296/ ; note that 245 $c does not seem to do the same: https://www.loc.gov/marc/bibliographic/bd245.html (test #92)
$n2FieldValue = $this->spaceOutInitials ($n2FieldValue); // Spacing-out needed in e.g. /records/1296/ (test #91)
# Add the value
$value .= $n2FieldValue;
# Classify *nd Field
$value = $this->classifyNdField ($path, $value);
# Return the value
return $value;
}
# Function to expand initials to add spaces (test #91); note that 245 $c requires the opposite - see spaceOutInitials() in generate245 (test #92)
public function spaceOutInitials ($string)
{
# Any initials should be separated by a space; e.g. /records/1296/
# This is tolerant of transliterated Cyrillic values (test #95), e.g. /records/175507/ or (old example) /records/194996/ which has "Ye.V." to become "E.V."
$regexp = '/\b([^ ]{1,2})(\.)([^ ]{1,2})/u'; // Unicode flag needed given e.g. Polish initial in /records/201319/ (test #96) (and therefore parent record /records/44492/ (test #97))
while (preg_match ($regexp, $string)) {
$string = preg_replace ($regexp, '\1\2 \3', $string);
}
# Return the amended string
return $string;
}
# Function to classify *nd field
private function classifyNdField ($path, $value)
{
# Does the *a contain a *nd? E.g. /records/1221/ (test #141)
$nd = $this->marcConversion->xPathValue ($this->xml, $path . '/nd');
if (!strlen ($nd)) {
# If no, GO TO: Classify *ad Field; e.g. /records/1201/ (test #142)
$value = $this->classifyAdField ($path, $value);
# Return the value
return $value;
}
# If present, strip out leading '\v' and trailing '\n' italics; e.g. /records/45578/ (test #98)
$nd = strip_tags ($nd);
# Is the *nd exactly equal to set of specific strings?
$strings = array (
'Sr SGM' => ",{$this->doubleDagger}cSr, {$this->doubleDagger}uSGM",
'Lord, 1920-1999' => ",{$this->doubleDagger}cLord,{$this->doubleDagger}d1920-1999", // Note no space before $d, e.g. /records/172094/ (test #559)
'Rev., O.M.I.' => ",{$this->doubleDagger}cRev.,{$this->doubleDagger}uO.M.I.",
'I, Prince of Monaco' => "{$this->doubleDagger}bI,{$this->doubleDagger}cPrince of Monaco", // E.g. /records/165177/ (test #99)
'Baron, 1880-1957' => ",{$this->doubleDagger}cBaron,{$this->doubleDagger}d1880-1957",
);
if (array_key_exists ($nd, $strings)) {
# Classify multiple value *nd field
$value = $this->classifyMultipleValueNdField ($value, $nd, $strings);
} else {
# Classify single value *nd field
$value = $this->classifySingleValueNdField ($value, $nd);
}
# GO TO: Classify *ad Field
$value = $this->classifyAdField ($path, $value);
# Return the value
return $value;
}
# Function to classify multiple value *nd field
private function classifyMultipleValueNdField ($value, $nd, $strings)
{
# Add the looked-up value
$value .= $strings[$nd];
# Return the value
return $value;
}
# Function to classify single value *nd field
private function classifySingleValueNdField ($value, $nd)
{
# Delegate
return $value = $this->_classifySingleValueNdOrAdField ($value, $nd, false);
}
# Helper function to classify a single value *nd or *ad field
private function _classifySingleValueNdOrAdField ($value, $fieldValue, $checkDateList)
{
# Does the value of the $fieldValue appear on the Prefix list?
# Does the value of the $fieldValue appear on the Suffix list?
# Does the value of the $fieldValue appear on the Between *n1 and *n2 list?
$prefixes = $this->entitiesToUtf8List ($this->lookups['prefixes']); // E.g. /records/1201/ (test #142), /records/53959/ (test #143)
$suffixes = $this->entitiesToUtf8List ($this->lookups['suffixes']); // E.g. /records/23362/ (test #144)
$betweenN1AndN2 = $this->entitiesToUtf8List ($this->lookups['betweenN1AndN2']); // E.g. /records/3180/ (test #145)
if (in_array ($fieldValue, $prefixes) || in_array ($fieldValue, $suffixes) || in_array ($fieldValue, $betweenN1AndN2)) {
$value .= ",{$this->doubleDagger}c{$fieldValue}"; // No space between comma and $c or between the $c and the title, e.g. /records/1290/ (test #796)
return $value;
}
# Check the date list if required
if ($checkDateList) {
# Does the value of the $fieldValue appear on the Date list? E.g. /records/6575/ (test #100)
if (in_array ($fieldValue, $this->lookups['dateList'])) {
$value .= ",{$this->doubleDagger}d {$fieldValue}"; // Avoid space after comma to avoid Bibcheck error "100: Subfield d must be preceded by a comma" in /records/6575/ (test #101)
return $value;
}
}
# Do one or more words or phrases in the $fieldValue appear in the Relator terms list? E.g. /records/10004/ (test #147), /records/181142/ (test #146)
if ($relatorTermsEField = $this->relatorTermsEField ($fieldValue)) {
$value .= $relatorTermsEField;
return $value;
}
# Does the value of the $fieldValue appear on the Misc. list? E.g. /records/1218/ (test #148)
if (in_array ($fieldValue, $this->lookups['miscList'])) {
$value = $this->marcConversion->macro_dotEnd ($value, $extendedCharacterList = '.?!'); // "700: Subfield g must be preceded by a full stop, question mark or exclamation mark." (test #148)
$value .= "{$this->doubleDagger}g ({$fieldValue})";
return $value;
}
# Does the value of the $fieldValue appear on the Affiliation list? E.g. /records/19171/ (test #150), /records/18045/ (test #151)
if (in_array ($fieldValue, $this->lookups['affiliationList'])) {
$value .= ", {$this->doubleDagger}u{$fieldValue}";
return $value;
}
# No change
return $value;
}
# Function to get the relator terms
private function getRelatorTerms ($valueForPrefiltering)
{
# Process the raw relator terms list into value => replacement; these have already been checked for uniqueness when replaced
$relatorTerms = array ();
foreach ($this->lookups['relatorTerms'] as $parent => $children) {
foreach ($children as $child) {
# Deal with pre-filters, which contain // in the terms list
if (preg_match ('|(.+)//(.+):(.+)|', $child, $matches)) {
$valueForPrefiltering = strtolower ($valueForPrefiltering);
$matches[3] = strtolower ($matches[3]);
# Determine whether to keep the entry in place
switch ($matches[2]) {
# Only - the entire string must match the specified value; e.g. "with//ONLY:with" will be ignored if the $valueForPrefiltering was "with foo"; e.g. /records/122529/ (test #152), /records/1253/ (test #153)
case 'ONLY':
$keep = ($matches[3] == $valueForPrefiltering);
break;
# Not - the string must not contain the specified value; e.g. "director//NOT:art director" will be ignored if the $valueForPrefiltering was "art director"; e.g. // /records/44786/ (test #154), /records/24674/ (test #155)
case 'NOT':
$keep = (!substr_count ($valueForPrefiltering, $matches[3])); // Partial match, e.g. 'revised//NOT:revised translation' means that "revised translation" (matches[3]) should not match *role="Revised translation by" in e.g. /records/24674/ (test #155)
break;
# Requires - the overall record must have the specified XPath entry; e.g. // /records/139689/ (test #156), /records/101462/ (test #157)
case 'REQUIRES':
$keep = ($this->marcConversion->xPathValue ($this->xml, $matches[3]));
break;
}