@natlibfi/melinda-record-matching 5.0.4 → 5.0.5

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 (47) hide show
  1. package/.github/workflows/melinda-node-tests-and-publish.yml +4 -4
  2. package/dist/candidate-search/query-list/bib.js +84 -67
  3. package/dist/candidate-search/query-list/bib.js.map +3 -3
  4. package/dist/candidate-search/query-list/component.js +94 -28
  5. package/dist/candidate-search/query-list/component.js.map +3 -3
  6. package/dist/cli.js +12 -4
  7. package/dist/cli.js.map +2 -2
  8. package/dist/match-detection/features/bib/f773.js +82 -0
  9. package/dist/match-detection/features/bib/f773.js.map +7 -0
  10. package/dist/match-detection/features/bib/index.js +1 -1
  11. package/dist/match-detection/features/bib/index.js.map +2 -2
  12. package/dist/match-detection/features/bib/index.test.js +1 -1
  13. package/dist/match-detection/features/bib/index.test.js.map +2 -2
  14. package/dist/match-detection/features/bib/isbn.js +3 -13
  15. package/dist/match-detection/features/bib/isbn.js.map +2 -2
  16. package/dist/match-detection/features/bib/issn.js +2 -16
  17. package/dist/match-detection/features/bib/issn.js.map +2 -2
  18. package/dist/match-detection/features/bib/language.js +12 -22
  19. package/dist/match-detection/features/bib/language.js.map +2 -2
  20. package/dist/match-detection/features/bib/publication-time-allow-cons-years.js +6 -2
  21. package/dist/match-detection/features/bib/publication-time-allow-cons-years.js.map +2 -2
  22. package/dist/match-detection/features/bib/publication-time.js +62 -16
  23. package/dist/match-detection/features/bib/publication-time.js.map +2 -2
  24. package/dist/match-detection/features/bib/title.js +76 -26
  25. package/dist/match-detection/features/bib/title.js.map +3 -3
  26. package/dist/match-detection/index.js +8 -3
  27. package/dist/match-detection/index.js.map +2 -2
  28. package/dist/matching-utils.js +14 -0
  29. package/dist/matching-utils.js.map +2 -2
  30. package/package.json +8 -12
  31. package/src/candidate-search/query-list/bib.js +100 -77
  32. package/src/candidate-search/query-list/component.js +100 -23
  33. package/src/cli.js +8 -4
  34. package/src/match-detection/features/bib/f773.js +118 -0
  35. package/src/match-detection/features/bib/index.js +1 -1
  36. package/src/match-detection/features/bib/index.test.js +1 -1
  37. package/src/match-detection/features/bib/isbn.js +3 -17
  38. package/src/match-detection/features/bib/issn.js +3 -32
  39. package/src/match-detection/features/bib/language.js +15 -26
  40. package/src/match-detection/features/bib/publication-time-allow-cons-years.js +7 -4
  41. package/src/match-detection/features/bib/publication-time.js +76 -30
  42. package/src/match-detection/features/bib/title.js +112 -34
  43. package/src/match-detection/index.js +9 -3
  44. package/src/matching-utils.js +17 -0
  45. package/dist/match-detection/features/bib/title-version-original.js +0 -37
  46. package/dist/match-detection/features/bib/title-version-original.js.map +0 -7
  47. package/src/match-detection/features/bib/title-version-original.js +0 -52
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../src/candidate-search/query-list/component.js"],
4
- "sourcesContent": ["import createDebugLogger from 'debug';\nimport {promisify} from 'util';\nimport {toQueries} from '../candidate-search-utils.js';\nimport {getSubfieldValues, testStringOrNumber, toMelindaIds} from '../../matching-utils.js';\n\nconst setTimeoutPromise = promisify(setTimeout); // eslint-disable-line\n\nexport function hostIdMelinda(record) {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:hostIdMelinda');\n const debugData = debug.extend('data'); // eslint-disable-line\n debug(`Creating query for the Melinda Id host`);\n\n const ids = hostId(record);\n if (ids.length > 0) {\n return toQueries(ids, 'melinda.partsofhost');\n }\n\n debug(`No valid Melinda Id host found`);\n return [];\n\n function hostId(record) {\n // Multi 773 handling\n const f773s = record.get(/^773$/u)\n .filter(f773 => f773.subfields.some(sub => sub.code === 'w' && (/\\(FI-MELINDA\\).*/ui).test(sub.value)));\n\n if (f773s.length === 0) {\n return false;\n }\n\n // Multi $w handling\n // $w (prefix)<id> handling\n // $w <id> & $w (prefix)<id> Match\n const melindaIds = f773s.map(f773 => toMelindaIds(f773, ['w'])).flat()\n .filter(value => testStringOrNumber(value)) // drop invalid values\n .filter((value, index, array) => array.indexOf(value) === index); // unique values;\n return melindaIds;\n }\n}\n\nexport async function hostIdOtherSource(record, client) {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:hostIdOtherSource');\n const debugData = debug.extend('data'); // eslint-disable-line\n debug(`Creating query for the Other source Id host`);\n\n const ids = getHostIds(record);\n const otherSources = getOtherSources(record);\n if (ids.length > 0 && otherSources.length > 0) {\n const otherSourceIds = ids.map(id => otherSources.map(source => `${id}${source}`)).flat();\n const melindaHostQueries = await handleSruCalls(otherSourceIds);\n debug(JSON.stringify(melindaHostQueries));\n\n return melindaHostQueries;\n }\n\n debug(`No valid other source hosts found`);\n return [];\n\n\n function getHostIds(record) {\n const f773s = record.get(/^773$/u)\n .filter(f773 => f773.subfields.some(sub => sub.code === 'w' && !(/\\(FI-MELINDA\\).*/ui).test(sub.value)));\n if (f773s.length === 0) {\n return false;\n }\n\n // Multi 773 handling\n const subfieldWs = f773s\n .map(f773 => {\n debugData(`f773: ${JSON.stringify(f773)}`);\n return getSubfieldValues(f773, 'w').flat();\n }).flat();\n\n // Multi $w handling\n // $w (prefix)<id> handling\n // $w <id> & $w (prefix)<id> Match\n const ids = subfieldWs.map(value => `${value}`.replace(/\\(FI-.*\\)/ui, '')) // remove prefixes\n .filter(value => testStringOrNumber(value)) // drop invalid values\n .filter((value, index, array) => array.indexOf(value) === index); // unique values\n\n return ids;\n }\n\n function getOtherSources(record) {\n const fSids = record.get('SID');\n return fSids.map(field => getSubfieldValues(field, 'b'));\n }\n\n async function handleSruCalls(otherSourceIds, ids = []) {\n const [otherSourceId, ...rest] = otherSourceIds;\n\n if (otherSourceId === undefined) {\n debug(`host ids: ${ids}`);\n const validIds = ids.filter(id => id);\n return toQueries(validIds, 'melinda.partsofhost');\n }\n\n const otherSourceHostQuery = await toQueries([otherSourceId], 'melinda.sourceid');\n const id = await new Promise((resolve, reject) => {\n debug(`Searching for hosts with query: ${otherSourceHostQuery}`);\n let recordId;\n\n client.searchRetrieve(otherSourceHostQuery)\n .on('error', err => {\n debug(`SRU error for query: ${otherSourceHostQuery}: ${err}`);\n reject(err);\n })\n .on('end', async () => {\n try {\n debug(`Searching for hosts: done`);\n await setTimeoutPromise(10);\n resolve(recordId);\n } catch (err) {\n debug(`Error caught on END`);\n reject(err);\n }\n })\n .on('record', record => {\n const [field] = record.get(/^001$/u);\n debug(field);\n recordId = field.value ? field.value : '';\n });\n });\n\n return handleSruCalls(rest, [...ids, id]);\n }\n}\n\n"],
5
- "mappings": "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",
6
- "names": ["record", "ids", "id"]
4
+ "sourcesContent": ["import createDebugLogger from 'debug';\nimport {promisify} from 'util';\nimport {toQueries} from '../candidate-search-utils.js';\nimport {getSubfieldValues, stringAfter, stringBefore, testStringOrNumber, toMelindaIds} from '../../matching-utils.js';\nimport {fieldToString, uniqArray} from '@natlibfi/marc-record-validators-melinda/dist/utils.js';\n\nconst setTimeoutPromise = promisify(setTimeout); // eslint-disable-line\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:component');\nconst debugData = debug.extend('data'); // eslint-disable-line\n\n\n\nexport function parse773g(field) {\n const g = field?.subfields?.find(sf => sf.code === 'g'); // NB! returns the first $g\n\n if (!g) {\n return {};\n }\n\n const value = normalizeValue(g.value);\n\n return {\n // NB! We are not currently interested in volume (vuosikerta)\n year: gToYear(value) || null,\n number: gToNumber(value) || null,\n pages: gToPages(value) || null // Might also be eg. Raita 5. However, returns only the number part\n }\n\n function normalizeValue(value) {\n return value.replace(/(?:\\. -|\\.)$/u, '');\n }\n\n function gToNumber(value) {\n // Allow up to 3-digit numbers\n const extract1 = value.replace(/^.*: ([1-9][0-9]?[0-9]), (?:p\\.|page|pp\\.|s\\.|Seite|sida|sidor|sivu).*$/ui, '$1');\n if (extract1 !== value) {\n return extract1;\n }\n return undefined;\n }\n\n function gToPages(value) {\n if (value.match(/^[0-9]+(?:-[0-9]+)?(?:, [0-9]+(?:-[0-9]+)?)*$/u)) {\n return value;\n }\n const numberPartOnly = value.replace(/^.*(?:p\\.|raidat|raita|s\\.|Seite|sivut?|pages?) /ui, '');\n if (numberPartOnly !== value) {\n return gToPages(numberPartOnly);\n }\n return undefined;\n }\n\n\n function gToYear(value) {\n debug(`EXTRACT YEAR FROM '${value}`);\n // extract year from within parentheses: $g vsk (yyyy) AND $g (YYYY)\n if (value.match(/^[1-9][0-9]?[0-9]? ?\\((?:20[012][0-9]|19[0-9][0-9])\\)/u) || value.match(/^\\((?:20[012][0-9]|19[0-9][0-9])\\)/u) ) {\n return stringBefore(stringAfter(value, '('), ')');\n }\n // If volume is missing, the year often seems to come without them parentheses:\n if (value.match(/^(?:20[012][0-9]|19[0-9][0-9]) :/u)) {\n return stringBefore(value, ' ');\n }\n // Seen in SB $g (vsk)year joulukuu\n if (value.match(/^\\([1-9][0-9]?\\) ?(?:20[012][0-9]|19[0-9][0-9])[^0-9]/)) {\n return value.replace(/^\\([0-9]+\\) ?/u, '').replace(/[^0-9].*$/u, '');\n }\n // Some magazines use DD.MM.YYYY\n if (value.match(/^[^0-9]*(?:[1-9]|[12][0-9]|30|31)\\.(?:1[012]|[1-9])\\. ?(?:20[012][0-9]|19[0-9][0-9])[^0-9]/u)) {\n return value.replace(/^[^0-9]*[0-9]+\\.[0-9]+\\. ?/u, '').replace(/[^0-9].*$/u, '');\n }\n\n return undefined;\n }\n}\n\nexport function extractPublicationYearFrom773(field) {\n const data = parse773g(field);\n debug(`Extracted year ${data.year} from f773$g ${fieldToString(field)}`);\n return data.year;\n}\n\nfunction isMelindaSubfieldW(subfield) {\n if (subfield.code !== 'w') {\n return false;\n }\n return (/^\\((?:FI-MELINDA|FIN01)\\)0[0-9]{8}$/ui).test(subfield.value);\n}\n\nfunction removeMelindaPrefixFromValue(value) {\n return value.replace(/^(?:\\(FI-MELINDA\\)|\\(FIN01\\))/, '');\n}\n\nexport function getHostMelindaFields(record) {\n return record.get(/^773$/u).filter(f => f.subfields.some(sf => isMelindaSubfieldW(sf)));\n}\n\nexport function extractHostMelindaIdsFromField(field) {\n return field.subfields.filter(sf => isMelindaSubfieldW(sf)).map(sf => removeMelindaPrefixFromValue(sf.value));\n}\n\nexport function getHostMelindaIds(record) {\n return uniqArray(getHostMelindaFields(record).map(f => extractHostMelindaIdsFromField(f)).flat());\n}\n\nexport function hostIdMelinda(record) { // old function with replaced code\n debug(`Creating query for the Melinda Id host`);\n const ids = getHostMelindaIds(record);\n if (ids.length === 0) {\n debug(`No valid Melinda Id host found`);\n return [];\n }\n return toQueries(getHostMelindaIds(record), 'melinda.partsofhost');\n}\n\nexport async function hostIdOtherSource(record, client) {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:hostIdOtherSource');\n const debugData = debug.extend('data'); // eslint-disable-line\n debug(`Creating query for the Other source Id host`);\n\n const ids = getHostIds(record);\n const otherSources = getOtherSources(record);\n if (ids.length > 0 && otherSources.length > 0) {\n const otherSourceIds = ids.map(id => otherSources.map(source => `${id}${source}`)).flat();\n const melindaHostQueries = await handleSruCalls(otherSourceIds);\n debug(JSON.stringify(melindaHostQueries));\n\n return melindaHostQueries;\n }\n\n debug(`No valid other source hosts found`);\n return [];\n\n\n function getHostIds(record) {\n const f773s = record.get(/^773$/u)\n .filter(f773 => f773.subfields.some(sub => sub.code === 'w' && !(/\\(FI-MELINDA\\).*/ui).test(sub.value)));\n if (f773s.length === 0) {\n return false;\n }\n\n // Multi 773 handling\n const subfieldWs = f773s\n .map(f773 => {\n debugData(`f773: ${JSON.stringify(f773)}`);\n return getSubfieldValues(f773, 'w').flat();\n }).flat();\n\n // Multi $w handling\n // $w (prefix)<id> handling\n // $w <id> & $w (prefix)<id> Match\n const ids = subfieldWs.map(value => `${value}`.replace(/\\(FI-.*\\)/ui, '')) // remove prefixes\n .filter(value => testStringOrNumber(value)) // drop invalid values\n .filter((value, index, array) => array.indexOf(value) === index); // unique values\n\n return ids;\n }\n\n function getOtherSources(record) {\n const fSids = record.get('SID');\n return fSids.map(field => getSubfieldValues(field, 'b'));\n }\n\n async function handleSruCalls(otherSourceIds, ids = []) {\n const [otherSourceId, ...rest] = otherSourceIds;\n\n if (otherSourceId === undefined) {\n debug(`host ids: ${ids}`);\n const validIds = ids.filter(id => id);\n return toQueries(validIds, 'melinda.partsofhost');\n }\n\n const otherSourceHostQuery = await toQueries([otherSourceId], 'melinda.sourceid');\n const id = await new Promise((resolve, reject) => {\n debug(`Searching for hosts with query: ${otherSourceHostQuery}`);\n let recordId;\n\n client.searchRetrieve(otherSourceHostQuery)\n .on('error', err => {\n debug(`SRU error for query: ${otherSourceHostQuery}: ${err}`);\n reject(err);\n })\n .on('end', async () => {\n try {\n debug(`Searching for hosts: done`);\n await setTimeoutPromise(10);\n resolve(recordId);\n } catch (err) {\n debug(`Error caught on END`);\n reject(err);\n }\n })\n .on('record', record => {\n const [field] = record.get(/^001$/u);\n debug(field);\n recordId = field.value ? field.value : '';\n });\n });\n\n return handleSruCalls(rest, [...ids, id]);\n }\n}\n\n"],
5
+ "mappings": "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",
6
+ "names": ["value", "debug", "debugData", "record", "ids", "id"]
7
7
  }
