@natlibfi/melinda-record-matching 5.1.1-alpha.1 → 5.1.2-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.
package/dist/cli.js CHANGED
@@ -59,6 +59,7 @@ async function cli() {
59
59
  matchDetection.features.bib.hostComponent(),
60
60
  matchDetection.features.bib.isbn(),
61
61
  matchDetection.features.bib.issn(),
62
+ matchDetection.features.bib.publisherNumber(),
62
63
  // matchDetection.features.bib.sid(),
63
64
  matchDetection.features.bib.otherStandardIdentifier(),
64
65
  // Let's not use the same title matchDetection here
@@ -96,6 +97,7 @@ async function cli() {
96
97
  matchDetection.features.bib.hostComponent(),
97
98
  matchDetection.features.bib.isbn(),
98
99
  matchDetection.features.bib.issn(),
100
+ matchDetection.features.bib.publisherNumber(),
99
101
  // matchDetection.features.bib.sid(),
100
102
  matchDetection.features.bib.otherStandardIdentifier(),
101
103
  matchDetection.features.bib.title(),
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', 'YSO'].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.sid(),\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.sid(),\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 if (['YSO']) {\n return [\n // matchDetection.features.bib.sid(), // SIDs might be relevant on the auth side as well?\n matchDetection.features.auth.yso()\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 if (searchType === 'YSO') {\n return [\n candidateSearch.searchTypes.auth.authURX\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', 'YSO'].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.publisherNumber(),\n // matchDetection.features.bib.sid(),\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.publisherNumber(),\n // matchDetection.features.bib.sid(),\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 if (['YSO']) {\n return [\n // matchDetection.features.bib.sid(), // SIDs might be relevant on the auth side as well?\n matchDetection.features.auth.yso()\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 if (searchType === 'YSO') {\n return [\n candidateSearch.searchTypes.auth.authURX\n ];\n }\n\n throw new Error('Unsupported match validation package');\n }\n}\n"],
5
+ "mappings": "AAAA,OAAO,QAAQ;AACf,OAAO,WAAW;AAClB,OAAO,uBAAsB,iBAAiB,sBAAqB;AACnE,OAAO,uBAAuB;AAC9B,SAAQ,kBAAiB;AAEzB,IAAI;AAEJ,eAAe,MAAM;AACnB,QAAM,QAAQ,kBAAkB,uCAAuC;AACvE,QAAM,OAAO,MAAM,QAAQ,KAAK,MAAM,CAAC,CAAC,EACrC,WAAW,4BAA4B,EACvC,OAAO,kFAAkF,EACzF,MAAM,qDAAqD,EAC3D,MAAM,yEAAyE,EAC/E,MAAM,wEAAwE,EAC9E,MAAM,wFAAwF,EAC9F,eAAe,IAAI,EACnB,QAAQ,CAAC,CAAC,iCAAiC,CAAC,CAAC,EAC7C,IAAI,sBAAsB,EAC1B,QAAQ,EACR,WAAW,QAAQ,EAAC,MAAM,UAAU,UAAU,gCAA+B,CAAC,EAC9E,QAAQ;AAAA,IACP,GAAG,EAAC,MAAM,UAAU,SAAS,OAAO,OAAO,cAAc,UAAU,sDAAqD;AAAA,IACxH,GAAG,EAAC,MAAM,UAAU,SAAS,GAAG,OAAO,cAAc,UAAU,GAAE;AAAA,IACjE,GAAG,EAAC,MAAM,UAAU,SAAS,KAAM,OAAO,iBAAiB,UAAU,GAAE;AAAA,IACvE,GAAG,EAAC,MAAM,WAAW,SAAS,OAAO,OAAO,kBAAkB,UAAU,GAAE;AAAA,IAC1E,GAAG,EAAC,MAAM,WAAW,SAAS,OAAO,OAAO,eAAe,UAAU,GAAE;AAAA,IACvE,GAAG,EAAC,MAAM,WAAW,SAAS,OAAO,OAAO,oBAAoB,UAAU,GAAE;AAAA,EAC9E,CAAC,EACA,MAAM,CAACA,UAAS;AACf,UAAM,CAACC,KAAI,IAAID,MAAK;AACpB,QAAIC,UAAS,QAAW;AACtB,YAAM,IAAI,MAAM,wBAAwB;AAAA,IAC1C;AAEA,QAAI,CAAC,GAAG,WAAWA,KAAI,GAAG;AACxB,YAAM,IAAI,MAAM,QAAQA,KAAI,iBAAiB;AAAA,IAC/C;AAEA,QAAID,MAAK,WAAW,QAAW;AAC7B,YAAM,IAAI,MAAM,eAAe;AAAA,IACjC;AAEA,QAAI,CAAC,CAAC,OAAO,gBAAgB,aAAa,WAAW,cAAc,KAAK,EAAE,SAASA,MAAK,UAAU,GAAG;AACnG,YAAM,IAAI,MAAM,qBAAqB;AAAA,IACvC;AAEA,WAAO;AAAA,EACT,CAAC,EACA,UAAU;AAEb,QAAM,CAAC,IAAI,IAAI,KAAK;AACpB,QAAM,EAAC,WAAU,IAAI;AACrB,QAAM,KAAK,UAAU,IAAI,CAAC;AAE1B,QAAM,YAAY;AAAA,IAChB,WAAW;AAAA,IACX,UAAU,iBAAiB,UAAU;AAAA,EACvC;AAEA,QAAM,SAAS;AAAA,IACb,KAAK,KAAK;AAAA,IAAQ,YAAY,mBAAmB,UAAU;AAAA,EAC7D;AAEA,QAAM,gBAAgB,MAAM,oBAAoB,EAAC,WAAW,QAAQ,GAAG,KAAI,CAAC;AAE5E,QAAM,UAAU,GAAG,aAAa,MAAM,MAAM;AAC5C,QAAM,SAAS,IAAI,WAAW,KAAK,MAAM,OAAO,GAAG,EAAC,gBAAgB,MAAK,CAAC;AAE1E,QAAM,SAAS,MAAM,cAAc,EAAC,OAAM,CAAC;AAC3C,QAAM,KAAK,UAAU,MAAM,CAAC;AAG5B,WAAS,iBAAiBE,aAAY;AACpC,QAAI,CAAC,KAAK,EAAE,SAASA,WAAU,GAAG;AAChC,aAAO;AAAA,QACL,eAAe,SAAS,IAAI,UAAU;AAAA,QACtC,eAAe,SAAS,IAAI,aAAa;AAAA,QACzC,eAAe,SAAS,IAAI,KAAK;AAAA;AAAA,MACnC;AAAA,IACF;AAKA,QAAI,CAAC,cAAc,EAAE,SAASA,WAAU,GAAG;AACzC,aAAO;AAAA,QACL,eAAe,SAAS,IAAI,cAAc;AAAA,QAC1C,eAAe,SAAS,IAAI,KAAK;AAAA,QACjC,eAAe,SAAS,IAAI,KAAK;AAAA,QACjC,eAAe,SAAS,IAAI,gBAAgB;AAAA;AAAA,QAE5C,eAAe,SAAS,IAAI,wBAAwB;AAAA;AAAA;AAAA,QAGpD,eAAe,SAAS,IAAI,QAAQ;AAAA;AAAA,QAEpC,eAAe,SAAS,IAAI,WAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAMvC,eAAe,SAAS,IAAI,mCAAmC;AAAA,QAC/D,eAAe,SAAS,IAAI,SAAS;AAAA,QACrC,eAAe,SAAS,IAAI,mBAAmB;AAAA,QAC/C,eAAe,SAAS,IAAI,KAAK;AAAA;AAAA,MACnC;AAAA,IACF;AAEA,QAAI,CAAC,WAAW,EAAE,SAASA,WAAU,GAAG;AACtC,aAAO;AAAA,QACL,eAAe,SAAS,IAAI,cAAc;AAAA,QAC1C,eAAe,SAAS,IAAI,wBAAwB;AAAA,QACpD,eAAe,SAAS,IAAI,WAAW;AAAA,QACvC,eAAe,SAAS,IAAI,MAAM;AAAA,QAClC,eAAe,SAAS,IAAI,SAAS;AAAA,QACrC,eAAe,SAAS,IAAI,QAAQ;AAAA,QACpC,eAAe,SAAS,IAAI,mBAAmB;AAAA,QAC/C,eAAe,SAAS,IAAI,KAAK;AAAA;AAAA,MACnC;AAAA,IACF;AAEA,QAAI,CAAC,WAAW,YAAY,EAAE,SAASA,WAAU,GAAG;AAClD,aAAO;AAAA,QACL,eAAe,SAAS,IAAI,cAAc;AAAA,QAC1C,eAAe,SAAS,IAAI,KAAK;AAAA,QACjC,eAAe,SAAS,IAAI,KAAK;AAAA,QACjC,eAAe,SAAS,IAAI,gBAAgB;AAAA;AAAA,QAE5C,eAAe,SAAS,IAAI,wBAAwB;AAAA,QACpD,eAAe,SAAS,IAAI,MAAM;AAAA,QAClC,eAAe,SAAS,IAAI,QAAQ;AAAA,QACpC,eAAe,SAAS,IAAI,WAAW;AAAA,QACvC,eAAe,SAAS,IAAI,gBAAgB;AAAA,QAC5C,eAAe,SAAS,IAAI,SAAS;AAAA,QACrC,eAAe,SAAS,IAAI,mBAAmB;AAAA,QAC/C,eAAe,SAAS,IAAI,KAAK;AAAA;AAAA,MACnC;AAAA,IACF;AAEA,QAAI,CAAC,KAAK,GAAG;AACX,aAAO;AAAA;AAAA,QAEL,eAAe,SAAS,KAAK,IAAI;AAAA,MACnC;AAAA,IACF;AAEA,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACxD;AAGA,WAAS,mBAAmBA,aAAY;AACtC,QAAI,CAAC,KAAK,EAAE,SAASA,WAAU,GAAG;AAChC,aAAO;AAAA,QACL,gBAAgB,YAAY,IAAI;AAAA,QAChC,gBAAgB,YAAY,IAAI;AAAA,MAClC;AAAA,IACF;AAEA,QAAI,CAAC,cAAc,EAAE,SAASA,WAAU,GAAG;AACzC,aAAO,CAAC,gBAAgB,YAAY,IAAI,mBAAmB;AAAA,IAC7D;AAEA,QAAI,CAAC,WAAW,EAAE,SAASA,WAAU,GAAG;AACtC,aAAO;AAAA;AAAA,QAEL,gBAAgB,YAAY,UAAU;AAAA,QACtC,gBAAgB,YAAY,UAAU;AAAA,MACxC;AAAA,IACF;AAEA,QAAI,CAAC,SAAS,EAAE,SAASA,WAAU,GAAG;AACpC,aAAO;AAAA,QACL,gBAAgB,YAAY,IAAI;AAAA;AAAA,QAEhC,gBAAgB,YAAY,IAAI;AAAA,MAClC;AAAA,IACF;AACA,QAAI,CAAC,YAAY,EAAE,SAASA,WAAU,GAAG;AACvC,aAAO;AAAA,QACL,gBAAgB,YAAY,IAAI;AAAA;AAAA;AAAA,QAGhC,gBAAgB,YAAY,IAAI;AAAA,MAClC;AAAA,IACF;AAEA,QAAIA,gBAAe,OAAO;AACxB,aAAO;AAAA,QACL,gBAAgB,YAAY,KAAK;AAAA,MACnC;AAAA,IACF;AAEA,UAAM,IAAI,MAAM,sCAAsC;AAAA,EACxD;AACF;",
6
6
  "names": ["args", "file", "searchType"]
7
7
  }
