@natlibfi/melinda-record-matching 1.0.9 → 2.0.1-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/candidate-search/candidate-search-utils.js +58 -0
  2. package/dist/candidate-search/candidate-search-utils.js.map +1 -0
  3. package/dist/candidate-search/index.js +16 -5
  4. package/dist/candidate-search/index.js.map +1 -1
  5. package/dist/candidate-search/index.spec.js +94 -86
  6. package/dist/candidate-search/index.spec.js.map +1 -1
  7. package/dist/candidate-search/query-list/bib.js +113 -23
  8. package/dist/candidate-search/query-list/bib.js.map +1 -1
  9. package/dist/candidate-search/query-list/bib.spec.js +25 -23
  10. package/dist/candidate-search/query-list/bib.spec.js.map +1 -1
  11. package/dist/candidate-search/query-list/index.js +6 -4
  12. package/dist/candidate-search/query-list/index.js.map +1 -1
  13. package/dist/index.js +3 -3
  14. package/dist/index.js.map +1 -1
  15. package/dist/index.spec.js +53 -46
  16. package/dist/index.spec.js.map +1 -1
  17. package/dist/match-detection/features/bib/all-source-ids.js +122 -0
  18. package/dist/match-detection/features/bib/all-source-ids.js.map +1 -0
  19. package/dist/match-detection/features/bib/authors.js.map +1 -1
  20. package/dist/match-detection/features/bib/bibliographic-level.js.map +1 -1
  21. package/dist/match-detection/features/bib/host-component.js.map +1 -1
  22. package/dist/match-detection/features/bib/index.js +32 -16
  23. package/dist/match-detection/features/bib/index.js.map +1 -1
  24. package/dist/match-detection/features/bib/index.spec.js +50 -42
  25. package/dist/match-detection/features/bib/index.spec.js.map +1 -1
  26. package/dist/match-detection/features/bib/isbn.js.map +1 -1
  27. package/dist/match-detection/features/bib/issn.js.map +1 -1
  28. package/dist/match-detection/features/bib/language.js.map +1 -1
  29. package/dist/match-detection/features/bib/melinda-id.js +52 -0
  30. package/dist/match-detection/features/bib/melinda-id.js.map +1 -0
  31. package/dist/match-detection/features/bib/melinda-identifier-factory.js +100 -0
  32. package/dist/match-detection/features/bib/melinda-identifier-factory.js.map +1 -0
  33. package/dist/match-detection/features/bib/other-standard-identifier.js.map +1 -1
  34. package/dist/match-detection/features/bib/publication-time.js.map +1 -1
  35. package/dist/match-detection/features/bib/record-type.js.map +1 -1
  36. package/dist/match-detection/features/bib/standard-identifier-factory.js.map +1 -1
  37. package/dist/match-detection/features/bib/title.js.map +1 -1
  38. package/dist/match-detection/features/index.js +2 -2
  39. package/dist/match-detection/index.js +10 -6
  40. package/dist/match-detection/index.js.map +1 -1
  41. package/dist/match-detection/index.spec.js +32 -25
  42. package/dist/match-detection/index.spec.js.map +1 -1
  43. package/dist/matching-utils.js +95 -0
  44. package/dist/matching-utils.js.map +1 -0
  45. package/package.json +26 -29
  46. package/src/candidate-search/candidate-search-utils.js +53 -0
  47. package/src/candidate-search/index.js +11 -1
  48. package/src/candidate-search/index.spec.js +64 -58
  49. package/src/candidate-search/query-list/bib.js +130 -20
  50. package/src/candidate-search/query-list/bib.spec.js +16 -14
  51. package/src/candidate-search/query-list/index.js +3 -1
  52. package/src/index.spec.js +46 -39
  53. package/src/match-detection/features/bib/all-source-ids.js +122 -0
  54. package/src/match-detection/features/bib/index.js +2 -0
  55. package/src/match-detection/features/bib/index.spec.js +32 -23
  56. package/src/match-detection/features/bib/melinda-id.js +34 -0
  57. package/src/match-detection/features/bib/melinda-identifier-factory.js +94 -0
  58. package/src/match-detection/index.js +7 -4
  59. package/src/match-detection/index.spec.js +27 -20
  60. package/src/matching-utils.js +86 -0