package/dist/cli.js CHANGED
@@ -49,7 +49,9 @@ async function cli() {
49
49
  if (["IDS"].includes(searchType2)) {
50
50
  return [
51
51
  matchDetection.features.bib.melindaId(),
52
- matchDetection.features.bib.allSourceIds()
52
+ matchDetection.features.bib.allSourceIds(),
53
+ matchDetection.features.bib.f773()
54
+ // Host data
53
55
  ];
54
56
  }
55
57
  if (["STANDARD_IDS"].includes(searchType2)) {
@@ -70,7 +72,9 @@ async function cli() {
70
72
  // normal publicationTime and a reprintPublication time
71
73
  matchDetection.features.bib.publicationTimeAllowConsYearsMulti(),
72
74
  matchDetection.features.bib.language(),
73
- matchDetection.features.bib.bibliographicLevel()
75
+ matchDetection.features.bib.bibliographicLevel(),
76
+ matchDetection.features.bib.f773()
77
+ // Host data
74
78
  ];
75
79
  }
76
80
  if (["COMPONENT"].includes(searchType2)) {
@@ -81,7 +85,9 @@ async function cli() {
81
85
  matchDetection.features.bib.title(),
82
86
  matchDetection.features.bib.language(),
83
87
  matchDetection.features.bib.authors(),
84
- matchDetection.features.bib.bibliographicLevel()
88
+ matchDetection.features.bib.bibliographicLevel(),
89
+ matchDetection.features.bib.f773()
90
+ // Host data
85
91
  ];
86
92
  }
87
93
  if (["CONTENT", "CONTENTALT"].includes(searchType2)) {
@@ -95,7 +101,9 @@ async function cli() {
95
101
  matchDetection.features.bib.recordType(),
96
102
  matchDetection.features.bib.publicationTime(),
97
103
  matchDetection.features.bib.language(),
98
- matchDetection.features.bib.bibliographicLevel()
104
+ matchDetection.features.bib.bibliographicLevel(),
105
+ matchDetection.features.bib.f773()
106
+ // Host data
99
107
  ];
100
108
  }
101
109
  throw new Error("Unsupported match validation package");
package/dist/cli.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/cli.js"],
4
- "sourcesContent": ["import fs from 'fs';\nimport yargs from 'yargs';\nimport createMatchOperator, {candidateSearch, matchDetection} from './index.js';\nimport createDebugLogger from 'debug';\nimport {MarcRecord} from '@natlibfi/marc-record';\n\ncli();\n\nasync function cli() {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:cli');\n const args = yargs(process.argv.slice(2))\n .scriptName('melinda-record-matching-js')\n .epilog('Copyright (C) 2022-2023 University Of Helsinki (The National Library Of Finland)')\n .usage('$0 <file> [options] and env variable info in README')\n .usage('Installed globally: $0 <file> [options] and env variable info in README')\n .usage('Not installed: npx $0 <file> [options] and env variable info in README')\n .usage('Build from source: node dist/index.js <file> [options] and env variable info in README')\n .showHelpOnFail(true)\n .example([['$ node /dist/cli.js record.json']])\n .env('MELINDA_RECORD_MATCH')\n .version()\n .positional('file', {type: 'string', describe: 'Json file of records to match'})\n .options({\n t: {type: 'string', default: 'IDS', alias: 'searchType', describe: 'IDS, STANDARD_IDS, COMPONENT, CONTENT or CONTENTALT'},\n m: {type: 'number', default: 1, alias: 'maxMatches', describe: ''},\n c: {type: 'number', default: 1000, alias: 'maxCandidates', describe: ''},\n s: {type: 'boolean', default: false, alias: 'returnStrategy', describe: ''},\n q: {type: 'boolean', default: false, alias: 'returnQuery', describe: ''},\n n: {type: 'boolean', default: false, alias: 'returnNonMatches', describe: ''}\n })\n .check((args) => {\n const [file] = args._;\n if (file === undefined) {\n throw new Error('No file argument given');\n }\n\n if (!fs.existsSync(file)) {\n throw new Error(`File ${file} does not exist`);\n }\n\n if (args.sruUrl === undefined) {\n throw new Error('Setup sru url');\n }\n\n if (!['IDS', 'STANDARD_IDS', 'COMPONENT', 'CONTENT', 'CONTENTALT'].includes(args.searchType)) {\n throw new Error('Invalid search type');\n }\n\n return true;\n })\n .parseSync();\n\n const [file] = args._;\n const {searchType} = args;\n debug(JSON.stringify(args));\n\n const detection = {\n threshold: 0.8999,\n strategy: generateStrategy(searchType)\n };\n\n const search = {\n url: args.sruUrl, searchSpec: generateSearchSpec(searchType)\n };\n\n const matchOperator = await createMatchOperator({detection, search, ...args});\n\n const fileRaw = fs.readFileSync(file, 'utf8');\n const record = new MarcRecord(JSON.parse(fileRaw), {subfieldValues: false});\n\n const result = await matchOperator({record});\n debug(JSON.stringify(result));\n\n\n function generateStrategy(searchType) {\n if (['IDS'].includes(searchType)) {\n return [\n matchDetection.features.bib.melindaId(),\n matchDetection.features.bib.allSourceIds()\n ];\n }\n\n // We could have differing strategy for STANDARD_IDS\n // Let's not run title in strategy when we found the candidates through standard_ids search\n\n if (['STANDARD_IDS'].includes(searchType)) {\n return [\n matchDetection.features.bib.hostComponent(),\n matchDetection.features.bib.isbn(),\n matchDetection.features.bib.issn(),\n matchDetection.features.bib.otherStandardIdentifier(),\n // Let's not use the same title matchDetection here\n //matchDetection.features.bib.title(),\n matchDetection.features.bib.authors(),\n // We probably should have some leeway here for notated music as BK etc.\n matchDetection.features.bib.recordType(),\n // Use publicationTimeAllowConsYearsMulti to\n // - ignore one year differences in publicationTime\n // - extract publicationTimes from f008, f26x and reprint notes in f500\n // - do not substract points for mismatching (normal) publicationTime, if there's a match between\n // normal publicationTime and a reprintPublication time\n matchDetection.features.bib.publicationTimeAllowConsYearsMulti(),\n matchDetection.features.bib.language(),\n matchDetection.features.bib.bibliographicLevel()\n ];\n }\n\n if (['COMPONENT'].includes(searchType)) {\n return [\n matchDetection.features.bib.hostComponent(),\n matchDetection.features.bib.otherStandardIdentifier(),\n matchDetection.features.bib.recordType(),\n matchDetection.features.bib.title(),\n matchDetection.features.bib.language(),\n matchDetection.features.bib.authors(),\n matchDetection.features.bib.bibliographicLevel()\n ];\n }\n\n if (['CONTENT', 'CONTENTALT'].includes(searchType)) {\n return [\n matchDetection.features.bib.hostComponent(),\n matchDetection.features.bib.isbn(),\n matchDetection.features.bib.issn(),\n matchDetection.features.bib.otherStandardIdentifier(),\n matchDetection.features.bib.title(),\n matchDetection.features.bib.authors(),\n matchDetection.features.bib.recordType(),\n matchDetection.features.bib.publicationTime(),\n matchDetection.features.bib.language(),\n matchDetection.features.bib.bibliographicLevel()\n ];\n }\n\n throw new Error('Unsupported match validation package');\n }\n\n\n function generateSearchSpec(searchType) {\n if (['IDS'].includes(searchType)) {\n return [\n candidateSearch.searchTypes.bib.melindaId,\n candidateSearch.searchTypes.bib.sourceIds\n ];\n }\n\n if (['STANDARD_IDS'].includes(searchType)) {\n return [candidateSearch.searchTypes.bib.standardIdentifiers];\n }\n\n if (['COMPONENT'].includes(searchType)) {\n return [\n //candidateSearch.searchTypes.bib.sourceIds,\n candidateSearch.searchTypes.component.hostIdMelinda,\n candidateSearch.searchTypes.component.hostIdOtherSource\n ];\n }\n\n if (['CONTENT'].includes(searchType)) {\n return [\n candidateSearch.searchTypes.bib.hostComponents,\n //candidateSearch.searchTypes.bib.titleAuthor,\n candidateSearch.searchTypes.bib.title\n ];\n }\n if (['CONTENTALT'].includes(searchType)) {\n return [\n candidateSearch.searchTypes.bib.hostComponents,\n // titleAuthorYearAlternates searches for matchCandidates\n // with alternate queries, starting from more tight searches\n candidateSearch.searchTypes.bib.titleAuthorYearAlternates\n ];\n }\n\n throw new Error('Unsupported match validation package');\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import fs from 'fs';\nimport yargs from 'yargs';\nimport createMatchOperator, {candidateSearch, matchDetection} from './index.js';\nimport createDebugLogger from 'debug';\nimport {MarcRecord} from '@natlibfi/marc-record';\n\ncli();\n\nasync function cli() {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:cli');\n const args = yargs(process.argv.slice(2))\n .scriptName('melinda-record-matching-js')\n .epilog('Copyright (C) 2022-2023 University Of Helsinki (The National Library Of Finland)')\n .usage('$0 <file> [options] and env variable info in README')\n .usage('Installed globally: $0 <file> [options] and env variable info in README')\n .usage('Not installed: npx $0 <file> [options] and env variable info in README')\n .usage('Build from source: node dist/index.js <file> [options] and env variable info in README')\n .showHelpOnFail(true)\n .example([['$ node /dist/cli.js record.json']])\n .env('MELINDA_RECORD_MATCH')\n .version()\n .positional('file', {type: 'string', describe: 'Json file of records to match'})\n .options({\n t: {type: 'string', default: 'IDS', alias: 'searchType', describe: 'IDS, STANDARD_IDS, COMPONENT, CONTENT or CONTENTALT'},\n m: {type: 'number', default: 1, alias: 'maxMatches', describe: ''},\n c: {type: 'number', default: 1000, alias: 'maxCandidates', describe: ''},\n s: {type: 'boolean', default: false, alias: 'returnStrategy', describe: ''},\n q: {type: 'boolean', default: false, alias: 'returnQuery', describe: ''},\n n: {type: 'boolean', default: false, alias: 'returnNonMatches', describe: ''}\n })\n .check((args) => {\n const [file] = args._;\n if (file === undefined) {\n throw new Error('No file argument given');\n }\n\n if (!fs.existsSync(file)) {\n throw new Error(`File ${file} does not exist`);\n }\n\n if (args.sruUrl === undefined) {\n throw new Error('Setup sru url');\n }\n\n if (!['IDS', 'STANDARD_IDS', 'COMPONENT', 'CONTENT', 'CONTENTALT'].includes(args.searchType)) {\n throw new Error('Invalid search type');\n }\n\n return true;\n })\n .parseSync();\n\n const [file] = args._;\n const {searchType} = args;\n debug(JSON.stringify(args));\n\n const detection = {\n threshold: 0.8999,\n strategy: generateStrategy(searchType)\n };\n\n const search = {\n url: args.sruUrl, searchSpec: generateSearchSpec(searchType)\n };\n\n const matchOperator = await createMatchOperator({detection, search, ...args});\n\n const fileRaw = fs.readFileSync(file, 'utf8');\n const record = new MarcRecord(JSON.parse(fileRaw), {subfieldValues: false});\n\n const result = await matchOperator({record});\n debug(JSON.stringify(result));\n\n\n function generateStrategy(searchType) {\n if (['IDS'].includes(searchType)) {\n return [\n matchDetection.features.bib.melindaId(),\n matchDetection.features.bib.allSourceIds(),\n matchDetection.features.bib.f773() // Host data\n ];\n }\n\n // We could have differing strategy for STANDARD_IDS\n // Let's not run title in strategy when we found the candidates through standard_ids search\n\n if (['STANDARD_IDS'].includes(searchType)) {\n return [\n matchDetection.features.bib.hostComponent(),\n matchDetection.features.bib.isbn(),\n matchDetection.features.bib.issn(),\n matchDetection.features.bib.otherStandardIdentifier(),\n // Let's not use the same title matchDetection here\n //matchDetection.features.bib.title(),\n matchDetection.features.bib.authors(),\n // We probably should have some leeway here for notated music as BK etc.\n matchDetection.features.bib.recordType(),\n // Use publicationTimeAllowConsYearsMulti to\n // - ignore one year differences in publicationTime\n // - extract publicationTimes from f008, f26x and reprint notes in f500\n // - do not substract points for mismatching (normal) publicationTime, if there's a match between\n // normal publicationTime and a reprintPublication time\n matchDetection.features.bib.publicationTimeAllowConsYearsMulti(),\n matchDetection.features.bib.language(),\n matchDetection.features.bib.bibliographicLevel(),\n matchDetection.features.bib.f773() // Host data\n ];\n }\n\n if (['COMPONENT'].includes(searchType)) {\n return [\n matchDetection.features.bib.hostComponent(),\n matchDetection.features.bib.otherStandardIdentifier(),\n matchDetection.features.bib.recordType(),\n matchDetection.features.bib.title(),\n matchDetection.features.bib.language(),\n matchDetection.features.bib.authors(),\n matchDetection.features.bib.bibliographicLevel(),\n matchDetection.features.bib.f773() // Host data\n ];\n }\n\n if (['CONTENT', 'CONTENTALT'].includes(searchType)) {\n return [\n matchDetection.features.bib.hostComponent(),\n matchDetection.features.bib.isbn(),\n matchDetection.features.bib.issn(),\n matchDetection.features.bib.otherStandardIdentifier(),\n matchDetection.features.bib.title(),\n matchDetection.features.bib.authors(),\n matchDetection.features.bib.recordType(),\n matchDetection.features.bib.publicationTime(),\n matchDetection.features.bib.language(),\n matchDetection.features.bib.bibliographicLevel(),\n matchDetection.features.bib.f773() // Host data\n ];\n }\n\n throw new Error('Unsupported match validation package');\n }\n\n\n function generateSearchSpec(searchType) {\n if (['IDS'].includes(searchType)) {\n return [\n candidateSearch.searchTypes.bib.melindaId,\n candidateSearch.searchTypes.bib.sourceIds\n ];\n }\n\n if (['STANDARD_IDS'].includes(searchType)) {\n return [candidateSearch.searchTypes.bib.standardIdentifiers];\n }\n\n if (['COMPONENT'].includes(searchType)) {\n return [\n //candidateSearch.searchTypes.bib.sourceIds,\n candidateSearch.searchTypes.component.hostIdMelinda,\n candidateSearch.searchTypes.component.hostIdOtherSource\n ];\n }\n\n if (['CONTENT'].includes(searchType)) {\n return [\n candidateSearch.searchTypes.bib.hostComponents,\n //candidateSearch.searchTypes.bib.titleAuthor,\n candidateSearch.searchTypes.bib.title\n ];\n }\n if (['CONTENTALT'].includes(searchType)) {\n return [\n candidateSearch.searchTypes.bib.hostComponents,\n // titleAuthorYearAlternates searches for matchCandidates\n // with alternate queries, starting from more tight searches\n candidateSearch.searchTypes.bib.titleAuthorYearAlternates\n ];\n }\n\n throw new Error('Unsupported match validation package');\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["args", "file", "searchType"]
7
7
  }
@@ -0,0 +1,82 @@
1
+ import createDebugLogger from "debug";
2
+ import { isComponentRecord } from "@natlibfi/melinda-commons";
3
+ import { uniqArray } from "./issn.js";
4
+ import { parse773g } from "../../../candidate-search/query-list/component.js";
5
+ const debug = createDebugLogger("@natlibfi/melinda-record-matching:match-detection:features:issn");
6
+ const debugData = debug.extend("data");
7
+ const MAX_IDENTIFIER = 0.1;
8
+ const MAX_G = 0.5;
9
+ export default () => ({
10
+ name: "f773 ",
11
+ extract: ({
12
+ record
13
+ /*, recordExternal*/
14
+ }) => {
15
+ if (!isComponentRecord(record, false, [])) {
16
+ return [];
17
+ }
18
+ const f773s = record.get(/^773$/u);
19
+ const values = f773s.map((f) => f.subfields).flat().filter((sf) => ["w", "o", "x", "z"].includes(sf.code)).map((sf) => normalizeValue(sf.value));
20
+ const gData = parse773g(f773s[0]);
21
+ return [uniqArray(values), gData];
22
+ function normalizeValue(value) {
23
+ return value.replace(/(?:\. -|\.)$/u, "");
24
+ }
25
+ },
26
+ compare: (aa, bb) => {
27
+ const [aIdentifiers, ag] = aa;
28
+ const [bIdentifiers, bg] = bb;
29
+ const identifierScore = scoreIdentifiers();
30
+ const gScore = scoreG();
31
+ if (identifierScore === MAX_IDENTIFIER && gScore === MAX_G) {
32
+ return 1;
33
+ }
34
+ return identifierScore + gScore;
35
+ function scoreG() {
36
+ if (ag.number && ag.number === bg.number && ag.pages && ag.pages === bg.pages && ag.year && ag.year === bg.year) {
37
+ return MAX_G;
38
+ }
39
+ return scoreYear() + scoreNumber() + scorePages();
40
+ }
41
+ function scoreNumber() {
42
+ if (!ag.number || !bg.number) {
43
+ return 0;
44
+ }
45
+ if (ag.number === bg.number) {
46
+ return 0.05;
47
+ }
48
+ return -0.02;
49
+ }
50
+ function scorePages() {
51
+ if (!ag.pages || !bg.pages) {
52
+ return 0;
53
+ }
54
+ if (ag.pages === bg.pages) {
55
+ return 0.1;
56
+ }
57
+ return -0.05;
58
+ }
59
+ function scoreYear() {
60
+ if (!ag.year || !bg.year) {
61
+ return 0;
62
+ }
63
+ if (ag.year === bg.year) {
64
+ return 0.02;
65
+ }
66
+ return -0.02;
67
+ }
68
+ function scoreIdentifiers() {
69
+ debugData(`Comparing ISSN sets ${JSON.stringify(aIdentifiers)} and ${JSON.stringify(bIdentifiers)}`);
70
+ if (aIdentifiers.length === 0 || bIdentifiers.length === 0) {
71
+ return 0;
72
+ }
73
+ const firstSharedIdentifier = aIdentifiers.find((val) => bIdentifiers.includes(val));
74
+ if (firstSharedIdentifier) {
75
+ debug(` Shared identifier found: '${firstSharedIdentifier}'`);
76
+ return MAX_IDENTIFIER;
77
+ }
78
+ return -0.5;
79
+ }
80
+ }
81
+ });
82
+ //# sourceMappingURL=f773.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/match-detection/features/bib/f773.js"],
4
+ "sourcesContent": ["// NB! This checks identifiers ($w=ID, $x=ISSN, $z=ISBN and $o=others) to check whether the host is the same.\n// BB! $g is not checked *yet*. It could very strongly indicate that the records are the same... (I doubt $q could be useful here)\n// Rationale:\n// 773$w indicates sameness. However, non-Melinda records probably refer to non-melinda hosts.\n// Thus check other identifiers subfields ($x, $z and $o as well)\n//\n\n\nimport createDebugLogger from 'debug';\n\nimport {isComponentRecord} from '@natlibfi/melinda-commons';\nimport {uniqArray} from './issn.js';\nimport {parse773g} from '../../../candidate-search/query-list/component.js';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:issn');\nconst debugData = debug.extend('data');\n\nconst MAX_IDENTIFIER = 0.1; // This should be pretty low: it only says something about the host\nconst MAX_G = 0.5; // $g is about the comp itself, so score can be high here\n\nexport default () => ({\n name: 'f773 ',\n extract: ({record/*, recordExternal*/}) => {\n //const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n\n if (!isComponentRecord(record, false, [])) {\n return [];\n }\n const f773s = record.get(/^773$/u); // Identifier extract handles multiple 773...\n\n // I think it's ok, if, say, $o and $x values match, so I'm not keeping the subfield codes\n const values = f773s.map(f => f.subfields)\n .flat()\n .filter(sf => ['w', 'o', 'x', 'z'].includes(sf.code))\n .map(sf => normalizeValue(sf.value));\n\n // $g: only on $g is supported\n const gData = parse773g(f773s[0]);\n\n return [uniqArray(values), gData];\n\n function normalizeValue(value) {\n return value.replace(/(?:\\. -|\\.)$/u, '');\n }\n\n\n },\n\n compare: (aa, bb) => {\n const [aIdentifiers, ag] = aa;\n const [bIdentifiers, bg] = bb;\n\n const identifierScore = scoreIdentifiers();\n const gScore = scoreG();\n\n if (identifierScore === MAX_IDENTIFIER && gScore === MAX_G) { // Pretty impressive hit, even if title matches not\n return 1.0;\n }\n\n return identifierScore + gScore;\n\n function scoreG() {\n // NB! $g contents are very noise, so wrong values may be extracted. Thus do not overpenalize.\n\n // All exist match: things must be pretty good:\n if (ag.number && ag.number === bg.number && ag.pages && ag.pages === bg.pages && ag.year && ag.year === bg.year) {\n return MAX_G;\n }\n // Not comparing volume. It correlates with year.\n return scoreYear() + scoreNumber() + scorePages();\n }\n\n function scoreNumber() {\n if (!ag.number || !bg.number) {\n return 0.0;\n }\n if (ag.number === bg.number) {\n return 0.05;\n }\n return -0.02;\n }\n\n function scorePages() {\n if (!ag.pages || !bg.pages) {\n return 0.0;\n }\n if (ag.pages === bg.pages) { // If pages match, things must be pretty good\n return 0.1;\n }\n return -0.05;\n }\n\n function scoreYear() { // publication-time.js also uses this, so don't score heavily here\n if (!ag.year || !bg.year) {\n return 0.0;\n }\n if (ag.year === bg.year) {\n return 0.02;\n }\n return -0.02;\n }\n\n function scoreIdentifiers() {\n debugData(`Comparing ISSN sets ${JSON.stringify(aIdentifiers)} and ${JSON.stringify(bIdentifiers)}`);\n if (aIdentifiers.length === 0 || bIdentifiers.length === 0) {\n // No data for decision\n return 0;\n }\n const firstSharedIdentifier = aIdentifiers.find(val => bIdentifiers.includes(val));\n if (firstSharedIdentifier) {\n debug(`\\t Shared identifier found: '${firstSharedIdentifier}'`);\n return MAX_IDENTIFIER;\n }\n return -0.5;\n }\n }\n});\n\n"],
5
+ "mappings": "AAQA,OAAO,uBAAuB;AAE9B,SAAQ,yBAAwB;AAChC,SAAQ,iBAAgB;AACxB,SAAQ,iBAAgB;AAExB,MAAM,QAAQ,kBAAkB,iEAAiE;AACjG,MAAM,YAAY,MAAM,OAAO,MAAM;AAErC,MAAM,iBAAiB;AACvB,MAAM,QAAQ;AAEd,eAAe,OAAO;AAAA,EACpB,MAAM;AAAA,EACN,SAAS,CAAC;AAAA,IAAC;AAAA;AAAA,EAA0B,MAAM;AAGzC,QAAI,CAAC,kBAAkB,QAAQ,OAAO,CAAC,CAAC,GAAG;AACzC,aAAO,CAAC;AAAA,IACV;AACA,UAAM,QAAQ,OAAO,IAAI,QAAQ;AAGjC,UAAM,SAAS,MAAM,IAAI,OAAK,EAAE,SAAS,EACtC,KAAK,EACL,OAAO,QAAM,CAAC,KAAK,KAAK,KAAK,GAAG,EAAE,SAAS,GAAG,IAAI,CAAC,EACnD,IAAI,QAAO,eAAe,GAAG,KAAK,CAAC;AAGtC,UAAM,QAAQ,UAAU,MAAM,CAAC,CAAC;AAEhC,WAAO,CAAC,UAAU,MAAM,GAAG,KAAK;AAEhC,aAAS,eAAe,OAAO;AAC7B,aAAO,MAAM,QAAQ,iBAAiB,EAAE;AAAA,IAC1C;AAAA,EAGF;AAAA,EAEA,SAAS,CAAC,IAAI,OAAO;AACnB,UAAM,CAAC,cAAc,EAAE,IAAI;AAC3B,UAAM,CAAC,cAAc,EAAE,IAAI;AAE3B,UAAM,kBAAkB,iBAAiB;AACzC,UAAM,SAAS,OAAO;AAEtB,QAAI,oBAAoB,kBAAkB,WAAW,OAAO;AAC1D,aAAO;AAAA,IACT;AAEA,WAAO,kBAAkB;AAEzB,aAAS,SAAS;AAIhB,UAAI,GAAG,UAAU,GAAG,WAAW,GAAG,UAAU,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM;AAC/G,eAAO;AAAA,MACT;AAEA,aAAO,UAAU,IAAI,YAAY,IAAI,WAAW;AAAA,IAClD;AAEA,aAAS,cAAc;AACrB,UAAI,CAAC,GAAG,UAAU,CAAC,GAAG,QAAQ;AAC5B,eAAO;AAAA,MACT;AACA,UAAI,GAAG,WAAW,GAAG,QAAQ;AAC3B,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAEA,aAAS,aAAa;AACpB,UAAI,CAAC,GAAG,SAAS,CAAC,GAAG,OAAO;AAC1B,eAAO;AAAA,MACT;AACA,UAAI,GAAG,UAAU,GAAG,OAAO;AACzB,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAEA,aAAS,YAAY;AACnB,UAAI,CAAC,GAAG,QAAQ,CAAC,GAAG,MAAM;AACxB,eAAO;AAAA,MACT;AACA,UAAI,GAAG,SAAS,GAAG,MAAM;AACvB,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAEA,aAAS,mBAAmB;AAC1B,gBAAU,uBAAuB,KAAK,UAAU,YAAY,CAAC,QAAQ,KAAK,UAAU,YAAY,CAAC,EAAE;AACnG,UAAI,aAAa,WAAW,KAAK,aAAa,WAAW,GAAG;AAE1D,eAAO;AAAA,MACT;AACA,YAAM,wBAAwB,aAAa,KAAK,SAAO,aAAa,SAAS,GAAG,CAAC;AACjF,UAAI,uBAAuB;AACzB,cAAM,+BAAgC,qBAAqB,GAAG;AAC9D,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -3,7 +3,6 @@ export { default as isbn } from "./isbn.js";
3
3
  export { default as issn } from "./issn.js";
4
4
  export { default as otherStandardIdentifier } from "./other-standard-identifier.js";
5
5
  export { default as title } from "./title.js";
6
- export { default as titleVersionOriginal } from "./title-version-original.js";
7
6
  export { default as authors } from "./authors.js";
8
7
  export { default as recordType } from "./record-type.js";
9
8
  export { default as publicationTime } from "./publication-time.js";
@@ -14,4 +13,5 @@ export { default as bibliographicLevel } from "./bibliographic-level.js";
14
13
  export { default as melindaId } from "./melinda-id.js";
15
14
  export { default as allSourceIds } from "./all-source-ids.js";
16
15
  export { default as mediaType } from "./media-type.js";
16
+ export { default as f773 } from "./f773.js";
17
17
  //# sourceMappingURL=index.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/match-detection/features/bib/index.js"],
4
- "sourcesContent": ["\nexport {default as hostComponent} from './host-component.js';\nexport {default as isbn} from './isbn.js';\nexport {default as issn} from './issn.js';\nexport {default as otherStandardIdentifier} from './other-standard-identifier.js';\nexport {default as title} from './title.js';\nexport {default as titleVersionOriginal} from './title-version-original.js';\nexport {default as authors} from './authors.js';\nexport {default as recordType} from './record-type.js';\nexport {default as publicationTime} from './publication-time.js';\nexport {default as publicationTimeAllowConsYears} from './publication-time-allow-cons-years.js';\nexport {default as publicationTimeAllowConsYearsMulti} from './publication-time-allow-cons-years-multi.js';\nexport {default as language} from './language.js';\nexport {default as bibliographicLevel} from './bibliographic-level.js';\nexport {default as melindaId} from './melinda-id.js';\nexport {default as allSourceIds} from './all-source-ids.js';\nexport {default as mediaType} from './media-type.js';\n"],
5
- "mappings": "AACA,SAAQ,WAAW,qBAAoB;AACvC,SAAQ,WAAW,YAAW;AAC9B,SAAQ,WAAW,YAAW;AAC9B,SAAQ,WAAW,+BAA8B;AACjD,SAAQ,WAAW,aAAY;AAC/B,SAAQ,WAAW,4BAA2B;AAC9C,SAAQ,WAAW,eAAc;AACjC,SAAQ,WAAW,kBAAiB;AACpC,SAAQ,WAAW,uBAAsB;AACzC,SAAQ,WAAW,qCAAoC;AACvD,SAAQ,WAAW,0CAAyC;AAC5D,SAAQ,WAAW,gBAAe;AAClC,SAAQ,WAAW,0BAAyB;AAC5C,SAAQ,WAAW,iBAAgB;AACnC,SAAQ,WAAW,oBAAmB;AACtC,SAAQ,WAAW,iBAAgB;",
4
+ "sourcesContent": ["\nexport {default as hostComponent} from './host-component.js';\nexport {default as isbn} from './isbn.js';\nexport {default as issn} from './issn.js';\nexport {default as otherStandardIdentifier} from './other-standard-identifier.js';\nexport {default as title} from './title.js';\nexport {default as authors} from './authors.js';\nexport {default as recordType} from './record-type.js';\nexport {default as publicationTime} from './publication-time.js';\nexport {default as publicationTimeAllowConsYears} from './publication-time-allow-cons-years.js';\nexport {default as publicationTimeAllowConsYearsMulti} from './publication-time-allow-cons-years-multi.js';\nexport {default as language} from './language.js';\nexport {default as bibliographicLevel} from './bibliographic-level.js';\nexport {default as melindaId} from './melinda-id.js';\nexport {default as allSourceIds} from './all-source-ids.js';\nexport {default as mediaType} from './media-type.js';\nexport {default as f773} from './f773.js';\n"],
5
+ "mappings": "AACA,SAAQ,WAAW,qBAAoB;AACvC,SAAQ,WAAW,YAAW;AAC9B,SAAQ,WAAW,YAAW;AAC9B,SAAQ,WAAW,+BAA8B;AACjD,SAAQ,WAAW,aAAY;AAC/B,SAAQ,WAAW,eAAc;AACjC,SAAQ,WAAW,kBAAiB;AACpC,SAAQ,WAAW,uBAAsB;AACzC,SAAQ,WAAW,qCAAoC;AACvD,SAAQ,WAAW,0CAAyC;AAC5D,SAAQ,WAAW,gBAAe;AAClC,SAAQ,WAAW,0BAAyB;AAC5C,SAAQ,WAAW,iBAAgB;AACnC,SAAQ,WAAW,oBAAmB;AACtC,SAAQ,WAAW,iBAAgB;AACnC,SAAQ,WAAW,YAAW;",
6
6
  "names": []
7
7
  }