package/dist/index.js CHANGED
@@ -146,7 +146,7 @@ export default ({ detection: detectionOptions, search: searchOptions, maxMatches
146
146
  debug(`${detectionResult.match ? `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is a match!` : `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is NOT a match!`}`);
147
147
  debugData(`Strategy: ${JSON.stringify(detectionResult.strategy)}, Threshold: ${JSON.stringify(detectionResult.threshold)}`);
148
148
  const matchResult = {
149
- probability: detectionResult.probability,
149
+ probability: Math.round(detectionResult.probability * 100) / 100,
150
150
  candidate: {
151
151
  id: candidateId,
152
152
  record: candidateRecord
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.js"],
4
- "sourcesContent": ["import createDebugLogger from 'debug';\nimport createSearchInterface, * as candidateSearch from './candidate-search/index.js';\nimport createDetectionInterface, * as matchDetection from './match-detection/index.js';\n//import inspect from 'util';\n\nexport {candidateSearch, matchDetection};\n\nexport default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false}) => {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:index');\n const debugData = debug.extend('data');\n\n debugData(`DetectionOptions: ${JSON.stringify(detectionOptions)}`);\n debugData(`SearchOptions: ${JSON.stringify(searchOptions)}`);\n debugData(`MaxMatches: ${JSON.stringify(maxMatches)}`);\n debugData(`MaxCandidates: ${JSON.stringify(maxCandidates)}`);\n debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);\n debugData(`ReturnQuery: ${JSON.stringify(returnQuery)}`);\n debugData(`ReturnNonMatches: ${JSON.stringify(returnNonMatches)}`);\n\n\n const detect = createDetectionInterface(detectionOptions, returnStrategy);\n\n return prepareSearch;\n\n async function prepareSearch({record, recordExternal = {recordSource: 'incomingRecord', label: 'ic'}}) {\n\n const {search} = await createSearchInterface({...searchOptions, record, maxCandidates, recordExternal});\n return iterate({});\n\n // candidateCount : amount of candidate records retrived from SRU for matching, NOT including current record set\n // matches : candidates that have been detected as matches by current matcher job\n // nonMatches : candidates that have been detected as non-matches by current matcher job (only if returnNonMatches is 'true')\n // duplicateCount : amount of candidate records that were retrieved from the SRU but not handled further because they were already found in the matches/nonMatches\n\n // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)\n // state.query : current query (undefined if there was no queries left)\n // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)\n // state.queryCandidateCounter: amount of candidate records retrieved from SRU for matching for current query, including the current record set (undefined if there were no queries left)\n // state.queriesLeft : amount of queries left\n // state.queryCounter : sequence for current query\n // state.maxedQueries : queries that resulted in more than serverMaxResults hits\n\n async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0, matchErrors = []}) {\n debugData(`Starting next matcher iteration.`);\n const {records, ...state} = await search(initialState);\n\n debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, nonMatchCount: ${nonMatchCount}, matchErrors: ${matchErrors.length}`);\n const recordSetSize = records.length;\n const newCandidateCount = candidateCount + recordSetSize;\n\n\n if (recordSetSize > 0) {\n return handleRecordSet();\n }\n\n if (state.queriesLeft > 0) {\n debug(`Empty record set ${state.searchCounter} for ${state.query}, but there are ${state.queriesLeft} queries left`);\n return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, nonMatchCount, duplicateCount, matchErrors});\n }\n\n debug(`No (more) candidate records to check, no more queries left, matches: ${matches.length}`);\n return returnResult({matches, state, stopReason: '', nonMatches, nonMatchCount, candidateCount: newCandidateCount, duplicateCount, matchErrors});\n\n function handleRecordSet() {\n debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);\n\n const matchResult = iterateRecords({records, recordSetSize, maxMatches, matches, nonMatches, nonMatchCount});\n\n const newDuplicateCount = duplicateCount + matchResult.duplicateCount;\n const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;\n const {newMatches, newNonMatches, newMatchErrors} = handleMatchResult(matchResult, matches, nonMatches, matchErrors);\n\n if (maxMatchesFound({matches: newMatches, maxMatches})) {\n return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});\n }\n\n if (maxCandidatesRetrieved(newCandidateCount, maxCandidates)) {\n return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});\n }\n\n return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});\n }\n\n function handleMatchResult(matchResult, matches, nonMatches, matchErrors) {\n debugData(`- Amount of new matches from record set: ${matchResult.matches.length}`);\n if (returnNonMatches) {\n debugData(`- Amount of new nonMatches from record set: ${matchResult.nonMatches.length}`);\n }\n\n const newMatches = matches.concat(returnQuery ? addQuery(matchResult.matches) : matchResult.matches);\n const newNonMatches = returnNonMatches ? nonMatches.concat(returnQuery ? addQuery(matchResult.nonMatches) : matchResult.nonMatches) : [];\n const newMatchErrors = matchErrors.concat(matchResult.matchErrors);\n\n debugData(`- Total amount of matches: ${newMatches.length}`);\n if (returnNonMatches) {\n debugData(`- Total amount of nonMatches: ${newNonMatches.length}`);\n }\n\n debugData(`MatchResult: ${JSON.stringify(matchResult)}`);\n debugData(`Old matchErrors: ${JSON.stringify(matchErrors)}, matchErrors from matchResult: ${JSON.stringify(matchResult.matchErrors)}, New matchErrors: ${JSON.stringify(newMatchErrors)}`);\n\n debugData(`- Total amount of matchErrors: ${newMatchErrors.length}`);\n\n return {newMatches, newNonMatches, newMatchErrors};\n }\n\n function addQuery(matches) {\n debugData(`Adding query ${state.query} to matches`);\n return matches.map((match) => ({...match, matchQuery: state.query}));\n }\n\n function maxCandidatesRetrieved(candidateCount, maxCandidates) {\n debugData(`Total amount of candidate records retrieved: ${newCandidateCount} (max: ${maxCandidates})`);\n if (maxCandidates && candidateCount >= maxCandidates) {\n debug(`Stopped matching because maximum number of candidate records ${candidateCount} / ${maxCandidates} have been retrieved`);\n return true;\n }\n }\n }\n\n // matches : array of matching candidate records\n // nonMatches : array of nonMatching candidate records (if returnNonMatches option is true, otherwise empty array)\n // - candidate.id\n // - candidate.record\n // - probability\n // - strategy (if returnStrategy option is true)\n // - threshold (if returnStrategy option is true)\n // - matchQuery (if returnQuery option is true)\n // failures: array of conversionFailures from candidate-search and matchErrors from matchDetection in error format {status, payload: {message, id}} if returnFailures is true\n\n // we could have here also returnRecords/returnMatchRecords/returnNonMatchRecord options that could be turned false for not to return actual record data\n\n // matchStatus.status: boolean, true if matcher retrieved and handled all found candidate records, false if it did not\n // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records\n // - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)\n // - currently stopReason can be non-empty also in cases where status is true, if matcher hit the stop reason when handling the last available candidate record\n\n function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount, matchErrors}) {\n const matchErrorCount = matchErrors.length;\n checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, matchErrorCount});\n const matchStatus = getMatchState(state, stopReason, matchErrorCount);\n // add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered\n const matchesResult = returnNonMatches ? {matches, matchStatus, nonMatches, candidateCount} : {matches, matchStatus, candidateCount};\n const result = matchesResult;\n debugData(`${JSON.stringify(result)}`);\n return result;\n\n // note that in cases where the matching has been stopped because of maxMatches checkCounts won't (in most cases) match\n\n function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, matchErrorCount}) {\n const matchCount = matches.length;\n debugData(`Return nonMatches: ${returnNonMatches}`);\n const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;\n const totalHandled = matchCount + chosenNonMatchCount + duplicateCount;\n debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, matchErrorCount: ${matchErrorCount}`);\n debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);\n if (totalHandled !== candidateCount) {\n debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);\n return;\n }\n return;\n }\n\n function getMatchState(state, stopReason, matchErrorCount) {\n debugData(`${JSON.stringify(state)}`);\n debug(`We had ${matchErrorCount} retrieved candidates that errored in matchDetection.`);\n debug(`Queries left ${state.queriesLeft}, Searches for current query left: ${state.resultSetOffset && state.resultSetOffset <= state.totalRecords}, non-retrieved records: ${state.totalRecords - state.queryCandidateCounter}, maxedQueries (${state.maxedQueries.length}): ${state.maxedQueries}`);\n\n debugData(`StopReason: <${stopReason}>`);\n\n const searchesLeft = state.resultSetOffset && state.resultSetOffset <= state.totalRecords;\n const nonRetrieved = searchesLeft ? state.totalRecords - state.queryCandidateCounter : 0;\n debugData(`nonRetrieved: ${nonRetrieved}`);\n\n // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries', empty string/undefined), reason for stopping retrieving or handling the candidate records\n // 'maxMatches' and 'maxCandidates' are in stopReason, 'maxedQueries', 'conversionFailures' and 'matchErrors' are created here\n\n if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 || matchErrorCount > 0) {\n const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : undefined;\n const matchErrorsStopReason = matchErrorCount > 0 ? 'matchErrors' : undefined;\n const newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason || matchErrorsStopReason : stopReason;\n debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);\n debugData(`MatchErrorsStopReason <${matchErrorsStopReason}>`);\n debugData(`NewStopReason: <${newStopReason}>`);\n debug(`Match status: false`);\n return {status: false, stopReason: newStopReason};\n }\n\n debug(`Match status: true`);\n return {status: true, stopReason};\n }\n }\n\n // NOTES:\n // - we could optimize by creating the featureSet for the incoming record once and using it for all database/candidateRecords\n // - if creating the featureSet for the incoming record fails we have an unprocessable entity\n // - if creating the featureSet for a candidate record fails we could skip that candidate - but list the case as a detectionFailure, same as conversionFailures\n\n function iterateRecords({records, recordSetSize, maxMatches, matches = [], nonMatches = [], recordMatches = [], recordNonMatches = [], recordCount = 0, recordDuplicateCount = 0, recordNonMatchCount = 0, recordMatchErrors = []}) {\n\n // recordSetSize : total amount of records in the current record set\n // recordCount : amount of records from the current record set that have been handled\n // maxMatches : setting for maximum amount found by current matcher job before the matcher job is stopped\n // recordDuplicateCount : amount of records from the current record set that are already included in matches/nonMatches results\n // recordNonMatchCount: amount of records from the current record set that are nonMatches (only is returnNonMatches setting is false)\n\n // records : non-handled records in the current record set\n // matches : found matches in the current matcher job\n // recordMatches : found matches in the current record set\n // recordNonMatches : found nonMatches in the current record set (only if returnNonMatches setting is true)\n // recordMatchErrors: errored matchDetection in the current record set\n\n const [candidate] = records;\n const newRecordCount = candidate ? recordCount + 1 : recordCount;\n\n // The matcher uses same matchDetection strategy for candidates from all candidate-searches -> matchDetection result for the same candidate is always same\n // Exceptions would happen if the candidate would have been updated in the database between candidate searches\n // Note that if returnNonMatches is false, matcher won't remember candidates that didn't match, so they will be matched again everytime they are retrieved by\n // different candidate search queries. Same candidate search query won't have duplicate records.\n\n /* We could optimize and detect all retrieved candidates at once\n const candidateRecords = records.map(record => record.record);\n const recordsIsArray = Array.isArray(candidateRecords);\n debug(`records is an array: ${recordsIsArray}`);\n const result = detect(record, candidateRecords);\n debugData(`${JSON.stringify(result)}`);\n */\n\n if (candidate) {\n if (candidateNotInMatches(matches.concat(nonMatches), candidate)) {\n const {record: candidateRecord, id: candidateId} = candidate;\n const recordBExternal = {id: candidate.id, recordSource: 'databaseRecord', label: `db-${candidate.id}`};\n try {\n debug(`Running matchDetection for record ${candidateId} (${newRecordCount}/${recordSetSize})`);\n // we should handle errors from detection somehow - ie. cases where either record or candidateRecord errors\n const detectionResult = detect({recordA: record, recordB: candidateRecord, recordAExternal: recordExternal, recordBExternal});\n\n return handleDetectionResult(detectionResult, candidateId, candidateRecord);\n } catch (error) {\n debug(`MatchDetection errored: database record ${candidateId}: ${error}`);\n\n const matchError = {status: 422, payload: {message: `Matching errored for database record ${candidateId}. ${error.message}.`, id: candidateId}};\n const newRecordMatchErrors = recordMatchErrors.concat(matchError);\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount, recordMatchErrors: newRecordMatchErrors});\n }\n }\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount: recordDuplicateCount + 1, recordNonMatchCount, recordMatchErrors});\n }\n\n debug(`No more candidates, record set (${recordCount}/${recordSetSize}) done, ${recordMatches.length} matches found, ${recordDuplicateCount} candidates already handled, ${returnNonMatches ? `${recordNonMatches.length}` : `${recordNonMatchCount}`} nonMatches found.`);\n return {matches: recordMatches, nonMatches: returnNonMatches ? recordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: recordNonMatchCount, matchErrors: recordMatchErrors};\n\n function handleDetectionResult(detectionResult, candidateId, candidateRecord) {\n debugData(`MatchDetection results for ${candidateId} (${newRecordCount}/${recordSetSize}): ${JSON.stringify(detectionResult)}`);\n\n if (detectionResult.match || returnNonMatches) {\n debug(`${detectionResult.match ? `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is a match!` : `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is NOT a match!`}`);\n debugData(`Strategy: ${JSON.stringify(detectionResult.strategy)}, Threshold: ${JSON.stringify(detectionResult.threshold)}`);\n\n const matchResult = {\n probability: detectionResult.probability,\n candidate: {\n id: candidateId,\n record: candidateRecord\n }\n };\n const strategyResult = {\n strategy: detectionResult.strategy,\n threshold: detectionResult.threshold\n };\n const newMatch = returnStrategy ? {...matchResult, ...strategyResult} : {...matchResult};\n\n debugData(`${JSON.stringify(newMatch)}`);\n\n return handleRecordMatch(detectionResult.match, newMatch);\n }\n\n const newRecordNonMatchCount = recordNonMatchCount + 1;\n debugData(`- Total nonMatches after this detection: ${newRecordNonMatchCount}`);\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount, recordMatchErrors});\n }\n\n function handleRecordMatch(isMatch, newMatch) {\n const newRecordMatches = isMatch ? recordMatches.concat(newMatch) : recordMatches;\n const newRecordNonMatches = isMatch ? recordNonMatches : recordNonMatches.concat(newMatch);\n const newRecordNonMatchCount = isMatch ? recordNonMatchCount : recordNonMatchCount + 1;\n\n debugData(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`);\n\n if (returnNonMatches) {\n debugData(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);\n }\n debugData(`- Total nonMatchCount after this detection: ${recordNonMatchCount}`);\n\n if (maxMatchesFound({matches: matches.concat(newRecordMatches), maxMatches})) {\n debug(`MaxMatches (${maxMatches}) reached, handled candidates in record set: ${newRecordCount} non-handled candidates in record set ${recordSetSize - newRecordCount}`);\n return {matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors};\n }\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors});\n }\n\n function candidateNotInMatches(matches, candidate) {\n debug(`Checking that record ${candidate.id} is not already included in ${matches.length} matches/nonMatches`);\n const newCandidateId = candidate.id;\n debugData(`newCandidateId: ${newCandidateId}`);\n const result = matches.find(({candidate}) => candidate.id === newCandidateId);\n debugData(`Result: ${result}`);\n if (result) {\n debug(`${candidate.id} was already handled.`);\n return false;\n }\n debug(`${candidate.id} not found in matches/nonMatches`);\n return true;\n }\n }\n\n function maxMatchesFound({matches, maxMatches}) {\n if (maxMatches && matches.length >= maxMatches) {\n debug(`Stopping recordSet iteration: maxMatches (${maxMatches}) for matcher job found.`);\n return true;\n }\n }\n }\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import createDebugLogger from 'debug';\nimport createSearchInterface, * as candidateSearch from './candidate-search/index.js';\nimport createDetectionInterface, * as matchDetection from './match-detection/index.js';\n//import inspect from 'util';\n\nexport {candidateSearch, matchDetection};\n\nexport default ({detection: detectionOptions, search: searchOptions, maxMatches = 1, maxCandidates = 25, returnStrategy = false, returnQuery = false, returnNonMatches = false}) => {\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:index');\n const debugData = debug.extend('data');\n\n debugData(`DetectionOptions: ${JSON.stringify(detectionOptions)}`);\n debugData(`SearchOptions: ${JSON.stringify(searchOptions)}`);\n debugData(`MaxMatches: ${JSON.stringify(maxMatches)}`);\n debugData(`MaxCandidates: ${JSON.stringify(maxCandidates)}`);\n debugData(`ReturnStrategy: ${JSON.stringify(returnStrategy)}`);\n debugData(`ReturnQuery: ${JSON.stringify(returnQuery)}`);\n debugData(`ReturnNonMatches: ${JSON.stringify(returnNonMatches)}`);\n\n\n const detect = createDetectionInterface(detectionOptions, returnStrategy);\n\n return prepareSearch;\n\n async function prepareSearch({record, recordExternal = {recordSource: 'incomingRecord', label: 'ic'}}) {\n\n const {search} = await createSearchInterface({...searchOptions, record, maxCandidates, recordExternal});\n return iterate({});\n\n // candidateCount : amount of candidate records retrived from SRU for matching, NOT including current record set\n // matches : candidates that have been detected as matches by current matcher job\n // nonMatches : candidates that have been detected as non-matches by current matcher job (only if returnNonMatches is 'true')\n // duplicateCount : amount of candidate records that were retrieved from the SRU but not handled further because they were already found in the matches/nonMatches\n\n // state.totalRecords : amount of candidate records available to the current query (undefined, if there was no queries left)\n // state.query : current query (undefined if there was no queries left)\n // state.searchCounter : sequence for current search for current query (undefined, if there we no queries left)\n // state.queryCandidateCounter: amount of candidate records retrieved from SRU for matching for current query, including the current record set (undefined if there were no queries left)\n // state.queriesLeft : amount of queries left\n // state.queryCounter : sequence for current query\n // state.maxedQueries : queries that resulted in more than serverMaxResults hits\n\n async function iterate({initialState = {}, matches = [], candidateCount = 0, nonMatches = [], duplicateCount = 0, nonMatchCount = 0, matchErrors = []}) {\n debugData(`Starting next matcher iteration.`);\n const {records, ...state} = await search(initialState);\n\n debugData(`Current state: ${JSON.stringify(state)}, matches: ${matches.length}, candidateCount: ${candidateCount}, nonMatches: ${nonMatches.length}, nonMatchCount: ${nonMatchCount}, matchErrors: ${matchErrors.length}`);\n const recordSetSize = records.length;\n const newCandidateCount = candidateCount + recordSetSize;\n\n\n if (recordSetSize > 0) {\n return handleRecordSet();\n }\n\n if (state.queriesLeft > 0) {\n debug(`Empty record set ${state.searchCounter} for ${state.query}, but there are ${state.queriesLeft} queries left`);\n return iterate({initialState: state, matches, candidateCount: newCandidateCount, nonMatches, nonMatchCount, duplicateCount, matchErrors});\n }\n\n debug(`No (more) candidate records to check, no more queries left, matches: ${matches.length}`);\n return returnResult({matches, state, stopReason: '', nonMatches, nonMatchCount, candidateCount: newCandidateCount, duplicateCount, matchErrors});\n\n function handleRecordSet() {\n debug(`Checking record set of ${recordSetSize} candidate records for possible matches, found by ${state.searchCounter} search for ${state.query}`);\n\n const matchResult = iterateRecords({records, recordSetSize, maxMatches, matches, nonMatches, nonMatchCount});\n\n const newDuplicateCount = duplicateCount + matchResult.duplicateCount;\n const newNonMatchCount = nonMatchCount + matchResult.nonMatchCount;\n const {newMatches, newNonMatches, newMatchErrors} = handleMatchResult(matchResult, matches, nonMatches, matchErrors);\n\n if (maxMatchesFound({matches: newMatches, maxMatches})) {\n return returnResult({matches: newMatches, state, stopReason: 'maxMatches', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});\n }\n\n if (maxCandidatesRetrieved(newCandidateCount, maxCandidates)) {\n return returnResult({matches: newMatches, state, stopReason: 'maxCandidates', nonMatches: newNonMatches, duplicateCount: newDuplicateCount, candidateCount: newCandidateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});\n }\n\n return iterate({initialState: state, matches: newMatches, candidateCount: newCandidateCount, nonMatches: newNonMatches, duplicateCount: newDuplicateCount, nonMatchCount: newNonMatchCount, matchErrors: newMatchErrors});\n }\n\n function handleMatchResult(matchResult, matches, nonMatches, matchErrors) {\n debugData(`- Amount of new matches from record set: ${matchResult.matches.length}`);\n if (returnNonMatches) {\n debugData(`- Amount of new nonMatches from record set: ${matchResult.nonMatches.length}`);\n }\n\n const newMatches = matches.concat(returnQuery ? addQuery(matchResult.matches) : matchResult.matches);\n const newNonMatches = returnNonMatches ? nonMatches.concat(returnQuery ? addQuery(matchResult.nonMatches) : matchResult.nonMatches) : [];\n const newMatchErrors = matchErrors.concat(matchResult.matchErrors);\n\n debugData(`- Total amount of matches: ${newMatches.length}`);\n if (returnNonMatches) {\n debugData(`- Total amount of nonMatches: ${newNonMatches.length}`);\n }\n\n debugData(`MatchResult: ${JSON.stringify(matchResult)}`);\n debugData(`Old matchErrors: ${JSON.stringify(matchErrors)}, matchErrors from matchResult: ${JSON.stringify(matchResult.matchErrors)}, New matchErrors: ${JSON.stringify(newMatchErrors)}`);\n\n debugData(`- Total amount of matchErrors: ${newMatchErrors.length}`);\n\n return {newMatches, newNonMatches, newMatchErrors};\n }\n\n function addQuery(matches) {\n debugData(`Adding query ${state.query} to matches`);\n return matches.map((match) => ({...match, matchQuery: state.query}));\n }\n\n function maxCandidatesRetrieved(candidateCount, maxCandidates) {\n debugData(`Total amount of candidate records retrieved: ${newCandidateCount} (max: ${maxCandidates})`);\n if (maxCandidates && candidateCount >= maxCandidates) {\n debug(`Stopped matching because maximum number of candidate records ${candidateCount} / ${maxCandidates} have been retrieved`);\n return true;\n }\n }\n }\n\n // matches : array of matching candidate records\n // nonMatches : array of nonMatching candidate records (if returnNonMatches option is true, otherwise empty array)\n // - candidate.id\n // - candidate.record\n // - probability\n // - strategy (if returnStrategy option is true)\n // - threshold (if returnStrategy option is true)\n // - matchQuery (if returnQuery option is true)\n // failures: array of conversionFailures from candidate-search and matchErrors from matchDetection in error format {status, payload: {message, id}} if returnFailures is true\n\n // we could have here also returnRecords/returnMatchRecords/returnNonMatchRecord options that could be turned false for not to return actual record data\n\n // matchStatus.status: boolean, true if matcher retrieved and handled all found candidate records, false if it did not\n // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries','conversionFailures', empty string/undefined), reason for stopping retrieving or handling the candidate records\n // - only one stopReason is returned (if there would be several possible stopReasons, stopReason is picked in the above order)\n // - currently stopReason can be non-empty also in cases where status is true, if matcher hit the stop reason when handling the last available candidate record\n\n function returnResult({matches, state, stopReason, nonMatches, duplicateCount, candidateCount, nonMatchCount, matchErrors}) {\n const matchErrorCount = matchErrors.length;\n checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, matchErrorCount});\n const matchStatus = getMatchState(state, stopReason, matchErrorCount);\n // add nonMatches to result only if returnNonMatches is 'true', otherwise nonMatches have not been gathered\n const matchesResult = returnNonMatches ? {matches, matchStatus, nonMatches, candidateCount} : {matches, matchStatus, candidateCount};\n const result = matchesResult;\n debugData(`${JSON.stringify(result)}`);\n return result;\n\n // note that in cases where the matching has been stopped because of maxMatches checkCounts won't (in most cases) match\n\n function checkCounts({matches, nonMatches, candidateCount, duplicateCount, nonMatchCount, matchErrorCount}) {\n const matchCount = matches.length;\n debugData(`Return nonMatches: ${returnNonMatches}`);\n const chosenNonMatchCount = returnNonMatches ? nonMatches.length : nonMatchCount;\n const totalHandled = matchCount + chosenNonMatchCount + duplicateCount;\n debug(`candidateCount: ${candidateCount}, matches: ${matchCount}, nonMatches: ${chosenNonMatchCount}, duplicateCount: ${duplicateCount}, matchErrorCount: ${matchErrorCount}`);\n debug(`We got result for ${totalHandled} / ${candidateCount} retrieved candidates`);\n if (totalHandled !== candidateCount) {\n debug(`WARNING: Missing results for ${candidateCount - totalHandled} candidates`);\n return;\n }\n return;\n }\n\n function getMatchState(state, stopReason, matchErrorCount) {\n debugData(`${JSON.stringify(state)}`);\n debug(`We had ${matchErrorCount} retrieved candidates that errored in matchDetection.`);\n debug(`Queries left ${state.queriesLeft}, Searches for current query left: ${state.resultSetOffset && state.resultSetOffset <= state.totalRecords}, non-retrieved records: ${state.totalRecords - state.queryCandidateCounter}, maxedQueries (${state.maxedQueries.length}): ${state.maxedQueries}`);\n\n debugData(`StopReason: <${stopReason}>`);\n\n const searchesLeft = state.resultSetOffset && state.resultSetOffset <= state.totalRecords;\n const nonRetrieved = searchesLeft ? state.totalRecords - state.queryCandidateCounter : 0;\n debugData(`nonRetrieved: ${nonRetrieved}`);\n\n // matchStatus.stopReason: string ('maxMatches','maxCandidates','maxedQueries', empty string/undefined), reason for stopping retrieving or handling the candidate records\n // 'maxMatches' and 'maxCandidates' are in stopReason, 'maxedQueries', 'conversionFailures' and 'matchErrors' are created here\n\n if (state.queriesLeft > 0 || nonRetrieved > 0 || state.maxedQueries.length > 0 || matchErrorCount > 0) {\n const maxedQueriesStopReason = state.maxedQueries.length > 0 ? 'maxedQueries' : undefined;\n const matchErrorsStopReason = matchErrorCount > 0 ? 'matchErrors' : undefined;\n const newStopReason = stopReason === '' || stopReason === undefined ? maxedQueriesStopReason || matchErrorsStopReason : stopReason;\n debugData(`MaxedQueriesStopReason: <${maxedQueriesStopReason}>`);\n debugData(`MatchErrorsStopReason <${matchErrorsStopReason}>`);\n debugData(`NewStopReason: <${newStopReason}>`);\n debug(`Match status: false`);\n return {status: false, stopReason: newStopReason};\n }\n\n debug(`Match status: true`);\n return {status: true, stopReason};\n }\n }\n\n // NOTES:\n // - we could optimize by creating the featureSet for the incoming record once and using it for all database/candidateRecords\n // - if creating the featureSet for the incoming record fails we have an unprocessable entity\n // - if creating the featureSet for a candidate record fails we could skip that candidate - but list the case as a detectionFailure, same as conversionFailures\n\n function iterateRecords({records, recordSetSize, maxMatches, matches = [], nonMatches = [], recordMatches = [], recordNonMatches = [], recordCount = 0, recordDuplicateCount = 0, recordNonMatchCount = 0, recordMatchErrors = []}) {\n\n // recordSetSize : total amount of records in the current record set\n // recordCount : amount of records from the current record set that have been handled\n // maxMatches : setting for maximum amount found by current matcher job before the matcher job is stopped\n // recordDuplicateCount : amount of records from the current record set that are already included in matches/nonMatches results\n // recordNonMatchCount: amount of records from the current record set that are nonMatches (only is returnNonMatches setting is false)\n\n // records : non-handled records in the current record set\n // matches : found matches in the current matcher job\n // recordMatches : found matches in the current record set\n // recordNonMatches : found nonMatches in the current record set (only if returnNonMatches setting is true)\n // recordMatchErrors: errored matchDetection in the current record set\n\n const [candidate] = records;\n const newRecordCount = candidate ? recordCount + 1 : recordCount;\n\n // The matcher uses same matchDetection strategy for candidates from all candidate-searches -> matchDetection result for the same candidate is always same\n // Exceptions would happen if the candidate would have been updated in the database between candidate searches\n // Note that if returnNonMatches is false, matcher won't remember candidates that didn't match, so they will be matched again everytime they are retrieved by\n // different candidate search queries. Same candidate search query won't have duplicate records.\n\n /* We could optimize and detect all retrieved candidates at once\n const candidateRecords = records.map(record => record.record);\n const recordsIsArray = Array.isArray(candidateRecords);\n debug(`records is an array: ${recordsIsArray}`);\n const result = detect(record, candidateRecords);\n debugData(`${JSON.stringify(result)}`);\n */\n\n if (candidate) {\n if (candidateNotInMatches(matches.concat(nonMatches), candidate)) {\n const {record: candidateRecord, id: candidateId} = candidate;\n const recordBExternal = {id: candidate.id, recordSource: 'databaseRecord', label: `db-${candidate.id}`};\n try {\n debug(`Running matchDetection for record ${candidateId} (${newRecordCount}/${recordSetSize})`);\n // we should handle errors from detection somehow - ie. cases where either record or candidateRecord errors\n const detectionResult = detect({recordA: record, recordB: candidateRecord, recordAExternal: recordExternal, recordBExternal});\n\n return handleDetectionResult(detectionResult, candidateId, candidateRecord);\n } catch (error) {\n debug(`MatchDetection errored: database record ${candidateId}: ${error}`);\n\n const matchError = {status: 422, payload: {message: `Matching errored for database record ${candidateId}. ${error.message}.`, id: candidateId}};\n const newRecordMatchErrors = recordMatchErrors.concat(matchError);\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount, recordMatchErrors: newRecordMatchErrors});\n }\n }\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount: recordDuplicateCount + 1, recordNonMatchCount, recordMatchErrors});\n }\n\n debug(`No more candidates, record set (${recordCount}/${recordSetSize}) done, ${recordMatches.length} matches found, ${recordDuplicateCount} candidates already handled, ${returnNonMatches ? `${recordNonMatches.length}` : `${recordNonMatchCount}`} nonMatches found.`);\n return {matches: recordMatches, nonMatches: returnNonMatches ? recordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: recordNonMatchCount, matchErrors: recordMatchErrors};\n\n function handleDetectionResult(detectionResult, candidateId, candidateRecord) {\n debugData(`MatchDetection results for ${candidateId} (${newRecordCount}/${recordSetSize}): ${JSON.stringify(detectionResult)}`);\n\n if (detectionResult.match || returnNonMatches) {\n debug(`${detectionResult.match ? `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is a match!` : `Record ${candidateId} (${newRecordCount}/${recordSetSize}) is NOT a match!`}`);\n debugData(`Strategy: ${JSON.stringify(detectionResult.strategy)}, Threshold: ${JSON.stringify(detectionResult.threshold)}`);\n\n const matchResult = {\n probability: Math.round(detectionResult.probability * 100)/100,\n candidate: {\n id: candidateId,\n record: candidateRecord\n }\n };\n const strategyResult = {\n strategy: detectionResult.strategy,\n threshold: detectionResult.threshold\n };\n const newMatch = returnStrategy ? {...matchResult, ...strategyResult} : {...matchResult};\n\n debugData(`${JSON.stringify(newMatch)}`);\n\n return handleRecordMatch(detectionResult.match, newMatch);\n }\n\n const newRecordNonMatchCount = recordNonMatchCount + 1;\n debugData(`- Total nonMatches after this detection: ${newRecordNonMatchCount}`);\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches, recordCount: newRecordCount, recordNonMatches, recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount, recordMatchErrors});\n }\n\n function handleRecordMatch(isMatch, newMatch) {\n const newRecordMatches = isMatch ? recordMatches.concat(newMatch) : recordMatches;\n const newRecordNonMatches = isMatch ? recordNonMatches : recordNonMatches.concat(newMatch);\n const newRecordNonMatchCount = isMatch ? recordNonMatchCount : recordNonMatchCount + 1;\n\n debugData(`- Total matches after this detection: ${matches.concat(newRecordMatches).length} (max: ${maxMatches})`);\n\n if (returnNonMatches) {\n debugData(`- Total nonMatches after this detection: ${nonMatches.concat(newRecordNonMatches).length}`);\n }\n debugData(`- Total nonMatchCount after this detection: ${recordNonMatchCount}`);\n\n if (maxMatchesFound({matches: matches.concat(newRecordMatches), maxMatches})) {\n debug(`MaxMatches (${maxMatches}) reached, handled candidates in record set: ${newRecordCount} non-handled candidates in record set ${recordSetSize - newRecordCount}`);\n return {matches: newRecordMatches, nonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, nonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors};\n }\n\n return iterateRecords({records: records.slice(1), recordSetSize, maxMatches, matches, recordMatches: newRecordMatches, recordCount: newRecordCount, recordNonMatches: returnNonMatches ? newRecordNonMatches : [], duplicateCount: recordDuplicateCount, recordNonMatchCount: newRecordNonMatchCount, matchErrors: recordMatchErrors});\n }\n\n function candidateNotInMatches(matches, candidate) {\n debug(`Checking that record ${candidate.id} is not already included in ${matches.length} matches/nonMatches`);\n const newCandidateId = candidate.id;\n debugData(`newCandidateId: ${newCandidateId}`);\n const result = matches.find(({candidate}) => candidate.id === newCandidateId);\n debugData(`Result: ${result}`);\n if (result) {\n debug(`${candidate.id} was already handled.`);\n return false;\n }\n debug(`${candidate.id} not found in matches/nonMatches`);\n return true;\n }\n }\n\n function maxMatchesFound({matches, maxMatches}) {\n if (maxMatches && matches.length >= maxMatches) {\n debug(`Stopping recordSet iteration: maxMatches (${maxMatches}) for matcher job found.`);\n return true;\n }\n }\n }\n};\n"],
5
+ "mappings": "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",
6
6
  "names": ["matches", "nonMatches", "matchErrors", "candidateCount", "maxCandidates", "duplicateCount", "nonMatchCount", "matchErrorCount", "state", "stopReason", "maxMatches", "candidate"]
7
7
  }
@@ -1,8 +1,8 @@
1
1
  import createDebugLogger from "debug";
2
2
  import { isComponentRecord } from "@natlibfi/melinda-commons";
3
- import { uniqArray } from "./issn.js";
4
3
  import { parse773g } from "../../../candidate-search/query-list/component.js";
5
- const debug = createDebugLogger("@natlibfi/melinda-record-matching:match-detection:features:issn");
4
+ import { uniqArray } from "@natlibfi/marc-record-validators-melinda/dist/utils.js";
5
+ const debug = createDebugLogger("@natlibfi/melinda-record-matching:match-detection:features:f773");
6
6
  const debugData = debug.extend("data");
7
7
  const MAX_IDENTIFIER = 0.1;
8
8
  const MAX_G = 0.5;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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"],
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 {parse773g} from '../../../candidate-search/query-list/component.js';\nimport {uniqArray} from '@natlibfi/marc-record-validators-melinda/dist/utils.js';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:f773');\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
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
6
  "names": []
7
7
  }
@@ -1,6 +1,7 @@
1
1
  export { default as hostComponent } from "./host-component.js";
2
2
  export { default as isbn } from "./isbn.js";
3
3
  export { default as issn } from "./issn.js";
4
+ export { default as publisherNumber } from "./publisherNumber.js";
4
5
  export { default as sid } from "./sid.js";
5
6
  export { default as otherStandardIdentifier } from "./other-standard-identifier.js";
6
7
  export { default as title } from "./title.js";
@@ -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 sid} from './sid.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,WAAU;AAC7B,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;",
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 publisherNumber} from './publisherNumber.js'; // 028$b$a\nexport {default as sid} from './sid.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,uBAAsB;AACzC,SAAQ,WAAW,WAAU;AAC7B,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
  }