@@ -27,6 +27,125 @@
27
27
  *
28
28
  */
29
29
  import createDebugLogger from 'debug';
30
+ import {toQueries} from '../candidate-search-utils';
31
+ import {getMelindaIdsF035, validateSidFieldSubfieldCounts, getSubfieldValues} from '../../matching-utils';
32
+
33
+
34
+ export function bibSourceIds(record) {
35
+
36
+ /* Melinda's SRU-index melinda.sourceid includes source IDs from SID fields in Melinda records
37
+ SID-fields in Melinda have sf $c with local id and sf $b with a code for the local db:
38
+
39
+ SID__ $c 123457 $b helka
40
+ SID__ $c (ANDL100020)1077305 $b sata
41
+ SID__ $c VER999999 $ FI-KV
42
+ SID__ $c /10024/508126 $ REPO_THESEUS
43
+
44
+ In melinda.sourceid -index case is kept, sourceprefixes in brackets and hyphens are normalized away:
45
+ Note: slashes are not normalized away, but a SRU-search-string including slashes needs to be quoted
46
+
47
+ 1234567helka
48
+ 1077305sata
49
+ VER999999FIKV
50
+ /10024/508126REPO_THESEUS
51
+
52
+ Note: All Melinda records that have a matching records in a local db do NOT have SID for that local records,
53
+ existence of a SID field depends on how the record has been added to Melinda and how it has been handled
54
+ afterwards. SIDs are also not reliably maintained. A record might or might not have a SID for a local db
55
+ after the matching record is removed from the local db.
56
+
57
+ */
58
+
59
+
60
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:source-ids');
61
+ const debugData = debug.extend('data');
62
+ //const debugInfo = debug.extend('info');
63
+
64
+ debug(`Creating queries for sourceid's`);
65
+
66
+ const fSids = record.get('SID');
67
+ debugData(`SID-fields (${fSids.length}): ${JSON.stringify(fSids)}`);
68
+
69
+ return fSids.length > 0 ? toSidQueries(fSids) : [];
70
+
71
+ function toSidQueries(fSids) {
72
+ debug(`Creating actual queries for sourceid's`);
73
+
74
+ const sidStrings = getSidStrings(fSids);
75
+
76
+ if (sidStrings.length < 1) {
77
+ debug(`No identifiers found, no queries created.`);
78
+ return [];
79
+ }
80
+
81
+ const sidQueries = toQueries(sidStrings, 'melinda.sourceid');
82
+
83
+ return sidQueries;
84
+
85
+ function getSidStrings(fSids) {
86
+ debug(`Getting Sid strings from SID fields`);
87
+
88
+ // Map SID fields to valid sidStrings, filter out empty strings
89
+ const sidStrings = fSids.map(toSidString).filter(nonEmptySid => nonEmptySid);
90
+ return sidStrings;
91
+
92
+ function toSidString(field) {
93
+ debug(`Getting string from a field`);
94
+
95
+ return validateSidFieldSubfieldCounts(field) ? createSidString(field) : '';
96
+
97
+ function createSidString(field) {
98
+ debug(`Creating string from a field`);
99
+ const [sfC] = getSubfieldValues(field, 'c');
100
+ const [sfB] = getSubfieldValues(field, 'b');
101
+
102
+ const cleanedSfC = removeSourcePrefix(normalizeSidSubfieldValue(sfC));
103
+ const cleanedSfB = normalizeSidSubfieldValue(sfB);
104
+
105
+ debugData(`${JSON.stringify(sfC)} + ${JSON.stringify(sfB)}`);
106
+ return cleanedSfC.concat(cleanedSfB);
107
+ }
108
+
109
+ function removeSourcePrefix(subfieldValue) {
110
+ const sourcePrefixRegex = (/^(?<sourcePrefix>\([A-Za-z0-9-]+\))(?<id>.+)$/u);
111
+ const normalizedValue = subfieldValue.replace(sourcePrefixRegex, '$<id>');
112
+ debugData(`Normalized ${subfieldValue} to ${normalizedValue}`);
113
+ return normalizedValue;
114
+ }
115
+
116
+ function normalizeSidSubfieldValue(subfieldValue) {
117
+ debugData(`Normalizing ${subfieldValue}`);
118
+ const normalizeAwayRegex = (/[- ]/u);
119
+ return subfieldValue.replace(normalizeAwayRegex, '');
120
+ }
121
+
122
+ }
123
+ }
124
+ }
125
+ }
126
+
127
+ export function bibMelindaIds(record) {
128
+ // Melinda's SRU-index melinda.melindaid includes f001 controlnumbers and old Melinda-IDs from f035z's for all non-deleted Melinda-records
129
+
130
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibMelindaIds');
131
+ const debugData = debug.extend('data');
132
+ debug(`Creating queries for MelindaIds`);
133
+
134
+ // Note: Melinda-ID's for search queries are created just from records f035a's and f035z's
135
+ // Both (FI-MELINDA)- and FCC-prefixed forms are found
136
+ // f001 controlnumber is not currently included, even if record's f003 is FI-MELINDA
137
+ const melindaIds = getMelindaIdsF035(record);
138
+
139
+ debugData(`Unique identifiers (${melindaIds.length}): ${JSON.stringify(melindaIds)}`);
140
+
141
+ if (melindaIds.length < 1) {
142
+ debug(`No identifiers found, no queries created.`);
143
+ return [];
144
+ }
145
+
146
+ return toQueries(melindaIds, 'melinda.melindaid');
147
+ }
148
+
30
149
 