@@ -30,7 +30,7 @@ describe("match-detection/features/bib/", () => {
30
30
  if (type === "compare") {
31
31
  const { featuresA, featuresB, expectedPoints } = expectations;
32
32
  const { compare } = features[feature](options);
33
- assert.equal(compare(featuresA, featuresB), expectedPoints);
33
+ assert.equal(Math.round(compare(featuresA, featuresB) * 100) / 100, expectedPoints);
34
34
  return;
35
35
  }
36
36
  throw new Error(`Invalid type ${type}`);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/match-detection/features/bib/index.test.js"],
4
- "sourcesContent": ["\nimport assert from 'node:assert';\nimport {describe} from 'node:test';\nimport createDebugLogger from 'debug';\nimport generateTests from '@natlibfi/fixugen';\nimport {READERS} from '@natlibfi/fixura';\nimport {MarcRecord} from '@natlibfi/marc-record';\nimport * as features from './index.js';\n\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib:test');\nconst debugData = debug.extend('data');\n\n\ndescribe('match-detection/features/bib/', () => {\n generateTests({\n path: [import.meta.dirname, '..', '..', '..', '..', 'test-fixtures', 'match-detection', 'features', 'bib'],\n useMetadataFile: true,\n fixura: {\n reader: READERS.JSON\n },\n\n callback: ({enabled = true, feature, options, type, ...expectations}) => {\n\n if (!enabled) {\n return;\n }\n\n debug(`Testing: ${feature} ${type}`);\n\n if (type === 'extract') {\n const {expectedFeatures, inputRecord} = expectations;\n const record = new MarcRecord(inputRecord, {subfieldValues: false});\n debugData(`Record: ${record}`);\n const {extract} = features[feature](options);\n\n assert.deepStrictEqual(extract({record}), expectedFeatures);\n return;\n }\n\n if (type === 'compare') {\n const {featuresA, featuresB, expectedPoints} = expectations;\n const {compare} = features[feature](options);\n\n assert.equal(compare(featuresA, featuresB), expectedPoints);\n return;\n }\n\n throw new Error(`Invalid type ${type}`);\n }\n });\n});\n"],
5
- "mappings": "AACA,OAAO,YAAY;AACnB,SAAQ,gBAAe;AACvB,OAAO,uBAAuB;AAC9B,OAAO,mBAAmB;AAC1B,SAAQ,eAAc;AACtB,SAAQ,kBAAiB;AACzB,YAAY,cAAc;AAG1B,MAAM,QAAQ,kBAAkB,qEAAqE;AACrG,MAAM,YAAY,MAAM,OAAO,MAAM;AAGrC,SAAS,iCAAiC,MAAM;AAC9C,gBAAc;AAAA,IACZ,MAAM,CAAC,YAAY,SAAS,MAAM,MAAM,MAAM,MAAM,iBAAiB,mBAAmB,YAAY,KAAK;AAAA,IACzG,iBAAiB;AAAA,IACjB,QAAQ;AAAA,MACN,QAAQ,QAAQ;AAAA,IAClB;AAAA,IAEA,UAAU,CAAC,EAAC,UAAU,MAAM,SAAS,SAAS,MAAM,GAAG,aAAY,MAAM;AAEvE,UAAI,CAAC,SAAS;AACZ;AAAA,MACF;AAEA,YAAM,YAAY,OAAO,IAAI,IAAI,EAAE;AAEnC,UAAI,SAAS,WAAW;AACtB,cAAM,EAAC,kBAAkB,YAAW,IAAI;AACxC,cAAM,SAAS,IAAI,WAAW,aAAa,EAAC,gBAAgB,MAAK,CAAC;AAClE,kBAAU,WAAW,MAAM,EAAE;AAC7B,cAAM,EAAC,QAAO,IAAI,SAAS,OAAO,EAAE,OAAO;AAE3C,eAAO,gBAAgB,QAAQ,EAAC,OAAM,CAAC,GAAG,gBAAgB;AAC1D;AAAA,MACF;AAEA,UAAI,SAAS,WAAW;AACtB,cAAM,EAAC,WAAW,WAAW,eAAc,IAAI;AAC/C,cAAM,EAAC,QAAO,IAAI,SAAS,OAAO,EAAE,OAAO;AAE3C,eAAO,MAAM,QAAQ,WAAW,SAAS,GAAG,cAAc;AAC1D;AAAA,MACF;AAEA,YAAM,IAAI,MAAM,gBAAgB,IAAI,EAAE;AAAA,IACxC;AAAA,EACF,CAAC;AACH,CAAC;",
4
+ "sourcesContent": ["\nimport assert from 'node:assert';\nimport {describe} from 'node:test';\nimport createDebugLogger from 'debug';\nimport generateTests from '@natlibfi/fixugen';\nimport {READERS} from '@natlibfi/fixura';\nimport {MarcRecord} from '@natlibfi/marc-record';\nimport * as features from './index.js';\n\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features/bib:test');\nconst debugData = debug.extend('data');\n\n\ndescribe('match-detection/features/bib/', () => {\n generateTests({\n path: [import.meta.dirname, '..', '..', '..', '..', 'test-fixtures', 'match-detection', 'features', 'bib'],\n useMetadataFile: true,\n fixura: {\n reader: READERS.JSON\n },\n\n callback: ({enabled = true, feature, options, type, ...expectations}) => {\n\n if (!enabled) {\n return;\n }\n\n debug(`Testing: ${feature} ${type}`);\n\n if (type === 'extract') {\n const {expectedFeatures, inputRecord} = expectations;\n const record = new MarcRecord(inputRecord, {subfieldValues: false});\n debugData(`Record: ${record}`);\n const {extract} = features[feature](options);\n\n assert.deepStrictEqual(extract({record}), expectedFeatures);\n return;\n }\n\n if (type === 'compare') {\n const {featuresA, featuresB, expectedPoints} = expectations;\n const {compare} = features[feature](options);\n\n assert.equal(Math.round(compare(featuresA, featuresB) *100)/100, expectedPoints);\n return;\n }\n\n throw new Error(`Invalid type ${type}`);\n }\n });\n});\n"],
5
+ "mappings": "AACA,OAAO,YAAY;AACnB,SAAQ,gBAAe;AACvB,OAAO,uBAAuB;AAC9B,OAAO,mBAAmB;AAC1B,SAAQ,eAAc;AACtB,SAAQ,kBAAiB;AACzB,YAAY,cAAc;AAG1B,MAAM,QAAQ,kBAAkB,qEAAqE;AACrG,MAAM,YAAY,MAAM,OAAO,MAAM;AAGrC,SAAS,iCAAiC,MAAM;AAC9C,gBAAc;AAAA,IACZ,MAAM,CAAC,YAAY,SAAS,MAAM,MAAM,MAAM,MAAM,iBAAiB,mBAAmB,YAAY,KAAK;AAAA,IACzG,iBAAiB;AAAA,IACjB,QAAQ;AAAA,MACN,QAAQ,QAAQ;AAAA,IAClB;AAAA,IAEA,UAAU,CAAC,EAAC,UAAU,MAAM,SAAS,SAAS,MAAM,GAAG,aAAY,MAAM;AAEvE,UAAI,CAAC,SAAS;AACZ;AAAA,MACF;AAEA,YAAM,YAAY,OAAO,IAAI,IAAI,EAAE;AAEnC,UAAI,SAAS,WAAW;AACtB,cAAM,EAAC,kBAAkB,YAAW,IAAI;AACxC,cAAM,SAAS,IAAI,WAAW,aAAa,EAAC,gBAAgB,MAAK,CAAC;AAClE,kBAAU,WAAW,MAAM,EAAE;AAC7B,cAAM,EAAC,QAAO,IAAI,SAAS,OAAO,EAAE,OAAO;AAE3C,eAAO,gBAAgB,QAAQ,EAAC,OAAM,CAAC,GAAG,gBAAgB;AAC1D;AAAA,MACF;AAEA,UAAI,SAAS,WAAW;AACtB,cAAM,EAAC,WAAW,WAAW,eAAc,IAAI;AAC/C,cAAM,EAAC,QAAO,IAAI,SAAS,OAAO,EAAE,OAAO;AAE3C,eAAO,MAAM,KAAK,MAAM,QAAQ,WAAW,SAAS,IAAG,GAAG,IAAE,KAAK,cAAc;AAC/E;AAAA,MACF;AAEA,YAAM,IAAI,MAAM,gBAAgB,IAAI,EAAE;AAAA,IACxC;AAAA,EACF,CAAC;AACH,CAAC;",
6
6
  "names": []
7
7
  }
@@ -1,7 +1,6 @@
1
1
  import createDebugLogger from "debug";
2
2
  import { parse as isbnParse } from "isbn3";
3
3
  import { uniqArray } from "./issn.js";
4
- import { isComponentRecord } from "@natlibfi/melinda-commons";
5
4
  const debug = createDebugLogger(`@natlibfi/melinda-record-matching:match-detection:features:standard-identifiers:ISBN`);
6
5
  const debugData = debug.extend("data");
7
6
  const MAX_SCORE = 0.75;
@@ -11,11 +10,7 @@ export default () => ({
11
10
  record
12
11
  /*, recordExternal*/
13
12
  }) => {
14
- const host = !isComponentRecord(record, false, []);
15
- if (host) {
16
- return record.get("020").filter((f) => f.subfields?.some((sf) => ["a", "z"].includes(sf.code) && sf.value));
17
- }
18
- return record.get("773").filter((f) => f.subfields?.some((sf) => ["z"].includes(sf.code) && sf.value));
13
+ return record.get("020").filter((f) => f.subfields?.some((sf) => ["a", "z"].includes(sf.code) && sf.value));
19
14
  },
20
15
  // eslint-disable-next-line max-statements
21
16
  compare: (aa, bb) => {
@@ -23,7 +18,8 @@ export default () => ({
23
18
  if (aa.length === 0 || bb.length === 0) {
24
19
  return 0;
25
20
  }
26
- const [subfieldCodeForGoodValues, subfieldCodeForBadValues] = getSubfieldCodes(aa[0].tag);
21
+ const subfieldCodeForGoodValues = "a";
22
+ const subfieldCodeForBadValues = "z";
27
23
  const [aValidValuesA, aInvalidValuesA, zValidValuesA, zInvalidValuesA] = getValuesWrapper(aa, "AA");
28
24
  const [aValidValuesB, aInvalidValuesB, zValidValuesB, zInvalidValuesB] = getValuesWrapper(bb, "BB");
29
25
  function getValuesWrapper(data, prefix) {
@@ -81,12 +77,6 @@ export default () => ({
81
77
  return 0;
82
78
  }
83
79
  return -0.75;
84
- function getSubfieldCodes(tag) {
85
- if (tag === "773") {
86
- return ["z", void 0];
87
- }
88
- return ["a", "z"];
89
- }
90
80
  function getValues(fields) {
91
81
  const goodValues = fields.flatMap((f) => f.subfields.filter((sf) => sf.code === subfieldCodeForGoodValues)).map((sf) => validatorAndNormalizer(sf.value));
92
82
  const trueGoodValues = goodValues.filter((val) => val.valid).map((val) => val.value);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/match-detection/features/bib/isbn.js"],
4
- "sourcesContent": ["\nimport createDebugLogger from 'debug';\nimport {parse as isbnParse} from 'isbn3';\n\nimport {uniqArray} from './issn.js';\nimport {isComponentRecord} from '@natlibfi/melinda-commons';\n\n\nconst debug = createDebugLogger(`@natlibfi/melinda-record-matching:match-detection:features:standard-identifiers:ISBN`);\nconst debugData = debug.extend('data');\n\nconst MAX_SCORE = 0.75;\n\nexport default () => ({\n name: 'ISBN',\n extract: ({record/*, recordExternal*/}) => {\n //const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n\n const host = !isComponentRecord(record, false, []);\n\n if (host) {\n return record.get('020').filter(f => f.subfields?.some(sf => ['a', 'z'].includes(sf.code) && sf.value));\n }\n return record.get('773').filter(f => f.subfields?.some(sf => ['z'].includes(sf.code) && sf.value));\n },\n // eslint-disable-next-line max-statements\n compare: (aa, bb) => {\n debugData(`Comparing ISBN sets ${JSON.stringify(aa)} and ${JSON.stringify(bb)}`);\n if (aa.length === 0 || bb.length === 0) {\n // No data for decision\n return 0;\n }\n\n const [subfieldCodeForGoodValues, subfieldCodeForBadValues] = getSubfieldCodes(aa[0].tag);\n\n const [aValidValuesA, aInvalidValuesA, zValidValuesA, zInvalidValuesA] = getValuesWrapper(aa, 'AA'); // initial 'a' and 'z' refer to 020 subfields codes\n const [aValidValuesB, aInvalidValuesB, zValidValuesB, zInvalidValuesB] = getValuesWrapper(bb, 'BB');\n\n function getValuesWrapper(data, prefix) {\n const [aValidValues, aInvalidValues, zValidValues, zInvalidValues] = getValues(data);\n if (aValidValues.length) {\n debug(`${prefix}: ${aa[0].tag}$${subfieldCodeForGoodValues} VALID: ${aValidValues.join(', ')}`);\n }\n if (aInvalidValues.length) {\n debug(`${prefix}: ${aa[0].tag}$${subfieldCodeForGoodValues} INVALID: ${aInvalidValues.join(', ')}`);\n }\n if (zValidValues.length) {\n debug(`${prefix}: ${aa[0].tag}$${subfieldCodeForBadValues} VALID: ${zValidValues.join(', ')}`);\n }\n if (zInvalidValues.length) {\n debug(`${prefix}: ${aa[0].tag}$${subfieldCodeForBadValues} INVALID: ${zInvalidValues.join(', ')}`);\n }\n return [aValidValues, aInvalidValues, zValidValues, zInvalidValues];\n }\n\n const [sharedGoodValues, goodValuesAOnly, goodValuesBOnly] = getUnionData(aValidValuesA, aValidValuesB);\n\n debug(`GOOD\\tBOTH: ${sharedGoodValues.length}, A only: ${goodValuesAOnly.length}, B only: ${goodValuesBOnly.length}`);\n\n if (sharedGoodValues.length > 0) {\n // Third argument (aka 'N times') is >= 0\n return scoreData(MAX_SCORE, 0.8, goodValuesAOnly.length + goodValuesBOnly.length);\n }\n\n const hitScore = scoreSuboptimalHit();\n\n function scoreSuboptimalHit() {\n // One record consider ISBN good and the other record considered it's canceled:\n if (aValidValuesA.some(valA => zValidValuesB.includes(valA)) || aValidValuesB.some(valB => zValidValuesA.includes(valB))) {\n return MAX_SCORE;\n }\n\n // Subfield is for cancelled/whatever values, but the value is syntactically valid:\n // Could happen for two canceled ISBNs for example. I'll give this two thirds of the full score\n const zzValid = zValidValuesA.find(valA => zValidValuesB.includes(valA));\n if (zzValid) {\n debug(`Both contain a valid value in 020$z: ${zzValid}`);\n return MAX_SCORE * 2 / 3;\n }\n // Shared invalid identifiers:\n const aaInvalid = aInvalidValuesA.find(valA => aInvalidValuesB.includes(valA) || zInvalidValuesB.includes(valA)) || aInvalidValuesB.find(valB => zInvalidValuesA.includes(valB));\n if (aaInvalid) {\n debug(`Shared invalid value in 020$a and 020$a-or-$z subfields: ${aaInvalid}`);\n return MAX_SCORE * 2 / 3;\n }\n\n /* // Currently I think that paired invalid idenfiers in 020$z are meaningless...\n const zzInvalid = zInvalidValuesB.find(valB => zInvalidValuesA.includes(valB)) || zInvalidValuesA.find(valA => zInvalidValuesB.includes(valA));\n if (zzInvalid) {\n debug(`Shared invalid value in 020$z subfields: ${zzInvalid}`);\n return MAX_SCORE / 3;\n }\n */\n\n return 0;\n }\n\n\n\n if (hitScore === MAX_SCORE) {\n // -1 is needed to make the third argument >= 0 (otherwise min val would be 0)\n return scoreData(hitScore, 0.8, goodValuesAOnly.length + goodValuesBOnly.length - 1);\n }\n\n if (hitScore > 0) { // Canceled/invalid ISBNs match\n // Note that this is not (currently) penalized for non-matching canceled/invalid ISBNs. Maybe it should be.\n return scoreData(hitScore, 0.8, goodValuesAOnly.length + goodValuesBOnly.length);\n }\n\n // No match:\n\n if (aValidValuesA.length + aInvalidValuesA.length === 0 || aValidValuesB.length + aInvalidValuesB.length === 0) { // At least one record did not have any good ISBNs, so not penalizing here! (Invalid 020$as are counted a bad.)\n return 0.0;\n }\n // We have same matching invalid identifiers. Don't penalize:\n const allA = [...aValidValuesA, ...aInvalidValuesA, ...zValidValuesA, ...zInvalidValuesA];\n const allB = [...aValidValuesB, ...aInvalidValuesB, ...zValidValuesB, ...zInvalidValuesB];\n const [sharedValues, tmp1, tmp2] = getUnionData(allA, allB);\n debug(`WHATEVER\\tBOTH: ${sharedGoodValues.length}, A only: ${tmp1.length}, B only: ${tmp2.length}`);\n\n if (sharedValues.length > 0) {\n return 0;\n }\n // We have values but they disagree:\n return -0.75; // Has good ISBNs on both records, but they did not match\n\n\n function getSubfieldCodes(tag) {\n if (tag === '773') {\n return ['z', undefined];\n }\n return ['a', 'z'];\n }\n\n function getValues(fields) {\n // Valid values are normalized to their isbn-13 form. Invalid values get their '-'s removed.\n const goodValues = fields.flatMap(f => f.subfields.filter(sf => sf.code === subfieldCodeForGoodValues)).map(sf => validatorAndNormalizer(sf.value));\n const trueGoodValues = goodValues.filter(val => val.valid).map(val => val.value);\n const wannabeGoodValues = goodValues.filter(val => !val.valid).map(val => val.value);\n if (!subfieldCodeForBadValues) { // 773\n return [trueGoodValues, wannabeGoodValues, [], []];\n }\n const badValues = fields.flatMap(f => f.subfields.filter(sf => sf.code === subfieldCodeForBadValues)).map(sf => validatorAndNormalizer(sf.value));\n const validBadValues = badValues.filter(val => val.valid).map(val => val.value);\n const invalidBadValues = badValues.filter(val => !val.valid).map(val => val.value);\n //const badValues = fields.flatMap(f => f.subfields.filter(sf => sf.code === subfieldCodeForBadValues)).map(sf => validatorAndNormalizer(sf.value).value);\n return [uniqArray(trueGoodValues), uniqArray(wannabeGoodValues), uniqArray(validBadValues), uniqArray(invalidBadValues)];\n }\n\n function validatorAndNormalizer(string) {\n const string2 = string.replace(/\\. -$/u, ''); // Remove punctuation (773$z)\n\n // Hack: Historically we 020$a \"1234567890 sidottu\" etc. Try the LHS alone:\n const string3 = string2.replace(/ .*$/u, '');\n if (string2 !== string3) {\n const altResult = validatorAndNormalizer(string3);\n if (altResult.valid) {\n return altResult;\n }\n }\n\n const isbnParseResult = isbnParse(string2, '') || '';\n debugData(`isbnParseResult: ${JSON.stringify(isbnParseResult)}`);\n if (!isbnParseResult.isValid) {\n debug(`Not parseable ISBN '${string2}', just removing hyphens`);\n return {valid: false, value: string2.replace(/-/ug, '')};\n }\n\n debug(`Parseable ISBN '${string2}', normalizing to ISBN-13 '${isbnParseResult.isbn13}'`);\n return {valid: true, value: isbnParseResult.isbn13};\n\n\n }\n }\n});\n\n// These are outside the default function as I'll probably want to export these later on\n\nfunction getUnionData(set1, set2) {\n const shared = set1.filter(val => set2.includes(val));\n const onlyInSet1 = set1.filter(val => !shared.includes(val));\n const onlyInSet2 = set2.filter(val => !shared.includes(val));\n return [shared, onlyInSet1, onlyInSet2];\n}\n\nfunction scoreData(score, factor, n) {\n return innerScoreData(score, n);\n function innerScoreData(currScore, remaining) {\n if (remaining > 0) {\n return innerScoreData(currScore * factor, remaining-1);\n }\n return Math.round(currScore * 100)/100; // 0.600000000001 => 0.6\n }\n}"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\nimport createDebugLogger from 'debug';\nimport {parse as isbnParse} from 'isbn3';\n\nimport {uniqArray} from './issn.js';\n\nconst debug = createDebugLogger(`@natlibfi/melinda-record-matching:match-detection:features:standard-identifiers:ISBN`);\nconst debugData = debug.extend('data');\n\nconst MAX_SCORE = 0.75;\n\nexport default () => ({\n name: 'ISBN',\n extract: ({record/*, recordExternal*/}) => {\n //const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n\n return record.get('020').filter(f => f.subfields?.some(sf => ['a', 'z'].includes(sf.code) && sf.value));\n },\n // eslint-disable-next-line max-statements\n compare: (aa, bb) => {\n debugData(`Comparing ISBN sets ${JSON.stringify(aa)} and ${JSON.stringify(bb)}`);\n if (aa.length === 0 || bb.length === 0) {\n // No data for decision\n return 0;\n }\n\n const subfieldCodeForGoodValues = 'a';\n const subfieldCodeForBadValues = 'z';\n\n const [aValidValuesA, aInvalidValuesA, zValidValuesA, zInvalidValuesA] = getValuesWrapper(aa, 'AA'); // initial 'a' and 'z' refer to 020 subfields codes\n const [aValidValuesB, aInvalidValuesB, zValidValuesB, zInvalidValuesB] = getValuesWrapper(bb, 'BB');\n\n function getValuesWrapper(data, prefix) {\n const [aValidValues, aInvalidValues, zValidValues, zInvalidValues] = getValues(data);\n if (aValidValues.length) {\n debug(`${prefix}: ${aa[0].tag}$${subfieldCodeForGoodValues} VALID: ${aValidValues.join(', ')}`);\n }\n if (aInvalidValues.length) {\n debug(`${prefix}: ${aa[0].tag}$${subfieldCodeForGoodValues} INVALID: ${aInvalidValues.join(', ')}`);\n }\n if (zValidValues.length) {\n debug(`${prefix}: ${aa[0].tag}$${subfieldCodeForBadValues} VALID: ${zValidValues.join(', ')}`);\n }\n if (zInvalidValues.length) {\n debug(`${prefix}: ${aa[0].tag}$${subfieldCodeForBadValues} INVALID: ${zInvalidValues.join(', ')}`);\n }\n return [aValidValues, aInvalidValues, zValidValues, zInvalidValues];\n }\n\n const [sharedGoodValues, goodValuesAOnly, goodValuesBOnly] = getUnionData(aValidValuesA, aValidValuesB);\n\n debug(`GOOD\\tBOTH: ${sharedGoodValues.length}, A only: ${goodValuesAOnly.length}, B only: ${goodValuesBOnly.length}`);\n\n if (sharedGoodValues.length > 0) {\n // Third argument (aka 'N times') is >= 0\n return scoreData(MAX_SCORE, 0.8, goodValuesAOnly.length + goodValuesBOnly.length);\n }\n\n const hitScore = scoreSuboptimalHit();\n\n function scoreSuboptimalHit() {\n // One record consider ISBN good and the other record considered it's canceled:\n if (aValidValuesA.some(valA => zValidValuesB.includes(valA)) || aValidValuesB.some(valB => zValidValuesA.includes(valB))) {\n return MAX_SCORE;\n }\n\n // Subfield is for cancelled/whatever values, but the value is syntactically valid:\n // Could happen for two canceled ISBNs for example. I'll give this two thirds of the full score\n const zzValid = zValidValuesA.find(valA => zValidValuesB.includes(valA));\n if (zzValid) {\n debug(`Both contain a valid value in 020$z: ${zzValid}`);\n return MAX_SCORE * 2 / 3;\n }\n // Shared invalid identifiers:\n const aaInvalid = aInvalidValuesA.find(valA => aInvalidValuesB.includes(valA) || zInvalidValuesB.includes(valA)) || aInvalidValuesB.find(valB => zInvalidValuesA.includes(valB));\n if (aaInvalid) {\n debug(`Shared invalid value in 020$a and 020$a-or-$z subfields: ${aaInvalid}`);\n return MAX_SCORE * 2 / 3;\n }\n\n /* // Currently I think that paired invalid idenfiers in 020$z are meaningless...\n const zzInvalid = zInvalidValuesB.find(valB => zInvalidValuesA.includes(valB)) || zInvalidValuesA.find(valA => zInvalidValuesB.includes(valA));\n if (zzInvalid) {\n debug(`Shared invalid value in 020$z subfields: ${zzInvalid}`);\n return MAX_SCORE / 3;\n }\n */\n\n return 0;\n }\n\n\n\n if (hitScore === MAX_SCORE) {\n // -1 is needed to make the third argument >= 0 (otherwise min val would be 0)\n return scoreData(hitScore, 0.8, goodValuesAOnly.length + goodValuesBOnly.length - 1);\n }\n\n if (hitScore > 0) { // Canceled/invalid ISBNs match\n // Note that this is not (currently) penalized for non-matching canceled/invalid ISBNs. Maybe it should be.\n return scoreData(hitScore, 0.8, goodValuesAOnly.length + goodValuesBOnly.length);\n }\n\n // No match:\n\n if (aValidValuesA.length + aInvalidValuesA.length === 0 || aValidValuesB.length + aInvalidValuesB.length === 0) { // At least one record did not have any good ISBNs, so not penalizing here! (Invalid 020$as are counted a bad.)\n return 0.0;\n }\n // We have same matching invalid identifiers. Don't penalize:\n const allA = [...aValidValuesA, ...aInvalidValuesA, ...zValidValuesA, ...zInvalidValuesA];\n const allB = [...aValidValuesB, ...aInvalidValuesB, ...zValidValuesB, ...zInvalidValuesB];\n const [sharedValues, tmp1, tmp2] = getUnionData(allA, allB);\n debug(`WHATEVER\\tBOTH: ${sharedGoodValues.length}, A only: ${tmp1.length}, B only: ${tmp2.length}`);\n\n if (sharedValues.length > 0) {\n return 0;\n }\n // We have values but they disagree:\n return -0.75; // Has good ISBNs on both records, but they did not match\n\n function getValues(fields) {\n // Valid values are normalized to their isbn-13 form. Invalid values get their '-'s removed.\n const goodValues = fields.flatMap(f => f.subfields.filter(sf => sf.code === subfieldCodeForGoodValues)).map(sf => validatorAndNormalizer(sf.value));\n const trueGoodValues = goodValues.filter(val => val.valid).map(val => val.value);\n const wannabeGoodValues = goodValues.filter(val => !val.valid).map(val => val.value);\n if (!subfieldCodeForBadValues) { // 773\n return [trueGoodValues, wannabeGoodValues, [], []];\n }\n const badValues = fields.flatMap(f => f.subfields.filter(sf => sf.code === subfieldCodeForBadValues)).map(sf => validatorAndNormalizer(sf.value));\n const validBadValues = badValues.filter(val => val.valid).map(val => val.value);\n const invalidBadValues = badValues.filter(val => !val.valid).map(val => val.value);\n //const badValues = fields.flatMap(f => f.subfields.filter(sf => sf.code === subfieldCodeForBadValues)).map(sf => validatorAndNormalizer(sf.value).value);\n return [uniqArray(trueGoodValues), uniqArray(wannabeGoodValues), uniqArray(validBadValues), uniqArray(invalidBadValues)];\n }\n\n function validatorAndNormalizer(string) {\n const string2 = string.replace(/\\. -$/u, ''); // Remove punctuation (773$z)\n\n // Hack: Historically we 020$a \"1234567890 sidottu\" etc. Try the LHS alone:\n const string3 = string2.replace(/ .*$/u, '');\n if (string2 !== string3) {\n const altResult = validatorAndNormalizer(string3);\n if (altResult.valid) {\n return altResult;\n }\n }\n\n const isbnParseResult = isbnParse(string2, '') || '';\n debugData(`isbnParseResult: ${JSON.stringify(isbnParseResult)}`);\n if (!isbnParseResult.isValid) {\n debug(`Not parseable ISBN '${string2}', just removing hyphens`);\n return {valid: false, value: string2.replace(/-/ug, '')};\n }\n\n debug(`Parseable ISBN '${string2}', normalizing to ISBN-13 '${isbnParseResult.isbn13}'`);\n return {valid: true, value: isbnParseResult.isbn13};\n\n\n }\n }\n});\n\n// These are outside the default function as I'll probably want to export these later on\n\nfunction getUnionData(set1, set2) {\n const shared = set1.filter(val => set2.includes(val));\n const onlyInSet1 = set1.filter(val => !shared.includes(val));\n const onlyInSet2 = set2.filter(val => !shared.includes(val));\n return [shared, onlyInSet1, onlyInSet2];\n}\n\nfunction scoreData(score, factor, n) {\n return innerScoreData(score, n);\n function innerScoreData(currScore, remaining) {\n if (remaining > 0) {\n return innerScoreData(currScore * factor, remaining-1);\n }\n return Math.round(currScore * 100)/100; // 0.600000000001 => 0.6\n }\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,5 +1,4 @@
1
1
  import createDebugLogger from "debug";
2
- import { isComponentRecord } from "@natlibfi/melinda-commons";
3
2
  const debug = createDebugLogger("@natlibfi/melinda-record-matching:match-detection:features:issn");
4
3
  const debugData = debug.extend("data");
5
4
  export default () => ({
@@ -8,15 +7,14 @@ export default () => ({
8
7
  record
9
8
  /*, recordExternal*/
10
9
  }) => {
11
- const isHost = !isComponentRecord(record, false, []);
12
10
  return getIssns();
13
11
  function getIssns() {
14
- const fields = getRelevantFields();
12
+ const fields = record.get(/^022$/u);
15
13
  if (fields.length === 0) {
16
14
  return [];
17
15
  }
18
16
  debug(` ${fields.length} potential ISSN fields (${fields[0].tag})`);
19
- const subfieldCodes = getRelevantSubfieldCodes();
17
+ const subfieldCodes = ["a", "y", "z"];
20
18
  const subfieldValues = fields.flatMap((f) => f.subfields.filter((sf) => subfieldCodes.includes(sf.code)).map((sf) => sf.value));
21
19
  const validSubfieldValues = subfieldValues?.map((val) => normalizeSubfieldValue(val)).filter((val) => isValidIssn(val));
22
20
  if (!validSubfieldValues) {
@@ -24,24 +22,12 @@ export default () => ({
24
22
  }
25
23
  return uniqArray(validSubfieldValues);
26
24
  }
27
- function getRelevantFields() {
28
- if (isHost) {
29
- return record.get(/^022$/u);
30
- }
31
- return record.get(/^[79]73$/u);
32
- }
33
25
  function normalizeSubfieldValue(val) {
34
26
  return val.replace(/[., :;].*$/u, "");
35
27
  }
36
28
  function isValidIssn(val) {
37
29
  return /^[0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9X]$/u.test(val);
38
30
  }
39
- function getRelevantSubfieldCodes() {
40
- if (isHost) {
41
- return ["a", "y", "z"];
42
- }
43
- return ["x"];
44
- }
45
31
  },
46
32
  // eslint-disable-next-line max-statements
47
33
  compare: (aa, bb) => {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/match-detection/features/bib/issn.js"],
4
- "sourcesContent": ["/*\n//import createInterface from './standard-identifier-factory.js';\n\n\nexport default () => {\n const {extract, compare} = createInterface({pattern: /^022$/u, subfieldCodes: ['a', 'z', 'y']});\n return {extract, compare, name: 'ISSN'};\n};\n*/\n\n\nimport createDebugLogger from 'debug';\n\nimport {isComponentRecord} from '@natlibfi/melinda-commons';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:issn');\nconst debugData = debug.extend('data');\n\nexport default () => ({\n name: 'ISSN',\n extract: ({record/*, recordExternal*/}) => {\n //const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n\n const isHost = !isComponentRecord(record, false, []);\n\n // debug(`\\t Is HOST: '${isHost}'`);\n return getIssns();\n\n function getIssns() {\n const fields = getRelevantFields();\n if (fields.length === 0) {\n return [];\n }\n debug(`\\t ${fields.length} potential ISSN fields (${fields[0].tag})`);\n const subfieldCodes = getRelevantSubfieldCodes();\n //debug(`\\t subfield codes: '${subfieldCodes.join(\"', '\")}'`);\n const subfieldValues = fields.flatMap(f => f.subfields.filter(sf => subfieldCodes.includes(sf.code)).map(sf => sf.value));\n //debug(`\\t cand values: '${subfieldValues.join(\"', '\")}'`);\n // Stripping punctuaction with substring here is pretty quick and dirty approach...\n const validSubfieldValues = subfieldValues?.map(val => normalizeSubfieldValue(val)).filter(val => isValidIssn(val));\n if (!validSubfieldValues) {\n return [];\n }\n return uniqArray(validSubfieldValues);\n }\n\n function getRelevantFields() {\n if (isHost) {\n return record.get(/^022$/u);\n }\n return record.get(/^[79]73$/u);\n }\n\n function normalizeSubfieldValue(val) {\n return val.replace(/[., :;].*$/u, '');\n }\n\n function isValidIssn(val) {\n return /^[0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9X]$/u.test(val);\n }\n\n function getRelevantSubfieldCodes() {\n if (isHost) {\n return ['a', 'y', 'z'];\n }\n return ['x'];\n }\n },\n // eslint-disable-next-line max-statements\n compare: (aa, bb) => {\n debugData(`Comparing ISSN sets ${JSON.stringify(aa)} and ${JSON.stringify(bb)}`);\n if (aa.length === 0 || bb.length === 0) {\n // No data for decision\n return 0;\n }\n const firstSharedIssn = aa.find(val => bb.includes(val));\n if (firstSharedIssn) {\n debug(`\\t Shared ISSN found: '${firstSharedIssn}'`);\n // Maybe less for comps?\n return 0.2;\n }\n return -0.2;\n\n }\n});\n\nexport function uniqArray(arr) {\n return arr.filter((val, i) => arr.indexOf(val) === i);\n}"],
5
- "mappings": "AAWA,OAAO,uBAAuB;AAE9B,SAAQ,yBAAwB;AAEhC,MAAM,QAAQ,kBAAkB,iEAAiE;AACjG,MAAM,YAAY,MAAM,OAAO,MAAM;AAErC,eAAe,OAAO;AAAA,EACpB,MAAM;AAAA,EACN,SAAS,CAAC;AAAA,IAAC;AAAA;AAAA,EAA0B,MAAM;AAGzC,UAAM,SAAS,CAAC,kBAAkB,QAAQ,OAAO,CAAC,CAAC;AAGnD,WAAO,SAAS;AAEhB,aAAS,WAAW;AAClB,YAAM,SAAS,kBAAkB;AACjC,UAAI,OAAO,WAAW,GAAG;AACvB,eAAO,CAAC;AAAA,MACV;AACA,YAAM,KAAM,OAAO,MAAM,2BAA2B,OAAO,CAAC,EAAE,GAAG,GAAG;AACpE,YAAM,gBAAgB,yBAAyB;AAE/C,YAAM,iBAAiB,OAAO,QAAQ,OAAK,EAAE,UAAU,OAAO,QAAM,cAAc,SAAS,GAAG,IAAI,CAAC,EAAE,IAAI,QAAM,GAAG,KAAK,CAAC;AAGxH,YAAM,sBAAsB,gBAAgB,IAAI,SAAO,uBAAuB,GAAG,CAAC,EAAE,OAAO,SAAO,YAAY,GAAG,CAAC;AAClH,UAAI,CAAC,qBAAqB;AACxB,eAAO,CAAC;AAAA,MACV;AACA,aAAO,UAAU,mBAAmB;AAAA,IACtC;AAEA,aAAS,oBAAoB;AAC3B,UAAI,QAAQ;AACV,eAAO,OAAO,IAAI,QAAQ;AAAA,MAC5B;AACA,aAAO,OAAO,IAAI,WAAW;AAAA,IAC/B;AAEA,aAAS,uBAAuB,KAAK;AACnC,aAAO,IAAI,QAAQ,eAAe,EAAE;AAAA,IACtC;AAEA,aAAS,YAAY,KAAK;AACxB,aAAO,gDAAgD,KAAK,GAAG;AAAA,IACjE;AAEA,aAAS,2BAA2B;AAClC,UAAI,QAAQ;AACV,eAAO,CAAC,KAAK,KAAK,GAAG;AAAA,MACvB;AACA,aAAO,CAAC,GAAG;AAAA,IACb;AAAA,EACF;AAAA;AAAA,EAEA,SAAS,CAAC,IAAI,OAAO;AACnB,cAAU,uBAAuB,KAAK,UAAU,EAAE,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC,EAAE;AAC/E,QAAI,GAAG,WAAW,KAAK,GAAG,WAAW,GAAG;AAEtC,aAAO;AAAA,IACT;AACA,UAAM,kBAAkB,GAAG,KAAK,SAAO,GAAG,SAAS,GAAG,CAAC;AACvD,QAAI,iBAAiB;AACnB,YAAM,yBAA0B,eAAe,GAAG;AAElD,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EAET;AACF;AAEO,gBAAS,UAAU,KAAK;AAC7B,SAAO,IAAI,OAAO,CAAC,KAAK,MAAM,IAAI,QAAQ,GAAG,MAAM,CAAC;AACtD;",
4
+ "sourcesContent": ["import createDebugLogger from 'debug';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:issn');\nconst debugData = debug.extend('data');\n\nexport default () => ({\n name: 'ISSN',\n extract: ({record/*, recordExternal*/}) => {\n //const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n\n return getIssns();\n\n function getIssns() {\n const fields = record.get(/^022$/u);\n if (fields.length === 0) {\n return [];\n }\n\n debug(`\\t ${fields.length} potential ISSN fields (${fields[0].tag})`);\n const subfieldCodes = ['a', 'y', 'z']\n //debug(`\\t subfield codes: '${subfieldCodes.join(\"', '\")}'`);\n const subfieldValues = fields.flatMap(f => f.subfields.filter(sf => subfieldCodes.includes(sf.code)).map(sf => sf.value));\n //debug(`\\t cand values: '${subfieldValues.join(\"', '\")}'`);\n // Stripping punctuaction with substring here is pretty quick and dirty approach...\n const validSubfieldValues = subfieldValues?.map(val => normalizeSubfieldValue(val)).filter(val => isValidIssn(val));\n if (!validSubfieldValues) {\n return [];\n }\n return uniqArray(validSubfieldValues);\n }\n\n function normalizeSubfieldValue(val) {\n return val.replace(/[., :;].*$/u, '');\n }\n\n function isValidIssn(val) {\n return /^[0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9X]$/u.test(val);\n }\n },\n // eslint-disable-next-line max-statements\n compare: (aa, bb) => {\n debugData(`Comparing ISSN sets ${JSON.stringify(aa)} and ${JSON.stringify(bb)}`);\n if (aa.length === 0 || bb.length === 0) {\n // No data for decision\n return 0;\n }\n const firstSharedIssn = aa.find(val => bb.includes(val));\n if (firstSharedIssn) {\n debug(`\\t Shared ISSN found: '${firstSharedIssn}'`);\n // Maybe less for comps?\n return 0.2;\n }\n return -0.2;\n\n }\n});\n\nexport function uniqArray(arr) {\n return arr.filter((val, i) => arr.indexOf(val) === i);\n}"],
5
+ "mappings": "AAAA,OAAO,uBAAuB;AAE9B,MAAM,QAAQ,kBAAkB,iEAAiE;AACjG,MAAM,YAAY,MAAM,OAAO,MAAM;AAErC,eAAe,OAAO;AAAA,EACpB,MAAM;AAAA,EACN,SAAS,CAAC;AAAA,IAAC;AAAA;AAAA,EAA0B,MAAM;AAGzC,WAAO,SAAS;AAEhB,aAAS,WAAW;AAClB,YAAM,SAAS,OAAO,IAAI,QAAQ;AAClC,UAAI,OAAO,WAAW,GAAG;AACvB,eAAO,CAAC;AAAA,MACV;AAEA,YAAM,KAAM,OAAO,MAAM,2BAA2B,OAAO,CAAC,EAAE,GAAG,GAAG;AACpE,YAAM,gBAAgB,CAAC,KAAK,KAAK,GAAG;AAEpC,YAAM,iBAAiB,OAAO,QAAQ,OAAK,EAAE,UAAU,OAAO,QAAM,cAAc,SAAS,GAAG,IAAI,CAAC,EAAE,IAAI,QAAM,GAAG,KAAK,CAAC;AAGxH,YAAM,sBAAsB,gBAAgB,IAAI,SAAO,uBAAuB,GAAG,CAAC,EAAE,OAAO,SAAO,YAAY,GAAG,CAAC;AAClH,UAAI,CAAC,qBAAqB;AACxB,eAAO,CAAC;AAAA,MACV;AACA,aAAO,UAAU,mBAAmB;AAAA,IACtC;AAEA,aAAS,uBAAuB,KAAK;AACnC,aAAO,IAAI,QAAQ,eAAe,EAAE;AAAA,IACtC;AAEA,aAAS,YAAY,KAAK;AACxB,aAAO,gDAAgD,KAAK,GAAG;AAAA,IACjE;AAAA,EACF;AAAA;AAAA,EAEA,SAAS,CAAC,IAAI,OAAO;AACnB,cAAU,uBAAuB,KAAK,UAAU,EAAE,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC,EAAE;AAC/E,QAAI,GAAG,WAAW,KAAK,GAAG,WAAW,GAAG;AAEtC,aAAO;AAAA,IACT;AACA,UAAM,kBAAkB,GAAG,KAAK,SAAO,GAAG,SAAS,GAAG,CAAC;AACvD,QAAI,iBAAiB;AACnB,YAAM,yBAA0B,eAAe,GAAG;AAElD,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EAET;AACF;AAEO,gBAAS,UAAU,KAAK;AAC7B,SAAO,IAAI,OAAO,CAAC,KAAK,MAAM,IAAI,QAAQ,GAAG,MAAM,CAAC;AACtD;",
6
6
  "names": []
7
7
  }
@@ -11,7 +11,8 @@ export default () => ({
11
11
  const clonedRecord = new MarcRecord(record, record._validationOptions);
12
12
  FixSami041().fix(clonedRecord);
13
13
  Remove041zxx().fix(clonedRecord);
14
- return [{ leader: clonedRecord.leader, fields: clonedRecord.fields }, label];
14
+ clonedRecord.fields = clonedRecord.fields.filter((f) => ["008", "041"].includes(f.tag));
15
+ return [clonedRecord.fields, label];
15
16
  },
16
17
  // eslint-disable-next-line max-statements
17
18
  compare: (aa, bb) => {
@@ -25,9 +26,6 @@ export default () => ({
25
26
  if (score > 0.1) {
26
27
  return 0.1;
27
28
  }
28
- if (score < -1) {
29
- return -1;
30
- }
31
29
  return score;
32
30
  }
33
31
  function compare008() {
@@ -60,11 +58,11 @@ export default () => ({
60
58
  return mainPenalty + sourcePenalty + scoreInd1;
61
59
  }
62
60
  return compareLanguageCodeLists(getLanguageCodesFrom041Fields(a), getLanguageCodesFrom041Fields(b));
63
- function getFields041(record) {
64
- if (!record.fields) {
61
+ function getFields041(fields) {
62
+ if (!fields) {
65
63
  return [];
66
64
  }
67
- return record.fields.filter((f) => f.tag === "041");
65
+ return fields.filter((f) => f.tag === "041");
68
66
  }
69
67
  }
70
68
  function getSourceOfLanguageCode(field) {
@@ -91,7 +89,7 @@ export default () => ({
91
89
  return [" ", "|||", "und"].includes(languageCode);
92
90
  }
93
91
  function compareLanguageCodeLists(a2, b2) {
94
- if (a2.length === 0 || b2.length === 0) {
92
+ if (a2.length === 0 || b2.length === 0 || a2.every((val) => containsNoData(val)) || b2.every((val) => containsNoData(val))) {
95
93
  debugData(`No language to compare`);
96
94
  return 0;
97
95
  }
@@ -114,16 +112,8 @@ export default () => ({
114
112
  if (b2.length === 1 && b2[0] === "mul" && a2.length > 1) {
115
113
  return 0;
116
114
  }
117
- const sharedValues = getSharedValues(a2, b2);
118
- const aOnly = a2.filter((val) => !sharedValues.includes(val));
119
- const bOnly = b2.filter((val) => !sharedValues.includes(val));
120
- const hasUnd = [...aOnly, ...bOnly].includes("und");
115
+ const sharedValues = getSharedValues(a2, b2).filter((val) => !containsNoData(val));
121
116
  if (sharedValues.length < 1) {
122
- console.info(`NV: ${sharedValues.join(", ")}`);
123
- if (aOnly.length === 1 && bOnly.length === 1 && hasUnd) {
124
- debug(`Both have languages, but none of these match. However, the benefit of doubt is given: '${aOnly[0]}' and '${bOnly[0]}' might mean the same}`);
125
- return 0;
126
- }
127
117
  debug(`Both have languages, but none of these match.`);
128
118
  return -1;
129
119
  }
@@ -144,11 +134,11 @@ export default () => ({
144
134
  }
145
135
  }
146
136
  });
147
- function get008Value(record, label = "record") {
148
- if (!record || !record.fields) {
137
+ function get008Value(fields, label = "record") {
138
+ if (!fields) {
149
139
  return;
150
140
  }
151
- const f008 = record.fields.find((f) => f.tag === "008");
141
+ const f008 = fields.find((f) => f.tag === "008");
152
142
  if (!f008) {
153
143
  return void 0;
154
144
  }
@@ -182,8 +172,8 @@ function isLangCodeForALanguage(code, label = "record", encoding = void 0) {
182
172
  }
183
173
  return true;
184
174
  }
185
- function getLanguageCodesFrom041Fields(record) {
186
- const values = record.fields.filter((f) => f.tag === "041").flatMap((f) => getFieldsLanguageCodes(f));
175
+ function getLanguageCodesFrom041Fields(fields) {
176
+ const values = fields.filter((f) => f.tag === "041").flatMap((f) => getFieldsLanguageCodes(f));
187
177
  return [...new Set(values)].sort();
188
178
  }
189
179
  function getFieldsLanguageCodes(field) {