@@ -1,6 +1,6 @@
1
1
  import createDebugLogger from "debug";
2
2
  import { parse as isbnParse } from "isbn3";
3
- import { uniqArray } from "./issn.js";
3
+ import { uniqArray } from "@natlibfi/marc-record-validators-melinda/dist/utils.js";
4
4
  const debug = createDebugLogger(`@natlibfi/melinda-record-matching:match-detection:features:standard-identifiers:ISBN`);
5
5
  const debugData = debug.extend("data");
6
6
  const MAX_SCORE = 0.75;
@@ -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';\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\nconst setRegexp = /(?:complete set|hela verket|helhet|koko kartasto|koko paketti|koko sarja|koko setti|koko teoksen isbn|koko teos|kokonaisuus|^set\\b|\\bset$|\\bsetti\\b|vol.*set)/ui;\n\nexport default () => ({\n name: 'ISBN',\n extract: ({record/*, recordExternal*/}) => {\n //const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n\n const all = record.get('020').filter(f => f.subfields?.some(sf => ['a', 'z'].includes(sf.code) && sf.value));\n const sets = all.filter(f => isSetIsbnField(f));\n const noSets = all.filter(f => !isSetIsbnField(f));\n if (sets.length > 0 && noSets.length > 0) {\n return noSets;\n }\n return all;\n\n function isSetIsbnField(field) {\n return field.subfields.some(sf => sf.code === 'q' && setRegexp.test(sf.value));\n }\n },\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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",
4
+ "sourcesContent": ["import createDebugLogger from 'debug';\nimport {parse as isbnParse} from 'isbn3';\nimport {uniqArray} from '@natlibfi/marc-record-validators-melinda/dist/utils.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\nconst setRegexp = /(?:complete set|hela verket|helhet|koko kartasto|koko paketti|koko sarja|koko setti|koko teoksen isbn|koko teos|kokonaisuus|^set\\b|\\bset$|\\bsetti\\b|vol.*set)/ui;\n\nexport default () => ({\n name: 'ISBN',\n extract: ({record/*, recordExternal*/}) => {\n //const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n\n const all = record.get('020').filter(f => f.subfields?.some(sf => ['a', 'z'].includes(sf.code) && sf.value));\n const sets = all.filter(f => isSetIsbnField(f));\n const noSets = all.filter(f => !isSetIsbnField(f));\n if (sets.length > 0 && noSets.length > 0) {\n return noSets;\n }\n return all;\n\n function isSetIsbnField(field) {\n return field.subfields.some(sf => sf.code === 'q' && setRegexp.test(sf.value));\n }\n },\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": "AAAA,OAAO,uBAAuB;AAC9B,SAAQ,SAAS,iBAAgB;AACjC,SAAQ,iBAAgB;AAExB,MAAM,QAAQ,kBAAkB,sFAAsF;AACtH,MAAM,YAAY,MAAM,OAAO,MAAM;AAErC,MAAM,YAAY;AAElB,MAAM,YAAY;AAElB,eAAe,OAAO;AAAA,EACpB,MAAM;AAAA,EACN,SAAS,CAAC;AAAA,IAAC;AAAA;AAAA,EAA0B,MAAM;AAGzC,UAAM,MAAM,OAAO,IAAI,KAAK,EAAE,OAAO,OAAK,EAAE,WAAW,KAAK,QAAM,CAAC,KAAK,GAAG,EAAE,SAAS,GAAG,IAAI,KAAK,GAAG,KAAK,CAAC;AAC3G,UAAM,OAAO,IAAI,OAAO,OAAK,eAAe,CAAC,CAAC;AAC9C,UAAM,SAAS,IAAI,OAAO,OAAK,CAAC,eAAe,CAAC,CAAC;AACjD,QAAI,KAAK,SAAS,KAAK,OAAO,SAAS,GAAG;AACxC,aAAO;AAAA,IACT;AACA,WAAO;AAEP,aAAS,eAAe,OAAO;AAC7B,aAAO,MAAM,UAAU,KAAK,QAAM,GAAG,SAAS,OAAO,UAAU,KAAK,GAAG,KAAK,CAAC;AAAA,IAC/E;AAAA,EACF;AAAA,EACA,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;AAEA,UAAM,4BAA4B;AAClC,UAAM,2BAA2B;AAEjC,UAAM,CAAC,eAAe,iBAAiB,eAAe,eAAe,IAAI,iBAAiB,IAAI,IAAI;AAClG,UAAM,CAAC,eAAe,iBAAiB,eAAe,eAAe,IAAI,iBAAiB,IAAI,IAAI;AAElG,aAAS,iBAAiB,MAAM,QAAQ;AACtC,YAAM,CAAC,cAAc,gBAAgB,cAAc,cAAc,IAAI,UAAU,IAAI;AACnF,UAAI,aAAa,QAAQ;AACvB,cAAM,GAAG,MAAM,KAAK,GAAG,CAAC,EAAE,GAAG,IAAI,yBAAyB,WAAW,aAAa,KAAK,IAAI,CAAC,EAAE;AAAA,MAChG;AACA,UAAI,eAAe,QAAQ;AACzB,cAAM,GAAG,MAAM,KAAK,GAAG,CAAC,EAAE,GAAG,IAAI,yBAAyB,aAAa,eAAe,KAAK,IAAI,CAAC,EAAE;AAAA,MACpG;AACA,UAAI,aAAa,QAAQ;AACvB,cAAM,GAAG,MAAM,KAAK,GAAG,CAAC,EAAE,GAAG,IAAI,wBAAwB,WAAW,aAAa,KAAK,IAAI,CAAC,EAAE;AAAA,MAC/F;AACA,UAAI,eAAe,QAAQ;AACzB,cAAM,GAAG,MAAM,KAAK,GAAG,CAAC,EAAE,GAAG,IAAI,wBAAwB,aAAa,eAAe,KAAK,IAAI,CAAC,EAAE;AAAA,MACnG;AACA,aAAO,CAAC,cAAc,gBAAgB,cAAc,cAAc;AAAA,IACpE;AAEA,UAAM,CAAC,kBAAkB,iBAAiB,eAAe,IAAI,aAAa,eAAe,aAAa;AAEtG,UAAM,cAAe,iBAAiB,MAAM,aAAa,gBAAgB,MAAM,aAAa,gBAAgB,MAAM,EAAE;AAEpH,QAAI,iBAAiB,SAAS,GAAG;AAE/B,aAAO,UAAU,WAAW,KAAK,gBAAgB,SAAS,gBAAgB,MAAM;AAAA,IAClF;AAEA,UAAM,WAAW,mBAAmB;AAEpC,aAAS,qBAAqB;AAE5B,UAAI,cAAc,KAAK,UAAQ,cAAc,SAAS,IAAI,CAAC,KAAK,cAAc,KAAK,UAAQ,cAAc,SAAS,IAAI,CAAC,GAAG;AACxH,eAAO;AAAA,MACT;AAIA,YAAM,UAAU,cAAc,KAAK,UAAQ,cAAc,SAAS,IAAI,CAAC;AACvE,UAAI,SAAS;AACX,cAAM,wCAAwC,OAAO,EAAE;AACvD,eAAO,YAAY,IAAI;AAAA,MACzB;AAEA,YAAM,YAAY,gBAAgB,KAAK,UAAQ,gBAAgB,SAAS,IAAI,KAAK,gBAAgB,SAAS,IAAI,CAAC,KAAK,gBAAgB,KAAK,UAAQ,gBAAgB,SAAS,IAAI,CAAC;AAC/K,UAAI,WAAW;AACb,cAAM,4DAA4D,SAAS,EAAE;AAC7E,eAAO,YAAY,IAAI;AAAA,MACzB;AAUA,aAAO;AAAA,IACT;AAIA,QAAI,aAAa,WAAW;AAE1B,aAAO,UAAU,UAAU,KAAK,gBAAgB,SAAS,gBAAgB,SAAS,CAAC;AAAA,IACrF;AAEA,QAAI,WAAW,GAAG;AAEhB,aAAO,UAAU,UAAU,KAAK,gBAAgB,SAAS,gBAAgB,MAAM;AAAA,IACjF;AAIA,QAAI,cAAc,SAAS,gBAAgB,WAAW,KAAK,cAAc,SAAS,gBAAgB,WAAW,GAAG;AAC9G,aAAO;AAAA,IACT;AAEA,UAAM,OAAO,CAAC,GAAG,eAAe,GAAG,iBAAiB,GAAG,eAAe,GAAG,eAAe;AACxF,UAAM,OAAO,CAAC,GAAG,eAAe,GAAG,iBAAiB,GAAG,eAAe,GAAG,eAAe;AACxF,UAAM,CAAC,cAAc,MAAM,IAAI,IAAI,aAAa,MAAM,IAAI;AAC1D,UAAM,kBAAmB,iBAAiB,MAAM,aAAa,KAAK,MAAM,aAAa,KAAK,MAAM,EAAE;AAElG,QAAI,aAAa,SAAS,GAAG;AAC3B,aAAO;AAAA,IACT;AAEA,WAAO;AAEP,aAAS,UAAU,QAAQ;AAEzB,YAAM,aAAa,OAAO,QAAQ,OAAK,EAAE,UAAU,OAAO,QAAM,GAAG,SAAS,yBAAyB,CAAC,EAAE,IAAI,QAAM,uBAAuB,GAAG,KAAK,CAAC;AAClJ,YAAM,iBAAiB,WAAW,OAAO,SAAO,IAAI,KAAK,EAAE,IAAI,SAAO,IAAI,KAAK;AAC/E,YAAM,oBAAoB,WAAW,OAAO,SAAO,CAAC,IAAI,KAAK,EAAE,IAAI,SAAO,IAAI,KAAK;AACnF,UAAI,CAAC,0BAA0B;AAC7B,eAAO,CAAC,gBAAgB,mBAAmB,CAAC,GAAG,CAAC,CAAC;AAAA,MACnD;AACA,YAAM,YAAY,OAAO,QAAQ,OAAK,EAAE,UAAU,OAAO,QAAM,GAAG,SAAS,wBAAwB,CAAC,EAAE,IAAI,QAAM,uBAAuB,GAAG,KAAK,CAAC;AAChJ,YAAM,iBAAiB,UAAU,OAAO,SAAO,IAAI,KAAK,EAAE,IAAI,SAAO,IAAI,KAAK;AAC9E,YAAM,mBAAmB,UAAU,OAAO,SAAO,CAAC,IAAI,KAAK,EAAE,IAAI,SAAO,IAAI,KAAK;AAEjF,aAAO,CAAC,UAAU,cAAc,GAAG,UAAU,iBAAiB,GAAG,UAAU,cAAc,GAAG,UAAU,gBAAgB,CAAC;AAAA,IACzH;AAEA,aAAS,uBAAuB,QAAQ;AACtC,YAAM,UAAU,OAAO,QAAQ,UAAU,EAAE;AAG3C,YAAM,UAAU,QAAQ,QAAQ,SAAS,EAAE;AAC3C,UAAI,YAAY,SAAS;AACvB,cAAM,YAAY,uBAAuB,OAAO;AAChD,YAAI,UAAU,OAAO;AACnB,iBAAO;AAAA,QACT;AAAA,MACF;AAEA,YAAM,kBAAkB,UAAU,SAAS,EAAE,KAAK;AAClD,gBAAU,oBAAoB,KAAK,UAAU,eAAe,CAAC,EAAE;AAC/D,UAAI,CAAC,gBAAgB,SAAS;AAC5B,cAAM,uBAAuB,OAAO,0BAA0B;AAC9D,eAAO,EAAC,OAAO,OAAO,OAAO,QAAQ,QAAQ,OAAO,EAAE,EAAC;AAAA,MACzD;AAEA,YAAM,mBAAmB,OAAO,8BAA8B,gBAAgB,MAAM,GAAG;AACvF,aAAO,EAAC,OAAO,MAAM,OAAO,gBAAgB,OAAM;AAAA,IAGpD;AAAA,EACF;AACF;AAIA,SAAS,aAAa,MAAM,MAAM;AAChC,QAAM,SAAS,KAAK,OAAO,SAAO,KAAK,SAAS,GAAG,CAAC;AACpD,QAAM,aAAa,KAAK,OAAO,SAAO,CAAC,OAAO,SAAS,GAAG,CAAC;AAC3D,QAAM,aAAa,KAAK,OAAO,SAAO,CAAC,OAAO,SAAS,GAAG,CAAC;AAC3D,SAAO,CAAC,QAAQ,YAAY,UAAU;AACxC;AAEA,SAAS,UAAU,OAAO,QAAQ,GAAG;AACnC,SAAO,eAAe,OAAO,CAAC;AAC9B,WAAS,eAAe,WAAW,WAAW;AAC5C,QAAI,YAAY,GAAG;AACjB,aAAO,eAAe,YAAY,QAAQ,YAAU,CAAC;AAAA,IACvD;AACA,WAAO,KAAK,MAAM,YAAY,GAAG,IAAE;AAAA,EACrC;AACF;",
6
6
  "names": []
7
7
  }