31
150
  // bibHostComponents returns host id from the first subfield $w of first field f773, see test-fixtures 04 and 05
32
151
  // bibHostComponents should search all 773 $ws for possible host id, but what should it do in case of multiple host ids?
@@ -54,12 +173,20 @@ export function bibHostComponents(record) {
54
173
  }
55
174
  }
56
175
 
176
+ // SRU search dc.title with a search phrase starting with ^ maps currently in Melinda to
177
+ // (probably) to *headings* index TIT
178
+ // Headings index TIT drops articles etc. from the start of the title according to the filing indicator
179
+ // Currently filing indicator is not implemented - if the title starts with an article and the Melinda
180
+ // record is correctly catalogued using a filing indicator -> dc.title search won't match
181
+
57
182
  export function bibTitle(record) {
58
183
  const title = getTitle();
59
184
 
60
185
  if (title) {
61
186
  const formatted = title
62
187
  .replace(/[^\w\s\p{Alphabetic}]/gu, '')
188
+ // Clean up concurrent spaces from fe. subfield changes
189
+ .replace(/ +/gu, ' ')
63
190
  .trim()
64
191
  .slice(0, 30)
65
192
  .trim();
@@ -77,7 +204,8 @@ export function bibTitle(record) {
77
204
  return field.subfields
78
205
  .filter(({code}) => ['a', 'b'].includes(code))
79
206
  .map(({value}) => value)
80
- .join('');
207
+ // In Melinda's index subfield separators are indexed as ' '
208
+ .join(' ');
81
209
  }
82
210
  return false;
83
211
  }
@@ -103,25 +231,7 @@ export function bibStandardIdentifiers(record) {
103
231
  return [];
104
232
  }
105
233
 
106
- return toQueries(uniqueIdentifiers);
107
-
108
- function toQueries(identifiers) {
109
- // Aleph supports only two queries with or -operator (This is not actually true)
110
- const pairs = toPairs(identifiers);
111
- const queries = pairs.map(([a, b]) => b ? `dc.identifier=${a} or dc.identifier=${b}` : `dc.identifier=${a}`);
112
-
113
- debugData(`Pairs (${pairs.length}): ${JSON.stringify(pairs)}`);
114
- debugData(`Queries (${queries.length}): ${JSON.stringify(queries)}`);
115
-
116
- return queries;
117
-
118
- function toPairs(array) {
119
- if (array.length === 0) {
120
- return [];
121
- }
122
- return [array.slice(0, 2)].concat(toPairs(array.slice(2), 2));
123
- }
124
- }
234
+ return toQueries(uniqueIdentifiers, 'dc.identifier');
125
235
 
126
236
  function toIdentifiers({tag, subfields}) {
127
237
  const issnIsbnReqExp = (/^[A-Za-z0-9-]+$/u);
@@ -32,20 +32,22 @@ import {expect} from 'chai';
32
32
  import {MarcRecord} from '@natlibfi/marc-record';
33
33
  import * as generators from './bib';
34
34
 
35
- generateTests({
36
- path: [__dirname, '..', '..', '..', 'test-fixtures', 'candidate-search', 'query-list', 'bib'],
37
- useMetadataFile: true,
38
- fixura: {
39
- reader: READERS.JSON
40
- },
41
- callback: ({type, inputRecord, expectedQuery, enabled = true}) => {
42
- const generate = generators[type];
43
- const record = new MarcRecord(inputRecord, {subfieldValues: false});
35
+ describe('candidate-search/query-list/bib/', () => {
36
+ generateTests({
37
+ path: [__dirname, '..', '..', '..', 'test-fixtures', 'candidate-search', 'query-list', 'bib'],
38
+ useMetadataFile: true,
39
+ fixura: {
40
+ reader: READERS.JSON
41
+ },
42
+ callback: ({type, inputRecord, expectedQuery, enabled = true}) => {
43
+ const generate = generators[type];
44
+ const record = new MarcRecord(inputRecord, {subfieldValues: false});
44
45
 
45
- if (!enabled) {
46
- return;
47
- }
46
+ if (!enabled) {
47
+ return;
48
+ }
48
49
 
49
- expect(generate(record)).to.eql(expectedQuery);
50
- }
50
+ expect(generate(record)).to.eql(expectedQuery);
51
+ }
52
+ });
51
53
  });
@@ -32,7 +32,9 @@ export const searchTypes = {
32
32
  bib: {
33
33
  standardIdentifiers: 'bibStandardIdentifiers',
34
34
  hostComponents: 'bibHostComponents',
35
- title: 'bibTitle'
35
+ title: 'bibTitle',
36
+ melindaId: 'bibMelindaIds',
37
+ sourceIds: 'bibSourceIds'
36
38
  }
37
39
  };
38
40
 
package/src/index.spec.js CHANGED
@@ -32,49 +32,56 @@ import generateTests from '@natlibfi/fixugen-http-client';
32
32
  import {MarcRecord} from '@natlibfi/marc-record';
33
33
  import createMatchInterface, {matchDetection} from '.';
34
34
 
35
- generateTests({
36
- callback,
37
- path: [__dirname, '..', 'test-fixtures', 'index'],
38
- recurse: false,
39
- fixura: {
40
- reader: READERS.JSON
41
- }
42
- });
35
+ describe('INDEX', () => {
36
+ generateTests({
37
+ callback,
38
+ path: [__dirname, '..', 'test-fixtures', 'index'],
39
+ recurse: false,
40
+ fixura: {
41
+ reader: READERS.JSON
42
+ }
43
+ });
43
44
 
44
- async function callback({getFixture, options}) {
45
- const record = new MarcRecord(getFixture('inputRecord.json'), {subfieldValues: false});
46
- const expectedMatches = getFixture('expectedMatches.json');
45
+ async function callback({getFixture, options, enabled = true}) {
47
46
 
48
- const match = createMatchInterface(formatOptions());
49
- const matches = await match(record);
47
+ if (!enabled) {
48
+ return;
49
+ }
50
50
 
51
- expect(formatResults()).to.eql(expectedMatches);
51
+ const record = new MarcRecord(getFixture('inputRecord.json'), {subfieldValues: false});
52
+ const expectedMatches = getFixture('expectedMatches.json');
52
53
 
53
- function formatOptions() {
54
- const contextFeatures = matchDetection.features[options.detection.strategy.type];
54
+ const match = createMatchInterface(formatOptions());
55
+ const matches = await match(record);
55
56
 
56
- return {
57
- ...options,
58
- search: {
59
- ...options.search,
60
- maxRecordsPerRequest: 1
61
- },
62
- detection: {
63
- ...options.detect,
64
- strategy: options.detection.strategy.features.map(v => contextFeatures[v]())
65
- },
66
- maxMatches: 2,
67
- maxCandidates: 1
68
- };
69
- }
57
+ expect(formatResults()).to.eql(expectedMatches);
70
58
 
71
- function formatResults() {
72
- return matches.map(({candidate, probability}) => ({
73
- probability,
74
- candidate: {
75
- id: candidate.id,
76
- record: candidate.record.toObject()
77
- }
78
- }));
59
+ function formatOptions() {
60
+ const contextFeatures = matchDetection.features[options.detection.strategy.type];
61
+
62
+ return {
63
+ ...options,
64
+ search: {
65
+ ...options.search,
66
+ maxRecordsPerRequest: 1
67
+ },
68
+ detection: {
69
+ ...options.detect,
70
+ strategy: options.detection.strategy.features.map(v => contextFeatures[v]())
71
+ },
72
+ maxMatches: 2,
73
+ maxCandidates: 1
74
+ };
75
+ }
76
+
77
+ function formatResults() {
78
+ return matches.map(({candidate, probability}) => ({
79
+ probability,
80
+ candidate: {
81
+ id: candidate.id,
82
+ record: candidate.record.toObject()
83
+ }
84
+ }));
85
+ }
79
86
  }
80
- }
87
+ });
@@ -0,0 +1,122 @@
1
+ /* eslint-disable max-statements */
2
+ /**
3
+ *
4
+ * @licstart The following is the entire license notice for the JavaScript code in this file.
5
+ *
6
+ * Melinda record matching modules for Javascript
7
+ *
8
+ * Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)
9
+ *
10
+ * This file is part of melinda-record-matching-js
11
+ *
12
+ * melinda-record-matching-js program is free software: you can redistribute it and/or modify
13
+ * it under the terms of the GNU Lesser General Public License as
14
+ * published by the Free Software Foundation, either version 3 of the
15
+ * License, or (at your option) any later version.
16
+ *
17
+ * melinda-record-matching-js is distributed in the hope that it will be useful,
18
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
20
+ * GNU Lesser General Public License for more details.
21
+ *
22
+ * You should have received a copy of the GNU Affero General Public License
23
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
24
+ *
25
+ * @licend The above is the entire license notice
26
+ * for the JavaScript code in this file.
27
+ *
28
+ */
29
+
30
+ import createDebugLogger from 'debug';
31
+ import {validateSidFieldSubfieldCounts, getSubfieldValues} from '../../../matching-utils';
32
+
33
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:all-source-ids');
34
+ const debugData = debug.extend('data');
35
+
36
+ /*
37
+
38
+ SID-fields in Melinda have sf $c with local id and sf $b with a code for the local db:
39
+
40
+ SID__ $c 123457 $b helka
41
+ SID__ $c (ANDL100020)1077305 $b sata
42
+ SID__ $c VER999999 $ FI-KV
43
+
44
+ allSourceIds matching feature is formatted as [{sourceDb: "helka", sourceId: "123457"}, {sourceDb: "sata", sourceId: "(ANDL100020)1077305"}, {sourceDb: "FI-KV", sourceId: "VER999999"}]
45
+ - no normalization / prefix deletions etc. in the feature
46
+
47
+ Note: All Melinda records that have a matching records in a local db do NOT have SID for that local records,
48
+ existence of a SID field depends on how the record has been added to Melinda and how it has been handled
49
+ afterwards. SIDs are also not reliably maintained. A record might or might not have a SID for a local db
50
+ after the matching record is removed from the local db.
51
+
52
+ Records with mismatching local ids for matching local db are a strong mismatch.
53
+ Records with matching local ids for matching local db are a very good match.
54
+
55
+ */
56
+
57
+ export default () => ({
58
+ name: 'All source IDs',
59
+ extract: record => {
60
+ debug(`Creating match detection features for all local source id's`);
61
+
62
+ const fSids = record.get('SID');
63
+ debugData(`SID-fields (${fSids.length}): ${JSON.stringify(fSids)}`);
64
+
65
+ const sidFeatures = getSidFeatures(fSids);
66
+ debugData(`SID-features (${sidFeatures.length}): ${JSON.stringify(sidFeatures)}`);
67
+
68
+ return sidFeatures;
69
+
70
+ function getSidFeatures(fSids) {
71
+ debug(`Getting Sid strings from SID fields`);
72
+
73
+ // Map SID fields to sidFeatures, filter out empty strings
74
+ const sidFeatures = fSids.map(toSidFeature).filter(nonEmptySid => nonEmptySid);
75
+ return sidFeatures;
76
+
77
+ function toSidFeature(field) {
78
+ debug(`Getting feature from a field`);
79
+
80
+ return validateSidFieldSubfieldCounts(field) ? createSidFeature(field) : '';
81
+
82
+ function createSidFeature(field) {
83
+ debug(`Creating feature from a field`);
84
+ const [sfC] = getSubfieldValues(field, 'c');
85
+ const [sfB] = getSubfieldValues(field, 'b');
86
+
87
+ debugData(`${JSON.stringify(sfC)} + ${JSON.stringify(sfB)}`);
88
+ debugData(`sourceDb: ${sfB}, sourceId: ${sfC}`);
89
+ return {'sourceDb': sfB, 'sourceId': sfC};
90
+ }
91
+ }
92
+
93
+ }
94
+
95
+ },
96
+ compare: (a, b) => {
97
+
98
+ debugData(`Comparing ${JSON.stringify(a)} and ${JSON.stringify(b)}`);
99
+
100
+ const matches = a.filter(sidA => b.some(sidB => sidA.sourceDb === sidB.sourceDb &&
101
+ sidA.sourceId === sidB.sourceId));
102
+ debugData(`Matches (${matches.length}): ${JSON.stringify(matches)}`);
103
+
104
+ const mismatches = a.filter(sidA => b.some(sidB => sidA.sourceDb === sidB.sourceDb &&
105
+ sidA.sourceId !== sidB.sourceId));
106
+ debugData(`Mismatches (${mismatches.length}): ${JSON.stringify(mismatches)}`);
107
+
108
+ // If there's at least one mismatching source ID from matching source db
109
+ if (mismatches.length > 0) {
110
+ return -1;
111
+ }
112
+
113
+ // If there's at least one matching source ID from matching source db
114
+ if (matches.length > 0) {
115
+ return 1;
116
+ }
117
+
118
+ // If there are no source IDs with matching sources
119
+ return 0;
120
+
121
+ }
122
+ });
@@ -36,3 +36,5 @@ export {default as recordType} from './record-type';
36
36
  export {default as publicationTime} from './publication-time';
37
37
  export {default as language} from './language';
38
38
  export {default as bibliographicLevel} from './bibliographic-level';
39
+ export {default as melindaId} from './melinda-id';
40
+ export {default as allSourceIds} from './all-source-ids';
@@ -32,30 +32,39 @@ import {expect} from 'chai';
32
32
  import {MarcRecord} from '@natlibfi/marc-record';
33
33
  import * as features from '.';
34
34
 
35
- generateTests({
36
- path: [__dirname, '..', '..', '..', '..', 'test-fixtures', 'match-detection', 'features', 'bib'],
37
- useMetadataFile: true,
38
- fixura: {
39
- reader: READERS.JSON
40
- },
41
- callback: ({feature, options, type, ...expectations}) => {
42
- if (type === 'extract') {
43
- const {expectedFeatures, inputRecord} = expectations;
44
- const record = new MarcRecord(inputRecord, {subfieldValues: false});
45
- const {extract} = features[feature](options);
46
-
47
- expect(extract(record)).to.eql(expectedFeatures);
48
- return;
49
- }
50
35
 
51
- if (type === 'compare') {
52
- const {featuresA, featuresB, expectedPoints} = expectations;
53
- const {compare} = features[feature](options);
36
+ describe('match-detection/features/bib/', () => {
37
+ generateTests({
38
+ path: [__dirname, '..', '..', '..', '..', 'test-fixtures', 'match-detection', 'features', 'bib'],
39
+ useMetadataFile: true,
40
+ fixura: {
41
+ reader: READERS.JSON
42
+ },
43
+ // eslint-disable-next-line max-statements
44
+ callback: ({enabled = true, feature, options, type, ...expectations}) => {
54
45
 
55
- expect(compare(featuresA, featuresB)).to.equal(expectedPoints);
56
- return;
57
- }
46
+ if (!enabled) {
47
+ return;
48
+ }
49
+
50
+ if (type === 'extract') {
51
+ const {expectedFeatures, inputRecord} = expectations;
52
+ const record = new MarcRecord(inputRecord, {subfieldValues: false});
53
+ const {extract} = features[feature](options);
54
+
55
+ expect(extract(record)).to.eql(expectedFeatures);
56
+ return;
57
+ }
58
58
 
59
- throw new Error(`Invalid type ${type}`);
60
- }
59
+ if (type === 'compare') {
60
+ const {featuresA, featuresB, expectedPoints} = expectations;
61
+ const {compare} = features[feature](options);
62
+
63
+ expect(compare(featuresA, featuresB)).to.equal(expectedPoints);
64
+ return;
65
+ }
66
+
67
+ throw new Error(`Invalid type ${type}`);
68
+ }
69
+ });
61
70
  });
@@ -0,0 +1,34 @@
1
+ /**
2
+ *
3
+ * @licstart The following is the entire license notice for the JavaScript code in this file.
4
+ *
5
+ * Melinda record matching modules for Javascript
6
+ *
7
+ * Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)
8
+ *
9
+ * This file is part of melinda-record-matching-js
10
+ *
11
+ * melinda-record-matching-js program is free software: you can redistribute it and/or modify
12
+ * it under the terms of the GNU Lesser General Public License as
13
+ * published by the Free Software Foundation, either version 3 of the
14
+ * License, or (at your option) any later version.
15
+ *
16
+ * melinda-record-matching-js is distributed in the hope that it will be useful,
17
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ * GNU Lesser General Public License for more details.
20
+ *
21
+ * You should have received a copy of the GNU Affero General Public License
22
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
+ *
24
+ * @licend The above is the entire license notice
25
+ * for the JavaScript code in this file.
26
+ *
27
+ */
28
+
29
+ import createInterface from './melinda-identifier-factory';
30
+
31
+ export default () => {
32
+ const {extract, compare} = createInterface();
33
+ return {extract, compare, name: 'melinda-id'};
34
+ };
@@ -0,0 +1,94 @@
1
+ /**
2
+ *
3
+ * @licstart The following is the entire license notice for the JavaScript code in this file.
4
+ *
5
+ * Melinda record matching modules for Javascript
6
+ *
7
+ * Copyright (C) 2020 University Of Helsinki (The National Library Of Finland)
8
+ *
9
+ * This file is part of melinda-record-matching-js
10
+ *
11
+ * melinda-record-matching-js program is free software: you can redistribute it and/or modify
12
+ * it under the terms of the GNU Lesser General Public License as
13
+ * published by the Free Software Foundation, either version 3 of the
14
+ * License, or (at your option) any later version.
15
+ *
16
+ * melinda-record-matching-js is distributed in the hope that it will be useful,
17
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19
+ * GNU Lesser General Public License for more details.
20
+ *
21
+ * You should have received a copy of the GNU Affero General Public License
22
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
23
+ *
24
+ * @licend The above is the entire license notice
25
+ * for the JavaScript code in this file.
26
+ *
27
+ */
28
+
29
+ import createDebugLogger from 'debug';
30
+ import {getMelindaIdsF035} from '../../../matching-utils';
31
+
32
+ // 003+001 FI-MELINDA <melinda-id>
33
+ // 035 $a (FI-MELINDA)<melinda-id>
34
+ // 035 $z (FI-MELINDA)<melinda-id>
35
+ // 035 $a FCC<melinda-id>
36
+ // 035 $z FCC<melinda-id>
37
+ // melinda-id = 001234567
38
+
39
+ export default () => {
40
+
41
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib/melinda-id');
42
+ const debugData = debug.extend('data');
43
+
44
+ return {extract, compare};
45
+
46
+ function extract(record) {
47
+
48
+ const isMelindaRecord = record.get('003').some(f003 => f003.value === 'FI-MELINDA');
49
+ const [f001] = record.get('001').map(field => field.value);
50
+ const f035MelindaIds = getMelindaIdsF035(record);
51
+
52
+ if (
53
+ isMelindaRecord === undefined &&
54
+ f001 === undefined &&
55
+ f035MelindaIds.length < 1) {
56
+
57
+ debug(`No Melinda-IDs found`);
58
+ return {};
59
+ }
60
+
61
+ return {isMelindaRecord, f001, f035MelindaIds};
62
+
63
+ }
64
+
65
+ // eslint-disable-next-line max-statements
66
+ function compare(a, b) {
67
+
68
+ if (a.isMelindaRecord && b.isMelindaRecord &&
69
+ a.f001 === b.f001) {
70
+ debugData(`Melinda record's A f001 ${a.f001} matches Melinda record's B f001 ${a.f001}`);
71
+ return 1;
72
+ }
73
+
74
+ if (a.isMelindaRecord && typeof b.f035MelindaIds !== 'undefined' &&
75
+ b.f035MelindaIds.some(id => id === a.f001)) {
76
+ debugData(`Melinda record's A f001 ${a.f001} matches record B f035 ${JSON.stringify(b.f035MelindaIds)}`);
77
+ return 1;
78
+ }
79
+
80
+ if (b.isMelindaRecord && typeof a.f035MelindaIds !== 'undefined' &&
81
+ a.f035MelindaIds.some(id => id === b.f001)) {
82
+ debugData(`Melinda record's B f001 ${b.f001} matches record A f035 ${JSON.stringify(a.f035MelindaIds)}`);
83
+ return 1;
84
+ }
85
+
86
+ if (typeof a.f035MelindaIds !== 'undefined' && typeof b.f035MelindaIds !== 'undefined' &&
87
+ a.f035MelindaIds.some(idA => b.f035MelindaIds.some(idB => idB === idA))) {
88
+ debugData(`Record A f035 ${JSON.stringify(a.f035MelindaIds)} matches record B f035 ${JSON.stringify(b.f035MelindaIds)}`);
89
+ return 1;
90
+ }
91
+ debug(`No matching Melinda-IDs.`);
92
+ return 0;
93
+ }
94
+ };