@@ -1,3 +1,4 @@
1
+ import { uniqArray } from "@natlibfi/marc-record-validators-melinda/dist/utils.js";
1
2
  import createDebugLogger from "debug";
2
3
  const debug = createDebugLogger("@natlibfi/melinda-record-matching:match-detection:features:issn");
3
4
  const debugData = debug.extend("data");
@@ -42,7 +43,4 @@ export default () => ({
42
43
  return -0.2;
43
44
  }
44
45
  });
45
- export function uniqArray(arr) {
46
- return arr.filter((val, i) => arr.indexOf(val) === i);
47
- }
48
46
  //# sourceMappingURL=issn.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/match-detection/features/bib/issn.js"],
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 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,EACA,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 {uniqArray} from '@natlibfi/marc-record-validators-melinda/dist/utils.js';\nimport 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 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"],
5
+ "mappings": "AAAA,SAAQ,iBAAgB;AACxB,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,EACA,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;",
6
6
  "names": []
7
7
  }
@@ -0,0 +1,61 @@
1
+ import { uniqArray } from "@natlibfi/marc-record-validators-melinda/dist/utils.js";
2
+ import createDebugLogger from "debug";
3
+ const debug = createDebugLogger("@natlibfi/melinda-record-matching:match-detection:features:publisher-number");
4
+ const debugData = debug.extend("data");
5
+ export default () => ({
6
+ name: "Publisher or Distributor Number",
7
+ extract: ({
8
+ record
9
+ /*, recordExternal*/
10
+ }) => {
11
+ return getIdentifiers();
12
+ function getIdentifiers() {
13
+ const values = fieldsToPublisherNumber(record.get(/^028$/u));
14
+ debug(` ${values.length} potential publisher identifiers: ${values.join(", ")}`);
15
+ return uniqArray(values);
16
+ function fieldsToPublisherNumber(fields, result = []) {
17
+ const [field, ...remainingFields] = fields;
18
+ if (!field) {
19
+ return result;
20
+ }
21
+ const a = field.subfields.find((sf) => sf.code === "a");
22
+ if (!a || !a.value) {
23
+ return fieldsToPublisherNumber(remainingFields, result);
24
+ }
25
+ const aval = normalizePublisherNumber(`${a.value}`);
26
+ const b = field.subfields.find((sf) => sf.code === "b");
27
+ if (!b || !b.value) {
28
+ return fieldsToPublisherNumber(remainingFields, [...result, aval]);
29
+ }
30
+ const baval = normalizePublisherNumber(`${b.value}${a.value}`);
31
+ return fieldsToPublisherNumber(remainingFields, [...result, baval, aval]);
32
+ }
33
+ function normalizePublisherNumber(val) {
34
+ return val.replace(/[- .,:;]/ug, "");
35
+ }
36
+ }
37
+ },
38
+ compare: (aa, bb) => {
39
+ debugData(`Comparing identifier sets ${JSON.stringify(aa)} and ${JSON.stringify(bb)}`);
40
+ if (aa.length === 0 || bb.length === 0) {
41
+ return 0;
42
+ }
43
+ const sharedIdentifiers = aa.filter((val) => bb.includes(val));
44
+ if (sharedIdentifiers.length > 0) {
45
+ const goodMatch = sharedIdentifiers.find((identifier) => identifier.match(/[^0-9]/u));
46
+ if (goodMatch) {
47
+ debug(` Shared identifier found: '${goodMatch}'`);
48
+ return 0.5;
49
+ }
50
+ debug(` Shared identifier (numbers only) found: '${sharedIdentifiers[0]}'`);
51
+ return 0.2;
52
+ }
53
+ const aa2 = aa.map((a) => a.replace(/[^0-9]/ug, ""));
54
+ const bb2 = bb.map((b) => b.replace(/[^0-9]/ug, ""));
55
+ if (aa2.find((val) => val !== "" && bb2.includes(val))) {
56
+ return 0;
57
+ }
58
+ return -0.2;
59
+ }
60
+ });
61
+ //# sourceMappingURL=publisherNumber.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/match-detection/features/bib/publisherNumber.js"],
4
+ "sourcesContent": ["// Compare publusher number (028$b$a or 028$a)\nimport {uniqArray} from '@natlibfi/marc-record-validators-melinda/dist/utils.js';\nimport createDebugLogger from 'debug';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:publisher-number');\nconst debugData = debug.extend('data');\n\nexport default () => ({\n name: 'Publisher or Distributor Number',\n extract: ({record/*, recordExternal*/}) => {\n //const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n\n return getIdentifiers();\n\n function getIdentifiers() {\n const values = fieldsToPublisherNumber(record.get(/^028$/u));\n\n debug(`\\t ${values.length} potential publisher identifiers: ${values.join(', ')}`);\n\n return uniqArray(values);\n\n function fieldsToPublisherNumber(fields, result = []) {\n const [field, ...remainingFields] = fields;\n if (!field) {\n return result;\n }\n const a = field.subfields.find(sf => sf.code === 'a');\n if (!a || !a.value) {\n return fieldsToPublisherNumber(remainingFields, result);\n }\n const aval = normalizePublisherNumber(`${a.value}`);\n\n const b = field.subfields.find(sf => sf.code === 'b');\n if (!b || !b.value) {\n return fieldsToPublisherNumber(remainingFields, [...result, aval]);\n }\n const baval = normalizePublisherNumber(`${b.value}${a.value}`);\n return fieldsToPublisherNumber(remainingFields, [...result, baval, aval]);\n }\n\n function normalizePublisherNumber(val) {\n return val.replace(/[- .,:;]/ug, '');\n }\n }\n },\n compare: (aa, bb) => {\n debugData(`Comparing identifier 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 sharedIdentifiers = aa.filter(val => bb.includes(val));\n if (sharedIdentifiers.length > 0) {\n const goodMatch = sharedIdentifiers.find(identifier => identifier.match(/[^0-9]/u));\n if (goodMatch) {\n debug(`\\tShared identifier found: '${goodMatch}'`);\n return 0.5;\n }\n debug(`\\tShared identifier (numbers only) found: '${sharedIdentifiers[0]}'`);\n return 0.2;\n }\n\n const aa2 = aa.map(a => a.replace(/[^0-9]/ug, ''));\n const bb2 = bb.map(b => b.replace(/[^0-9]/ug, ''));\n\n // Numbers matches (aka don't penalize \"Fazer F-123\" vs \"F-123\")\n // Note that FOOLP-123 vs FOOCD-123 will now also return 0.0. Still nothing positive is returned, so no damage done.\n if (aa2.find(val => val !== '' && bb2.includes(val))) {\n return 0.0;\n }\n\n return -0.2;\n\n }\n});\n\n"],
5
+ "mappings": "AACA,SAAQ,iBAAgB;AACxB,OAAO,uBAAuB;AAE9B,MAAM,QAAQ,kBAAkB,6EAA6E;AAC7G,MAAM,YAAY,MAAM,OAAO,MAAM;AAErC,eAAe,OAAO;AAAA,EACpB,MAAM;AAAA,EACN,SAAS,CAAC;AAAA,IAAC;AAAA;AAAA,EAA0B,MAAM;AAGzC,WAAO,eAAe;AAEtB,aAAS,iBAAiB;AACxB,YAAM,SAAS,wBAAwB,OAAO,IAAI,QAAQ,CAAC;AAE3D,YAAM,KAAM,OAAO,MAAM,qCAAqC,OAAO,KAAK,IAAI,CAAC,EAAE;AAEjF,aAAO,UAAU,MAAM;AAEvB,eAAS,wBAAwB,QAAQ,SAAS,CAAC,GAAG;AACpD,cAAM,CAAC,OAAO,GAAG,eAAe,IAAI;AACpC,YAAI,CAAC,OAAO;AACV,iBAAO;AAAA,QACT;AACA,cAAM,IAAI,MAAM,UAAU,KAAK,QAAM,GAAG,SAAS,GAAG;AACpD,YAAI,CAAC,KAAK,CAAC,EAAE,OAAO;AAClB,iBAAO,wBAAwB,iBAAiB,MAAM;AAAA,QACxD;AACA,cAAM,OAAO,yBAAyB,GAAG,EAAE,KAAK,EAAE;AAElD,cAAM,IAAI,MAAM,UAAU,KAAK,QAAM,GAAG,SAAS,GAAG;AACpD,YAAI,CAAC,KAAK,CAAC,EAAE,OAAO;AAClB,iBAAO,wBAAwB,iBAAiB,CAAC,GAAG,QAAQ,IAAI,CAAC;AAAA,QACnE;AACA,cAAM,QAAQ,yBAAyB,GAAG,EAAE,KAAK,GAAG,EAAE,KAAK,EAAE;AAC7D,eAAO,wBAAwB,iBAAiB,CAAC,GAAG,QAAQ,OAAO,IAAI,CAAC;AAAA,MAC1E;AAEA,eAAS,yBAAyB,KAAK;AACrC,eAAO,IAAI,QAAQ,cAAc,EAAE;AAAA,MACrC;AAAA,IACF;AAAA,EACF;AAAA,EACA,SAAS,CAAC,IAAI,OAAO;AACnB,cAAU,6BAA6B,KAAK,UAAU,EAAE,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC,EAAE;AACrF,QAAI,GAAG,WAAW,KAAK,GAAG,WAAW,GAAG;AAEtC,aAAO;AAAA,IACT;AACA,UAAM,oBAAoB,GAAG,OAAO,SAAO,GAAG,SAAS,GAAG,CAAC;AAC3D,QAAI,kBAAkB,SAAS,GAAG;AAChC,YAAM,YAAY,kBAAkB,KAAK,gBAAc,WAAW,MAAM,SAAS,CAAC;AAClF,UAAI,WAAW;AACb,cAAM,8BAA+B,SAAS,GAAG;AACjD,eAAO;AAAA,MACT;AACA,YAAM,6CAA8C,kBAAkB,CAAC,CAAC,GAAG;AAC3E,aAAO;AAAA,IACT;AAEA,UAAM,MAAM,GAAG,IAAI,OAAK,EAAE,QAAQ,YAAY,EAAE,CAAC;AACjD,UAAM,MAAM,GAAG,IAAI,OAAK,EAAE,QAAQ,YAAY,EAAE,CAAC;AAIjD,QAAI,IAAI,KAAK,SAAO,QAAQ,MAAM,IAAI,SAAS,GAAG,CAAC,GAAG;AACpD,aAAO;AAAA,IACT;AAEA,WAAO;AAAA,EAET;AACF;",
6
+ "names": []
7
+ }
@@ -26,7 +26,7 @@ B: ${recordB}`);
26
26
  const featurePairs = generateFeaturePairs(featuresA2, featuresB);
27
27
  const similarityVector = generateSimilarityVector(featurePairs);
28
28
  if (similarityVector.some((v) => v >= MIN_PROPABILITY_QUALIFIER)) {
29
- const probability = calculateProbability(similarityVector);
29
+ const probability = Math.round(calculateProbability(similarityVector) * 100) / 100;
30
30
  debug(`probability: ${probability} (Threshold: ${threshold})`);
31
31
  if (similarityVector.some((v) => v <= FORCE_FAIL)) {
32
32
  debug(`Some feature resulted in utter failure (${FORCE_FAIL} or less)`);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/match-detection/index.js"],
4
- "sourcesContent": ["\nimport createDebugLogger from 'debug';\nimport * as features from './features/index.js';\n\nexport {features};\n\nexport default ({strategy, threshold = 0.8999}, returnStrategy = false, localSettings = {}) => ({recordA, recordB, recordAExternal = {}, recordBExternal = {}}) => {\n const MIN_PROPABILITY_QUALIFIER = 0.4; // Title perfect match: 0.5\n const FORCE_FAIL = -1.0;\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection');\n const debugData = debug.extend('data');\n\n debugData(`Strategy: ${JSON.stringify(strategy)}, Threshold: ${JSON.stringify(threshold)}, ReturnStrategy: ${JSON.stringify(returnStrategy)}`);\n debugData(`Records: A: ${recordA}\\nB: ${recordB}`);\n debug(`Externals: A: ${JSON.stringify(recordAExternal)}, B: ${JSON.stringify(recordBExternal)}`);\n // We could add here labels for records if we didn't get external labels\n\n const featuresA = extractFeatures({record: recordA, recordExternal: recordAExternal, localSettings});\n\n debug(`We got an array of records: ${Array.isArray(recordB)}`);\n const recordsB = Array.isArray(recordB) ? recordB : [recordB];\n const recordsBExternal = Array.isArray(recordB) ? recordBExternal : [recordBExternal];\n\n const detectionResults = recordsB.map((record, index) => actualDetection({featuresA, recordAExternal, record, recordExternal: recordsBExternal[index], index}));\n\n // if we got array of records, we return an array of result\n // if we got a singular record, we return a singular result\n return Array.isArray(recordB) ? detectionResults : detectionResults[0];\n\n function actualDetection({featuresA, record, recordExternal, index, localSettings}) {\n const labelA = recordAExternal && recordAExternal.label ? recordAExternal.label : 'a';\n const labelB = recordExternal && recordExternal.label ? recordExternal.label : 'b';\n\n\n debug(`Actual detection for record ${index + 1} ${labelB}`);\n const featuresB = extractFeatures({record, recordExternal, localSettings});\n\n debugData(`Features (a: ${labelA}): ${JSON.stringify(featuresA)}`);\n debugData(`Features (b: ${labelB}): ${JSON.stringify(featuresB)}`);\n\n const featurePairs = generateFeaturePairs(featuresA, featuresB);\n const similarityVector = generateSimilarityVector(featurePairs);\n\n if (similarityVector.some(v => v >= MIN_PROPABILITY_QUALIFIER)) {\n const probability = calculateProbability(similarityVector);\n debug(`probability: ${probability} (Threshold: ${threshold})`);\n if (similarityVector.some(v => v <= FORCE_FAIL)) {\n debug(`Some feature resulted in utter failure (${FORCE_FAIL} or less)`);\n return returnResult({match: false, probability: 0.0});\n }\n\n return returnResult({match: probability >= threshold, probability});\n }\n\n debugData(`No feature yielded minimum probability amount of points (${MIN_PROPABILITY_QUALIFIER})`);\n return returnResult({match: false, probability: 0.0});\n }\n\n function extractFeatures({record, recordExternal}) {\n return strategy.reduce((acc, {name, extract}) => acc.concat({name, value: extract({record, recordExternal})}), []);\n }\n\n function returnResult(result) {\n if (returnStrategy) {\n debug(`Returning detection strategy with the result`);\n const resultWithStrategy = {match: result.match, probability: result.probability, strategy: formatStrategy(strategy), threshold};\n debugData(`${JSON.stringify(resultWithStrategy)}`);\n return resultWithStrategy;\n }\n return result;\n }\n\n function formatStrategy(strategy) {\n const strategyNames = strategy.map(element => element.name);\n return strategyNames || [];\n }\n\n function calculateProbability(similarityVector) {\n const probability = similarityVector.reduce((acc, v) => acc + v, 0.0);\n return probability > 1.0 ? 1.0 : probability;\n }\n\n function generateSimilarityVector(featurePairs) {\n const compared = featurePairs.map(({name, a, b}) => {\n const {compare} = strategy.find(({name: featureName}) => name === featureName);\n const points = compare(a, b);\n return {name, points};\n });\n\n debugData(`Points: ${JSON.stringify(compared)}`);\n return compared.map(({points}) => points);\n }\n\n function generateFeaturePairs(featuresA, featuresB) {\n const pairs = generatePairs();\n const missingFeatures = findMissing();\n\n debug(`Not comparing the following features because one, or both records are missing features: ${JSON.stringify(missingFeatures)}`);\n return pairs;\n\n function generatePairs() {\n return featuresA\n .reduce((acc, {name, value}, index) => acc.concat({\n name,\n a: value,\n b: featuresB[index].value\n }), [])\n .filter(({a, b}) => {\n if (a.length === 0 || b.length === 0) {\n return false;\n }\n\n return true;\n });\n }\n\n function findMissing() {\n return featuresA\n .map(({name}) => name)\n .filter(v => pairs.some(({name}) => name === v) === false);\n }\n }\n\n};\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["\nimport createDebugLogger from 'debug';\nimport * as features from './features/index.js';\n\nexport {features};\n\nexport default ({strategy, threshold = 0.8999}, returnStrategy = false, localSettings = {}) => ({recordA, recordB, recordAExternal = {}, recordBExternal = {}}) => {\n const MIN_PROPABILITY_QUALIFIER = 0.4; // Title perfect match: 0.5\n const FORCE_FAIL = -1.0;\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection');\n const debugData = debug.extend('data');\n\n debugData(`Strategy: ${JSON.stringify(strategy)}, Threshold: ${JSON.stringify(threshold)}, ReturnStrategy: ${JSON.stringify(returnStrategy)}`);\n debugData(`Records: A: ${recordA}\\nB: ${recordB}`);\n debug(`Externals: A: ${JSON.stringify(recordAExternal)}, B: ${JSON.stringify(recordBExternal)}`);\n // We could add here labels for records if we didn't get external labels\n\n const featuresA = extractFeatures({record: recordA, recordExternal: recordAExternal, localSettings});\n\n debug(`We got an array of records: ${Array.isArray(recordB)}`);\n const recordsB = Array.isArray(recordB) ? recordB : [recordB];\n const recordsBExternal = Array.isArray(recordB) ? recordBExternal : [recordBExternal];\n\n const detectionResults = recordsB.map((record, index) => actualDetection({featuresA, recordAExternal, record, recordExternal: recordsBExternal[index], index}));\n\n // if we got array of records, we return an array of result\n // if we got a singular record, we return a singular result\n return Array.isArray(recordB) ? detectionResults : detectionResults[0];\n\n function actualDetection({featuresA, record, recordExternal, index, localSettings}) {\n const labelA = recordAExternal && recordAExternal.label ? recordAExternal.label : 'a';\n const labelB = recordExternal && recordExternal.label ? recordExternal.label : 'b';\n\n\n debug(`Actual detection for record ${index + 1} ${labelB}`);\n const featuresB = extractFeatures({record, recordExternal, localSettings});\n\n debugData(`Features (a: ${labelA}): ${JSON.stringify(featuresA)}`);\n debugData(`Features (b: ${labelB}): ${JSON.stringify(featuresB)}`);\n\n const featurePairs = generateFeaturePairs(featuresA, featuresB);\n const similarityVector = generateSimilarityVector(featurePairs);\n\n if (similarityVector.some(v => v >= MIN_PROPABILITY_QUALIFIER)) {\n const probability = Math.round(calculateProbability(similarityVector) * 100)/100;\n debug(`probability: ${probability} (Threshold: ${threshold})`);\n if (similarityVector.some(v => v <= FORCE_FAIL)) {\n debug(`Some feature resulted in utter failure (${FORCE_FAIL} or less)`);\n return returnResult({match: false, probability: 0.0});\n }\n\n return returnResult({match: probability >= threshold, probability});\n }\n\n debugData(`No feature yielded minimum probability amount of points (${MIN_PROPABILITY_QUALIFIER})`);\n return returnResult({match: false, probability: 0.0});\n }\n\n function extractFeatures({record, recordExternal}) {\n return strategy.reduce((acc, {name, extract}) => acc.concat({name, value: extract({record, recordExternal})}), []);\n }\n\n function returnResult(result) {\n if (returnStrategy) {\n debug(`Returning detection strategy with the result`);\n const resultWithStrategy = {match: result.match, probability: result.probability, strategy: formatStrategy(strategy), threshold};\n debugData(`${JSON.stringify(resultWithStrategy)}`);\n return resultWithStrategy;\n }\n return result;\n }\n\n function formatStrategy(strategy) {\n const strategyNames = strategy.map(element => element.name);\n return strategyNames || [];\n }\n\n function calculateProbability(similarityVector) {\n const probability = similarityVector.reduce((acc, v) => acc + v, 0.0);\n return probability > 1.0 ? 1.0 : probability;\n }\n\n function generateSimilarityVector(featurePairs) {\n const compared = featurePairs.map(({name, a, b}) => {\n const {compare} = strategy.find(({name: featureName}) => name === featureName);\n const points = compare(a, b);\n return {name, points};\n });\n\n debugData(`Points: ${JSON.stringify(compared)}`);\n return compared.map(({points}) => points);\n }\n\n function generateFeaturePairs(featuresA, featuresB) {\n const pairs = generatePairs();\n const missingFeatures = findMissing();\n\n debug(`Not comparing the following features because one, or both records are missing features: ${JSON.stringify(missingFeatures)}`);\n return pairs;\n\n function generatePairs() {\n return featuresA\n .reduce((acc, {name, value}, index) => acc.concat({\n name,\n a: value,\n b: featuresB[index].value\n }), [])\n .filter(({a, b}) => {\n if (a.length === 0 || b.length === 0) {\n return false;\n }\n\n return true;\n });\n }\n\n function findMissing() {\n return featuresA\n .map(({name}) => name)\n .filter(v => pairs.some(({name}) => name === v) === false);\n }\n }\n\n};\n"],
5
+ "mappings": "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",
6
6
  "names": ["featuresA", "localSettings", "strategy"]
7
7
  }
package/package.json CHANGED
@@ -13,7 +13,7 @@
13
13
  "url": "https://github.com/NatLibFi/melinda-record-matching-js"
14
14
  },
15
15
  "license": "MIT",
16
- "version": "5.1.1-alpha.1",
16
+ "version": "5.1.2-alpha.1",
17
17
  "type": "module",
18
18
  "main": "./dist/index.js",
19
19
  "bin": "./dist/cli.js",
@@ -57,7 +57,7 @@
57
57
  "@natlibfi/fixura": "^4.0.1",
58
58
  "cross-env": "^10.1.0",
59
59
  "esbuild": "^0.28.0",
60
- "eslint": "^10.3.0"
60
+ "eslint": "^10.4.0"
61
61
  },
62
62
  "overrides": {
63
63
  "uuid": "^14.0.0"
package/src/cli.js CHANGED
@@ -89,6 +89,7 @@ async function cli() {
89
89
  matchDetection.features.bib.hostComponent(),
90
90
  matchDetection.features.bib.isbn(),
91
91
  matchDetection.features.bib.issn(),
92
+ matchDetection.features.bib.publisherNumber(),
92
93
  // matchDetection.features.bib.sid(),
93
94
  matchDetection.features.bib.otherStandardIdentifier(),
94
95
  // Let's not use the same title matchDetection here
@@ -126,6 +127,7 @@ async function cli() {
126
127
  matchDetection.features.bib.hostComponent(),
127
128
  matchDetection.features.bib.isbn(),
128
129
  matchDetection.features.bib.issn(),
130
+ matchDetection.features.bib.publisherNumber(),
129
131
  // matchDetection.features.bib.sid(),
130
132
  matchDetection.features.bib.otherStandardIdentifier(),
131
133
  matchDetection.features.bib.title(),
package/src/index.js CHANGED
@@ -259,7 +259,7 @@ export default ({detection: detectionOptions, search: searchOptions, maxMatches
259
259
  debugData(`Strategy: ${JSON.stringify(detectionResult.strategy)}, Threshold: ${JSON.stringify(detectionResult.threshold)}`);
260
260
 
261
261
  const matchResult = {
262
- probability: detectionResult.probability,
262
+ probability: Math.round(detectionResult.probability * 100)/100,
263
263
  candidate: {
264
264
  id: candidateId,
265
265
  record: candidateRecord
@@ -9,10 +9,10 @@
9
9
  import createDebugLogger from 'debug';
10
10
 
11
11
  import {isComponentRecord} from '@natlibfi/melinda-commons';
12
- import {uniqArray} from './issn.js';
13
12
  import {parse773g} from '../../../candidate-search/query-list/component.js';
13
+ import {uniqArray} from '@natlibfi/marc-record-validators-melinda/dist/utils.js';
14
14
 
15
- const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:issn');
15
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:f773');
16
16
  const debugData = debug.extend('data');
17
17
 
18
18
  const MAX_IDENTIFIER = 0.1; // This should be pretty low: it only says something about the host
@@ -2,6 +2,7 @@
2
2
  export {default as hostComponent} from './host-component.js';
3
3
  export {default as isbn} from './isbn.js';
4
4
  export {default as issn} from './issn.js';
5
+ export {default as publisherNumber} from './publisherNumber.js'; // 028$b$a
5
6
  export {default as sid} from './sid.js';
6
7
  export {default as otherStandardIdentifier} from './other-standard-identifier.js';
7
8
  export {default as title} from './title.js';
@@ -1,8 +1,6 @@
1
-
2
1
  import createDebugLogger from 'debug';
3
2
  import {parse as isbnParse} from 'isbn3';
4
-
5
- import {uniqArray} from './issn.js';
3
+ import {uniqArray} from '@natlibfi/marc-record-validators-melinda/dist/utils.js';
6
4
 
7
5
  const debug = createDebugLogger(`@natlibfi/melinda-record-matching:match-detection:features:standard-identifiers:ISBN`);
8
6
  const debugData = debug.extend('data');
@@ -1,3 +1,4 @@
1
+ import {uniqArray} from '@natlibfi/marc-record-validators-melinda/dist/utils.js';
1
2
  import createDebugLogger from 'debug';
2
3
 
3
4
  const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:issn');
@@ -53,7 +54,3 @@ export default () => ({
53
54
 
54
55
  }
55
56
  });
56
-
57
- export function uniqArray(arr) {
58
- return arr.filter((val, i) => arr.indexOf(val) === i);
59
- }
@@ -0,0 +1,76 @@
1
+ // Compare publusher number (028$b$a or 028$a)
2
+ import {uniqArray} from '@natlibfi/marc-record-validators-melinda/dist/utils.js';
3
+ import createDebugLogger from 'debug';
4
+
5
+ const debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:publisher-number');
6
+ const debugData = debug.extend('data');
7
+
8
+ export default () => ({
9
+ name: 'Publisher or Distributor Number',
10
+ extract: ({record/*, recordExternal*/}) => {
11
+ //const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';
12
+
13
+ return getIdentifiers();
14
+
15
+ function getIdentifiers() {
16
+ const values = fieldsToPublisherNumber(record.get(/^028$/u));
17
+
18
+ debug(`\t ${values.length} potential publisher identifiers: ${values.join(', ')}`);
19
+
20
+ return uniqArray(values);
21
+
22
+ function fieldsToPublisherNumber(fields, result = []) {
23
+ const [field, ...remainingFields] = fields;
24
+ if (!field) {
25
+ return result;
26
+ }
27
+ const a = field.subfields.find(sf => sf.code === 'a');
28
+ if (!a || !a.value) {
29
+ return fieldsToPublisherNumber(remainingFields, result);
30
+ }
31
+ const aval = normalizePublisherNumber(`${a.value}`);
32
+
33
+ const b = field.subfields.find(sf => sf.code === 'b');
34
+ if (!b || !b.value) {
35
+ return fieldsToPublisherNumber(remainingFields, [...result, aval]);
36
+ }
37
+ const baval = normalizePublisherNumber(`${b.value}${a.value}`);
38
+ return fieldsToPublisherNumber(remainingFields, [...result, baval, aval]);
39
+ }
40
+
41
+ function normalizePublisherNumber(val) {
42
+ return val.replace(/[- .,:;]/ug, '');
43
+ }
44
+ }
45
+ },
46
+ compare: (aa, bb) => {
47
+ debugData(`Comparing identifier sets ${JSON.stringify(aa)} and ${JSON.stringify(bb)}`);
48
+ if (aa.length === 0 || bb.length === 0) {
49
+ // No data for decision
50
+ return 0;
51
+ }
52
+ const sharedIdentifiers = aa.filter(val => bb.includes(val));
53
+ if (sharedIdentifiers.length > 0) {
54
+ const goodMatch = sharedIdentifiers.find(identifier => identifier.match(/[^0-9]/u));
55
+ if (goodMatch) {
56
+ debug(`\tShared identifier found: '${goodMatch}'`);
57
+ return 0.5;
58
+ }
59
+ debug(`\tShared identifier (numbers only) found: '${sharedIdentifiers[0]}'`);
60
+ return 0.2;
61
+ }
62
+
63
+ const aa2 = aa.map(a => a.replace(/[^0-9]/ug, ''));
64
+ const bb2 = bb.map(b => b.replace(/[^0-9]/ug, ''));
65
+
66
+ // Numbers matches (aka don't penalize "Fazer F-123" vs "F-123")
67
+ // Note that FOOLP-123 vs FOOCD-123 will now also return 0.0. Still nothing positive is returned, so no damage done.
68
+ if (aa2.find(val => val !== '' && bb2.includes(val))) {
69
+ return 0.0;
70
+ }
71
+
72
+ return -0.2;
73
+
74
+ }
75
+ });
76
+
@@ -43,7 +43,7 @@ export default ({strategy, threshold = 0.8999}, returnStrategy = false, localSet
43
43
  const similarityVector = generateSimilarityVector(featurePairs);
44
44
 
45
45
  if (similarityVector.some(v => v >= MIN_PROPABILITY_QUALIFIER)) {
46
- const probability = calculateProbability(similarityVector);
46
+ const probability = Math.round(calculateProbability(similarityVector) * 100)/100;
47
47
  debug(`probability: ${probability} (Threshold: ${threshold})`);
48
48
  if (similarityVector.some(v => v <= FORCE_FAIL)) {
49
49
  debug(`Some feature resulted in utter failure (${FORCE_FAIL} or less)`);