@natlibfi/melinda-record-matching 5.0.3 → 5.0.5-alpha.2
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/.github/workflows/melinda-node-tests-and-publish.yml +4 -4
- package/dist/candidate-search/query-list/bib.js +84 -67
- package/dist/candidate-search/query-list/bib.js.map +3 -3
- package/dist/candidate-search/query-list/component.js +94 -28
- package/dist/candidate-search/query-list/component.js.map +3 -3
- package/dist/cli.js +12 -4
- package/dist/cli.js.map +2 -2
- package/dist/match-detection/features/bib/f773.js +82 -0
- package/dist/match-detection/features/bib/f773.js.map +7 -0
- package/dist/match-detection/features/bib/index.js +1 -1
- package/dist/match-detection/features/bib/index.js.map +2 -2
- package/dist/match-detection/features/bib/index.test.js +1 -1
- package/dist/match-detection/features/bib/index.test.js.map +2 -2
- package/dist/match-detection/features/bib/isbn.js +47 -48
- package/dist/match-detection/features/bib/isbn.js.map +2 -2
- package/dist/match-detection/features/bib/issn.js +2 -16
- package/dist/match-detection/features/bib/issn.js.map +2 -2
- package/dist/match-detection/features/bib/language.js +12 -22
- package/dist/match-detection/features/bib/language.js.map +2 -2
- package/dist/match-detection/features/bib/publication-time-allow-cons-years.js +6 -2
- package/dist/match-detection/features/bib/publication-time-allow-cons-years.js.map +2 -2
- package/dist/match-detection/features/bib/publication-time.js +62 -16
- package/dist/match-detection/features/bib/publication-time.js.map +2 -2
- package/dist/match-detection/features/bib/title.js +76 -26
- package/dist/match-detection/features/bib/title.js.map +3 -3
- package/dist/match-detection/index.js +8 -3
- package/dist/match-detection/index.js.map +2 -2
- package/dist/matching-utils.js +14 -0
- package/dist/matching-utils.js.map +2 -2
- package/package.json +11 -12
- package/src/candidate-search/query-list/bib.js +100 -77
- package/src/candidate-search/query-list/component.js +100 -23
- package/src/cli.js +8 -4
- package/src/match-detection/features/bib/f773.js +118 -0
- package/src/match-detection/features/bib/index.js +1 -1
- package/src/match-detection/features/bib/index.test.js +1 -1
- package/src/match-detection/features/bib/isbn.js +66 -60
- package/src/match-detection/features/bib/issn.js +3 -32
- package/src/match-detection/features/bib/language.js +15 -26
- package/src/match-detection/features/bib/publication-time-allow-cons-years.js +7 -4
- package/src/match-detection/features/bib/publication-time.js +76 -30
- package/src/match-detection/features/bib/title.js +112 -34
- package/src/match-detection/index.js +9 -3
- package/src/matching-utils.js +17 -0
- package/dist/match-detection/features/bib/title-version-original.js +0 -37
- package/dist/match-detection/features/bib/title-version-original.js.map +0 -7
- package/src/match-detection/features/bib/title-version-original.js +0 -52
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/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
|
|
5
|
-
"mappings": "AACA,OAAO,uBAAuB;AAC9B,YAAY,cAAc;AAE1B,SAAQ;AAER,eAAe,CAAC,EAAC,UAAU,YAAY,OAAM,GAAG,iBAAiB,OAAO,gBAAgB,CAAC,MAAM,CAAC,EAAC,SAAS,SAAS,kBAAkB,CAAC,GAAG,kBAAkB,CAAC,EAAC,MAAM;AACjK,QAAM,
|
|
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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",
|
|
6
6
|
"names": ["featuresA", "localSettings", "strategy"]
|
|
7
7
|
}
|
package/dist/matching-utils.js
CHANGED
|
@@ -76,4 +76,18 @@ export function getMatchCounts(aValues, bValues) {
|
|
|
76
76
|
return aToB < bToA ? aToB : bToA;
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
|
+
export function stringAfter(string, substring) {
|
|
80
|
+
const pos = string.indexOf(substring);
|
|
81
|
+
if (pos === -1) {
|
|
82
|
+
return string;
|
|
83
|
+
}
|
|
84
|
+
return string.substring(pos + substring.length);
|
|
85
|
+
}
|
|
86
|
+
export function stringBefore(string, substring) {
|
|
87
|
+
const pos = string.indexOf(substring);
|
|
88
|
+
if (pos === -1) {
|
|
89
|
+
return string;
|
|
90
|
+
}
|
|
91
|
+
return string.substring(0, pos);
|
|
92
|
+
}
|
|
79
93
|
//# sourceMappingURL=matching-utils.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/matching-utils.js"],
|
|
4
|
-
"sourcesContent": ["import createDebugLogger from 'debug';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:utils');\nconst debugData = debug.extend('data');\n\nexport function getMelindaIdsF035(record) {\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:melinda-id');\n const debugData = debug.extend('data');\n\n const f035s = record.getFields('035');\n\n if (f035s.length < 1) {\n debug(`No f035s found.`);\n return [];\n }\n\n const allF035MelindaIds = [].concat(...f035s.map(field => toMelindaIds(field)));\n const f035MelindaIds = [...new Set(allF035MelindaIds)];\n\n debugData(`Fields (${f035s.length}): ${JSON.stringify(f035s)}`);\n debugData(`Ids (${allF035MelindaIds.length}): ${JSON.stringify(allF035MelindaIds)}`);\n debugData(`Unique ids (${f035MelindaIds.length}): ${JSON.stringify(f035MelindaIds)}`);\n\n return f035MelindaIds;\n}\n\nexport function toMelindaIds({subfields}, subfieldsToParse = ['a', 'z']) {\n const melindaIdRegExp = /^(?<prefix>\\(FI-MELINDA\\)|FCC)(?<id>\\d{9})$/u;\n\n return subfields\n .filter(sub => subfieldsToParse.includes(sub.code))\n .filter(sub => testStringOrNumber(sub.value) && melindaIdRegExp.test(String(sub.value)))\n .map(({value}) => testStringOrNumber(value) ? String(value).replace(melindaIdRegExp, '$<id>') : '');\n}\n\nexport function validateSidFieldSubfieldCounts(field) {\n // Valid SID-fields have just one $c and one $b\n debugData(`Validating SID field ${JSON.stringify(field)}`);\n const countC = countSubfields(field, 'c');\n const countB = countSubfields(field, 'b');\n debug(`Found ${countC} sf $cs and ${countB} sf $bs. IsValid: ${countC === 1 && countB === 1}`);\n\n return countC === 1 && countB === 1;\n}\n\nfunction countSubfields(field, subfieldCode) {\n // debug(`Counting subfields ${subfieldCode}`);\n return field.subfields.filter(({code}) => code === subfieldCode).length;\n}\n\nexport function getSubfieldValues(field, subfieldCode) {\n debugData(`Get subfield(s) $${subfieldCode} from ${JSON.stringify(field)}`);\n return field.subfields\n .filter(({code}) => code === subfieldCode)\n .map(({value}) => testStringOrNumber(value) ? String(value) : '')\n .filter(value => value);\n}\n\nexport function testStringOrNumber(value) {\n if (typeof value === 'string' || typeof value === 'number') {\n return true;\n }\n return false;\n}\n\nexport function extractSubfieldsFromField(field, subfieldCodes) {\n if (field === undefined || field.subfields === undefined) {\n return [];\n }\n const resultSubfields = field.subfields\n .filter(({code}) => subfieldCodes.includes(code))\n .map(({code, value}) => ({code, value: testStringOrNumber(value) ? String(value) : ''}))\n .filter(value => value);\n return resultSubfields;\n}\n\nexport function uniqueSubfields(subfields) {\n return subfields.reduce((arr, e) => {\n if (!arr.find(item => item.code === e.code && item.value === e.value)) {\n const newArr = arr.concat(e);\n return newArr;\n }\n return arr;\n }, []);\n}\n\nexport function getMatchCounts(aValues, bValues) {\n\n const matchingValues = getMatchingValuesAmount(aValues, bValues);\n\n return {\n maxValues: aValues.length > bValues.length ? aValues.length : bValues.length,\n // possibleMatchingValues: amount of identifiers in set of less identifiers (we cannot more values than these)\n possibleMatchValues: aValues.length > bValues.length ? bValues.length : aValues.length,\n matchingValues\n };\n\n function getMatchingValuesAmount(aValues, bValues) {\n if (bValues.length > aValues.length) {\n return aValues.filter(aValue => bValues.some(bValue => aValue === bValue)).length;\n }\n if (aValues.length > bValues.length) {\n return bValues.filter(bValue => aValues.some(aValue => bValue === aValue)).length;\n }\n\n // If we have same amount of values, we'll check matches both ways, to avoid mixups in cases\n // there would be duplicate values\n const aToB = aValues.filter(aValue => bValues.some(bValue => aValue === bValue)).length;\n const bToA = bValues.filter(bValue => aValues.some(aValue => bValue === aValue)).length;\n\n return aToB < bToA ? aToB : bToA;\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["import createDebugLogger from 'debug';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:utils');\nconst debugData = debug.extend('data');\n\nexport function getMelindaIdsF035(record) {\n\n const debug = createDebugLogger('@natlibfi/melinda-record-matching:melinda-id');\n const debugData = debug.extend('data');\n\n const f035s = record.getFields('035');\n\n if (f035s.length < 1) {\n debug(`No f035s found.`);\n return [];\n }\n\n const allF035MelindaIds = [].concat(...f035s.map(field => toMelindaIds(field)));\n const f035MelindaIds = [...new Set(allF035MelindaIds)];\n\n debugData(`Fields (${f035s.length}): ${JSON.stringify(f035s)}`);\n debugData(`Ids (${allF035MelindaIds.length}): ${JSON.stringify(allF035MelindaIds)}`);\n debugData(`Unique ids (${f035MelindaIds.length}): ${JSON.stringify(f035MelindaIds)}`);\n\n return f035MelindaIds;\n}\n\nexport function toMelindaIds({subfields}, subfieldsToParse = ['a', 'z']) {\n const melindaIdRegExp = /^(?<prefix>\\(FI-MELINDA\\)|FCC)(?<id>\\d{9})$/u;\n\n return subfields\n .filter(sub => subfieldsToParse.includes(sub.code))\n .filter(sub => testStringOrNumber(sub.value) && melindaIdRegExp.test(String(sub.value)))\n .map(({value}) => testStringOrNumber(value) ? String(value).replace(melindaIdRegExp, '$<id>') : '');\n}\n\nexport function validateSidFieldSubfieldCounts(field) {\n // Valid SID-fields have just one $c and one $b\n debugData(`Validating SID field ${JSON.stringify(field)}`);\n const countC = countSubfields(field, 'c');\n const countB = countSubfields(field, 'b');\n debug(`Found ${countC} sf $cs and ${countB} sf $bs. IsValid: ${countC === 1 && countB === 1}`);\n\n return countC === 1 && countB === 1;\n}\n\nfunction countSubfields(field, subfieldCode) {\n // debug(`Counting subfields ${subfieldCode}`);\n return field.subfields.filter(({code}) => code === subfieldCode).length;\n}\n\nexport function getSubfieldValues(field, subfieldCode) {\n debugData(`Get subfield(s) $${subfieldCode} from ${JSON.stringify(field)}`);\n return field.subfields\n .filter(({code}) => code === subfieldCode)\n .map(({value}) => testStringOrNumber(value) ? String(value) : '')\n .filter(value => value);\n}\n\nexport function testStringOrNumber(value) {\n if (typeof value === 'string' || typeof value === 'number') {\n return true;\n }\n return false;\n}\n\nexport function extractSubfieldsFromField(field, subfieldCodes) {\n if (field === undefined || field.subfields === undefined) {\n return [];\n }\n const resultSubfields = field.subfields\n .filter(({code}) => subfieldCodes.includes(code))\n .map(({code, value}) => ({code, value: testStringOrNumber(value) ? String(value) : ''}))\n .filter(value => value);\n return resultSubfields;\n}\n\nexport function uniqueSubfields(subfields) {\n return subfields.reduce((arr, e) => {\n if (!arr.find(item => item.code === e.code && item.value === e.value)) {\n const newArr = arr.concat(e);\n return newArr;\n }\n return arr;\n }, []);\n}\n\nexport function getMatchCounts(aValues, bValues) {\n\n const matchingValues = getMatchingValuesAmount(aValues, bValues);\n\n return {\n maxValues: aValues.length > bValues.length ? aValues.length : bValues.length,\n // possibleMatchingValues: amount of identifiers in set of less identifiers (we cannot more values than these)\n possibleMatchValues: aValues.length > bValues.length ? bValues.length : aValues.length,\n matchingValues\n };\n\n function getMatchingValuesAmount(aValues, bValues) {\n if (bValues.length > aValues.length) {\n return aValues.filter(aValue => bValues.some(bValue => aValue === bValue)).length;\n }\n if (aValues.length > bValues.length) {\n return bValues.filter(bValue => aValues.some(aValue => bValue === aValue)).length;\n }\n\n // If we have same amount of values, we'll check matches both ways, to avoid mixups in cases\n // there would be duplicate values\n const aToB = aValues.filter(aValue => bValues.some(bValue => aValue === bValue)).length;\n const bToA = bValues.filter(bValue => aValues.some(aValue => bValue === aValue)).length;\n\n return aToB < bToA ? aToB : bToA;\n }\n}\n\n\nexport function stringAfter(string, substring) {\n const pos = string.indexOf(substring);\n if (pos === -1) {\n return string;\n }\n return string.substring(pos+substring.length);\n}\n\nexport function stringBefore(string, substring) {\n const pos = string.indexOf(substring);\n if (pos === -1) {\n return string;\n }\n return string.substring(0, pos);\n}\n"],
|
|
5
|
+
"mappings": "AAAA,OAAO,uBAAuB;AAE9B,MAAM,QAAQ,kBAAkB,yCAAyC;AACzE,MAAM,YAAY,MAAM,OAAO,MAAM;AAE9B,gBAAS,kBAAkB,QAAQ;AAExC,QAAMA,SAAQ,kBAAkB,8CAA8C;AAC9E,QAAMC,aAAYD,OAAM,OAAO,MAAM;AAErC,QAAM,QAAQ,OAAO,UAAU,KAAK;AAEpC,MAAI,MAAM,SAAS,GAAG;AACpB,IAAAA,OAAM,iBAAiB;AACvB,WAAO,CAAC;AAAA,EACV;AAEA,QAAM,oBAAoB,CAAC,EAAE,OAAO,GAAG,MAAM,IAAI,WAAS,aAAa,KAAK,CAAC,CAAC;AAC9E,QAAM,iBAAiB,CAAC,GAAG,IAAI,IAAI,iBAAiB,CAAC;AAErD,EAAAC,WAAU,WAAW,MAAM,MAAM,MAAM,KAAK,UAAU,KAAK,CAAC,EAAE;AAC9D,EAAAA,WAAU,QAAQ,kBAAkB,MAAM,MAAM,KAAK,UAAU,iBAAiB,CAAC,EAAE;AACnF,EAAAA,WAAU,eAAe,eAAe,MAAM,MAAM,KAAK,UAAU,cAAc,CAAC,EAAE;AAEpF,SAAO;AACT;AAEO,gBAAS,aAAa,EAAC,UAAS,GAAG,mBAAmB,CAAC,KAAK,GAAG,GAAG;AACvE,QAAM,kBAAkB;AAExB,SAAO,UACJ,OAAO,SAAO,iBAAiB,SAAS,IAAI,IAAI,CAAC,EACjD,OAAO,SAAO,mBAAmB,IAAI,KAAK,KAAK,gBAAgB,KAAK,OAAO,IAAI,KAAK,CAAC,CAAC,EACtF,IAAI,CAAC,EAAC,MAAK,MAAM,mBAAmB,KAAK,IAAI,OAAO,KAAK,EAAE,QAAQ,iBAAiB,OAAO,IAAI,EAAE;AACtG;AAEO,gBAAS,+BAA+B,OAAO;AAEpD,YAAU,wBAAwB,KAAK,UAAU,KAAK,CAAC,EAAE;AACzD,QAAM,SAAS,eAAe,OAAO,GAAG;AACxC,QAAM,SAAS,eAAe,OAAO,GAAG;AACxC,QAAM,SAAS,MAAM,eAAe,MAAM,qBAAqB,WAAW,KAAK,WAAW,CAAC,EAAE;AAE7F,SAAO,WAAW,KAAK,WAAW;AACpC;AAEA,SAAS,eAAe,OAAO,cAAc;AAE3C,SAAO,MAAM,UAAU,OAAO,CAAC,EAAC,KAAI,MAAM,SAAS,YAAY,EAAE;AACnE;AAEO,gBAAS,kBAAkB,OAAO,cAAc;AACrD,YAAU,oBAAoB,YAAY,SAAS,KAAK,UAAU,KAAK,CAAC,EAAE;AAC1E,SAAO,MAAM,UACV,OAAO,CAAC,EAAC,KAAI,MAAM,SAAS,YAAY,EACxC,IAAI,CAAC,EAAC,MAAK,MAAM,mBAAmB,KAAK,IAAI,OAAO,KAAK,IAAI,EAAE,EAC/D,OAAO,WAAS,KAAK;AAC1B;AAEO,gBAAS,mBAAmB,OAAO;AACxC,MAAI,OAAO,UAAU,YAAY,OAAO,UAAU,UAAU;AAC1D,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAEO,gBAAS,0BAA0B,OAAO,eAAe;AAC9D,MAAI,UAAU,UAAa,MAAM,cAAc,QAAW;AACxD,WAAO,CAAC;AAAA,EACV;AACA,QAAM,kBAAkB,MAAM,UAC3B,OAAO,CAAC,EAAC,KAAI,MAAM,cAAc,SAAS,IAAI,CAAC,EAC/C,IAAI,CAAC,EAAC,MAAM,MAAK,OAAO,EAAC,MAAM,OAAO,mBAAmB,KAAK,IAAI,OAAO,KAAK,IAAI,GAAE,EAAE,EACtF,OAAO,WAAS,KAAK;AACxB,SAAO;AACT;AAEO,gBAAS,gBAAgB,WAAW;AACzC,SAAO,UAAU,OAAO,CAAC,KAAK,MAAM;AAClC,QAAI,CAAC,IAAI,KAAK,UAAQ,KAAK,SAAS,EAAE,QAAQ,KAAK,UAAU,EAAE,KAAK,GAAG;AACrE,YAAM,SAAS,IAAI,OAAO,CAAC;AAC3B,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AACP;AAEO,gBAAS,eAAe,SAAS,SAAS;AAE/C,QAAM,iBAAiB,wBAAwB,SAAS,OAAO;AAE/D,SAAO;AAAA,IACL,WAAW,QAAQ,SAAS,QAAQ,SAAS,QAAQ,SAAS,QAAQ;AAAA;AAAA,IAEtE,qBAAqB,QAAQ,SAAS,QAAQ,SAAS,QAAQ,SAAS,QAAQ;AAAA,IAChF;AAAA,EACF;AAEA,WAAS,wBAAwBC,UAASC,UAAS;AACjD,QAAIA,SAAQ,SAASD,SAAQ,QAAQ;AACnC,aAAOA,SAAQ,OAAO,YAAUC,SAAQ,KAAK,YAAU,WAAW,MAAM,CAAC,EAAE;AAAA,IAC7E;AACA,QAAID,SAAQ,SAASC,SAAQ,QAAQ;AACnC,aAAOA,SAAQ,OAAO,YAAUD,SAAQ,KAAK,YAAU,WAAW,MAAM,CAAC,EAAE;AAAA,IAC7E;AAIA,UAAM,OAAOA,SAAQ,OAAO,YAAUC,SAAQ,KAAK,YAAU,WAAW,MAAM,CAAC,EAAE;AACjF,UAAM,OAAOA,SAAQ,OAAO,YAAUD,SAAQ,KAAK,YAAU,WAAW,MAAM,CAAC,EAAE;AAEjF,WAAO,OAAO,OAAO,OAAO;AAAA,EAC9B;AACF;AAGO,gBAAS,YAAY,QAAQ,WAAW;AAC7C,QAAM,MAAM,OAAO,QAAQ,SAAS;AACpC,MAAI,QAAQ,IAAI;AACd,WAAO;AAAA,EACT;AACA,SAAO,OAAO,UAAU,MAAI,UAAU,MAAM;AAC9C;AAEO,gBAAS,aAAa,QAAQ,WAAW;AAC9C,QAAM,MAAM,OAAO,QAAQ,SAAS;AACpC,MAAI,QAAQ,IAAI;AACd,WAAO;AAAA,EACT;AACA,SAAO,OAAO,UAAU,GAAG,GAAG;AAChC;",
|
|
6
6
|
"names": ["debug", "debugData", "aValues", "bValues"]
|
|
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.0.
|
|
16
|
+
"version": "5.0.5-alpha.2",
|
|
17
17
|
"type": "module",
|
|
18
18
|
"main": "./dist/index.js",
|
|
19
19
|
"bin": "./dist/cli.js",
|
|
@@ -32,33 +32,32 @@
|
|
|
32
32
|
"lint:dev": "eslint --fix ./src",
|
|
33
33
|
"test:base": "node --test --test-force-exit --experimental-test-coverage --test-reporter=spec './src/**/*.test.js'",
|
|
34
34
|
"test": "npm run lint && npm run test:base",
|
|
35
|
+
"test:only": "npm run lint && npm run test:base:only",
|
|
36
|
+
"test:base:only": "cross-env DEBUG=@natlibfi/*matching* NODE_ENV=test node --test --test-only --test-force-exit --experimental-test-coverage --test-reporter=spec './src/**/*.test.js'",
|
|
35
37
|
"watch:test": "cross-env DEBUG=@natlibfi/* NODE_ENV=test node --watch --test --experimental-test-coverage --test-reporter=spec './src/**/*.test.js'",
|
|
36
38
|
"dev": "npm run watch:test",
|
|
37
39
|
"dev:debug": "cross-env LOG_LEVEL=debug DEBUG=@natlibfi/* NODE_ENV=test npm run watch:test"
|
|
38
40
|
},
|
|
39
41
|
"dependencies": {
|
|
40
42
|
"@natlibfi/marc-record": "^10.0.1",
|
|
41
|
-
"@natlibfi/marc-record-serializers": "^11.0.
|
|
42
|
-
"@natlibfi/marc-record-validators-melinda": "^12.0.
|
|
43
|
+
"@natlibfi/marc-record-serializers": "^11.0.2",
|
|
44
|
+
"@natlibfi/marc-record-validators-melinda": "^12.0.15",
|
|
43
45
|
"@natlibfi/melinda-commons": "^14.0.2",
|
|
44
46
|
"@natlibfi/sru-client": "^7.0.1",
|
|
45
47
|
"debug": "^4.4.3",
|
|
46
48
|
"isbn3": "^2.0.6",
|
|
47
49
|
"moment": "^2.30.1",
|
|
48
50
|
"natural": "^8.1.1",
|
|
49
|
-
"uuid": "^
|
|
51
|
+
"uuid": "^14.0.0",
|
|
50
52
|
"yargs": "^18.0.0"
|
|
51
53
|
},
|
|
52
54
|
"devDependencies": {
|
|
53
|
-
"@natlibfi/fixugen": "^3.0.
|
|
54
|
-
"@natlibfi/fixugen-http-client": "^4.0.
|
|
55
|
-
"@natlibfi/fixura": "^4.0.
|
|
55
|
+
"@natlibfi/fixugen": "^3.0.1",
|
|
56
|
+
"@natlibfi/fixugen-http-client": "^4.0.3",
|
|
57
|
+
"@natlibfi/fixura": "^4.0.1",
|
|
56
58
|
"cross-env": "^10.1.0",
|
|
57
|
-
"esbuild": "^0.
|
|
58
|
-
"eslint": "^10.
|
|
59
|
-
},
|
|
60
|
-
"overrides": {
|
|
61
|
-
"nanoid": "^3.3.8"
|
|
59
|
+
"esbuild": "^0.28.0",
|
|
60
|
+
"eslint": "^10.2.1"
|
|
62
61
|
},
|
|
63
62
|
"nvolkComment": "cld3-asm 4.0.0 uses emscripten-wasm-loader ^3.0.3 which uses problematic, non-secure nanoid version 2.X.X."
|
|
64
63
|
}
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import createDebugLogger from 'debug';
|
|
2
2
|
import {toQueries} from '../candidate-search-utils.js';
|
|
3
3
|
import {getMelindaIdsF035, validateSidFieldSubfieldCounts, getSubfieldValues, testStringOrNumber} from '../../matching-utils.js';
|
|
4
|
+
import {extractHostMelindaIdsFromField, extractPublicationYearFrom773, getHostMelindaFields} from './component.js';
|
|
5
|
+
import {fieldToString} from '@natlibfi/marc-record-validators-melinda';
|
|
6
|
+
import {getTitle} from '../../match-detection/features/bib/title.js';
|
|
4
7
|
|
|
5
8
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query');
|
|
6
9
|
|
|
10
|
+
const IS_OK_ALONE_THRESHOLD = 5;
|
|
11
|
+
|
|
7
12
|
export function bibSourceIds(record) {
|
|
8
13
|
|
|
9
14
|
/* Melinda's SRU-index melinda.sourceid includes source IDs from SID fields in Melinda records
|
|
@@ -126,31 +131,27 @@ export function bibHostComponents(record) {
|
|
|
126
131
|
const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:bibHostComponents');
|
|
127
132
|
const debugData = debug.extend('data');
|
|
128
133
|
debug(`Creating queries for hostIds`);
|
|
134
|
+
const f773s = getHostMelindaFields(record);
|
|
135
|
+
if (f773s.length !== 1) { // This needs some thinking...
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
129
138
|
|
|
130
|
-
const id =
|
|
131
|
-
|
|
139
|
+
const [id] = extractHostMelindaIdsFromField(f773s[0]);
|
|
140
|
+
debug(`Found id: ${JSON.stringify(id)}`);
|
|
132
141
|
|
|
133
|
-
|
|
142
|
+
// NB! record.isCR() does not work if LDR/07=a, so we get year from 773$g!
|
|
143
|
+
const year = extractPublicationYearFrom773(f773s[0]);
|
|
144
|
+
debug(`${fieldToString(f773s[0])} => ${year ? year : 'N/A'}`);
|
|
145
|
+
if (year) {
|
|
146
|
+
return [`dc.date=${year} AND melinda.partsofhost=${id}`];
|
|
147
|
+
}
|
|
134
148
|
|
|
135
|
-
|
|
136
|
-
const [field] = record.get(/^773$/u);
|
|
149
|
+
return [`melinda.partsofhost=${id}`];
|
|
137
150
|
|
|
138
|
-
|
|
139
|
-
const {value} = field.subfields.find(({code}) => code === 'w') || {};
|
|
151
|
+
}
|
|
140
152
|
|
|
141
|
-
if (testStringOrNumber(value) && (/^\(FI-MELINDA\)/u).test(String(value))) {
|
|
142
|
-
return String(value).replace(/^\(FI-MELINDA\)/u, '');
|
|
143
|
-
}
|
|
144
153
|
|
|
145
|
-
if (testStringOrNumber(value) && (/^\(FIN01\)/u).test(String(value))) {
|
|
146
|
-
return String(value).replace(/^\(FIN01\)/u, '');
|
|
147
|
-
}
|
|
148
154
|
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
return false;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
155
|
|
|
155
156
|
// SRU search dc.title with a search phrase starting with ^ maps currently in Melinda to (probably) to *headings* index TIT
|
|
156
157
|
// - Aleph cannot currently handle headings searches starting with a boolean - in these cases use word search
|
|
@@ -184,43 +185,78 @@ export function bibTitleAuthorYearAlternates(record) {
|
|
|
184
185
|
return {queryList: Array.from(origQueryList).reverse(), queryListType: 'alternates'};
|
|
185
186
|
}
|
|
186
187
|
|
|
188
|
+
function getTitleForQuery(record) {
|
|
189
|
+
const title = getTitle(record, ['a']);
|
|
190
|
+
if (title) {
|
|
191
|
+
const nWords = title.split(' ');
|
|
192
|
+
// If lone $a is deemed too short, fetch $b as well:
|
|
193
|
+
// (NV: I've seen pairs with 245$a-only vs 245$a$b, so I'd like to use $a only if it is long enough)
|
|
194
|
+
|
|
195
|
+
if (nWords < 3 || title.length < 12) {
|
|
196
|
+
const [f245] = record.get('245');
|
|
197
|
+
// If punctuation is ' =' I think that f245$a is good enough despite shortness. Trying to balance between two bad situations...
|
|
198
|
+
// "Suo (= short title) siellä, vetelä (= missing $b name in another language) täällä..."
|
|
199
|
+
if (f245 && f245.subfields.find(sf => sf.code === 'a' && sf.value.match(/ =$/u)) && title.length >= IS_OK_ALONE_THRESHOLD) {
|
|
200
|
+
return title;
|
|
201
|
+
}
|
|
202
|
+
return getTitle(record, ['a', 'b']); // Try to get a longer title
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return title;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function dcTitle(record, onlyTitleLength, alternates = false) {
|
|
209
|
+
const title = getTitleForQuery(record);
|
|
210
|
+
if (!testStringOrNumber(title)) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const formatted = getFormattedTitle(title);
|
|
215
|
+
|
|
216
|
+
// use word search for titles starting with a boolean
|
|
217
|
+
const useWordSearch = checkUseWordSearch(formatted);
|
|
218
|
+
// Prevent too many matches / SRU crashing by having a minimum length
|
|
219
|
+
// Note that currently this fails matching if there are no matches from previous matchers
|
|
220
|
+
if (formatted.length >= onlyTitleLength && !alternates) {
|
|
221
|
+
return [`dc.title="${useWordSearch ? '' : '^'}${formatted}*"`, formatted, true];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const queryIsOkAlone = !useWordSearch && formatted.length >= IS_OK_ALONE_THRESHOLD;
|
|
225
|
+
|
|
226
|
+
// use word search without ending * also in combination searches to avoid SRU-server crashes [MRA-189]
|
|
227
|
+
return [`dc.title="${useWordSearch || !queryIsOkAlone ? '' : '^'}${formatted}${queryIsOkAlone ? '*' : ''}"`, formatted, queryIsOkAlone];
|
|
228
|
+
|
|
229
|
+
function getFormattedTitle(title) {
|
|
230
|
+
const formatted = String(title)
|
|
231
|
+
.replace(/[\-+ !"(){}\[\]<>;:.?/@*%=^_`~]/gu, ' ')
|
|
232
|
+
.replace(/[^\w\s\p{Alphabetic}]/gu, '') // Apparently matches to/works with non-aplhabetic scripts such as Chinese as well
|
|
233
|
+
.replace(/ +/gu, ' ') // Clean up concurrent spaces from eg. subfield changes
|
|
234
|
+
.trim()
|
|
235
|
+
.replace(/^(.{30}\S*) .*$/, "$1")
|
|
236
|
+
.trim();
|
|
237
|
+
|
|
238
|
+
return formatted;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
187
242
|
export function bibTitleAuthorPublisher({record, onlyTitleLength, addYear = false, alternates = false, alternateQueries = []}) {
|
|
188
243
|
debug(`bibTitleAuthorPublisher, onlyTitleLength: ${onlyTitleLength}, addYear: ${addYear}, alternates: ${alternates}`);
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
191
|
-
|
|
244
|
+
const [query, formatted, queryIsOkAlone] = dcTitle(record, onlyTitleLength, alternates);
|
|
245
|
+
if (query === undefined) {
|
|
246
|
+
return [];
|
|
247
|
+
}
|
|
192
248
|
|
|
193
|
-
|
|
194
|
-
const useWordSearch = checkUseWordSearch(formatted);
|
|
195
|
-
// Prevent too many matches / SRU crashing by having a minimum length
|
|
196
|
-
// Note that currently this fails matching if there are no matches from previous matchers
|
|
197
|
-
if (formatted.length >= onlyTitleLength && !alternates) {
|
|
198
|
-
return [`dc.title="${useWordSearch ? '' : '^'}${formatted}*"`];
|
|
199
|
-
}
|
|
200
|
-
const queryIsOkAlone = formatted.length >= 5;
|
|
201
|
-
|
|
202
|
-
// use word search without ending * also in combination searches to avoid SRU-server crashes [MRA-189]
|
|
203
|
-
const query = `dc.title="${useWordSearch || !queryIsOkAlone ? '' : '^'}${formatted}${queryIsOkAlone ? '*' : ''}"`;
|
|
204
|
-
debug(`query: ${query}`);
|
|
205
|
-
const newAlternateQueries = alternates ? [...alternateQueries, query] : alternateQueries;
|
|
206
|
-
|
|
207
|
-
return addAuthorsToSearch({query, queryIsOkAlone, addYear, alternates, alternateQueries: newAlternateQueries});
|
|
208
|
-
|
|
209
|
-
function getFormatted(title) {
|
|
210
|
-
const formatted = String(title)
|
|
211
|
-
.replace(/[\-+ !"(){}\[\]<>;:.?/@*%=^_`~]/gu, ' ')
|
|
212
|
-
.replace(/[^\w\s\p{Alphabetic}]/gu, '')
|
|
213
|
-
// Clean up concurrent spaces from fe. subfield changes
|
|
214
|
-
.replace(/ +/gu, ' ')
|
|
215
|
-
.trim()
|
|
216
|
-
.replace(/^(.{30}\S*) .*$/, "$1")
|
|
217
|
-
.trim();
|
|
249
|
+
debug(`query: ${query}`);
|
|
218
250
|
|
|
219
|
-
|
|
220
|
-
|
|
251
|
+
// Prevent too many matches / SRU crashing by having a minimum length
|
|
252
|
+
// Note that currently this fails matching if there are no matches from previous matchers
|
|
253
|
+
if (formatted.length >= onlyTitleLength && !alternates) {
|
|
254
|
+
return [query];
|
|
221
255
|
}
|
|
222
256
|
|
|
223
|
-
|
|
257
|
+
const newAlternateQueries = alternates ? [...alternateQueries, query] : alternateQueries;
|
|
258
|
+
|
|
259
|
+
return addAuthorsToSearch({query, queryIsOkAlone, addYear, alternates, alternateQueries: newAlternateQueries});
|
|
224
260
|
|
|
225
261
|
function addAuthorsToSearch({query, queryIsOkAlone = false, addYear = false, alternates = false, alternateQueries = []}) {
|
|
226
262
|
debug('addAuthorsToSearch');
|
|
@@ -254,8 +290,8 @@ export function bibTitleAuthorPublisher({record, onlyTitleLength, addYear = fals
|
|
|
254
290
|
function addYearToSearch({query, queryIsOkAlone = false, alternates = false, alternateQueries = []}) {
|
|
255
291
|
const [yearQuery] = bibYear(record);
|
|
256
292
|
if (yearQuery !== undefined) {
|
|
257
|
-
const
|
|
258
|
-
return alternates ?
|
|
293
|
+
const yearAndOtherQueries = `${yearQuery} AND ${query}`;
|
|
294
|
+
return alternates ? [...alternateQueries, yearAndOtherQueries] : [yearAndOtherQueries];
|
|
259
295
|
}
|
|
260
296
|
if (queryIsOkAlone) {
|
|
261
297
|
return alternates ? alternateQueries : [`${query}`];
|
|
@@ -263,26 +299,6 @@ export function bibTitleAuthorPublisher({record, onlyTitleLength, addYear = fals
|
|
|
263
299
|
return [];
|
|
264
300
|
}
|
|
265
301
|
|
|
266
|
-
function getTitle() {
|
|
267
|
-
const [field] = record.get(/^245$/u);
|
|
268
|
-
|
|
269
|
-
if (field) {
|
|
270
|
-
const titleString = field.subfields
|
|
271
|
-
//.filter(({code}) => ['a', 'b', 'n', 'p'].includes(code))
|
|
272
|
-
.filter(({code}) => ['a', 'b'].includes(code))
|
|
273
|
-
//.filter(({code}) => ['a'].includes(code))
|
|
274
|
-
.map(({value}) => testStringOrNumber(value) ? String(value) : '')
|
|
275
|
-
.filter(value => value)
|
|
276
|
-
// In Melinda's index subfield separators are indexed as ' '
|
|
277
|
-
.join(' ');
|
|
278
|
-
|
|
279
|
-
if (/^[1-9]$/u.test(field.ind2)) { // Skip non-filing characters
|
|
280
|
-
return titleString.slice(parseInt(field.ind2));
|
|
281
|
-
}
|
|
282
|
-
return titleString;
|
|
283
|
-
}
|
|
284
|
-
return false;
|
|
285
|
-
}
|
|
286
302
|
}
|
|
287
303
|
|
|
288
304
|
export function bibAuthors(record) {
|
|
@@ -349,7 +365,10 @@ export function bibPublishers(record) {
|
|
|
349
365
|
.slice(0, 30)
|
|
350
366
|
.trim();
|
|
351
367
|
|
|
352
|
-
debugData(`Publisher string: ${formatted}`);
|
|
368
|
+
debugData(`Publisher string: '${formatted}'`);
|
|
369
|
+
if (formatted.length === 0) {
|
|
370
|
+
return [];
|
|
371
|
+
}
|
|
353
372
|
// use non-wildcard word search from dc.publisher
|
|
354
373
|
return [`dc.publisher="${formatted}"`];
|
|
355
374
|
}
|
|
@@ -358,7 +377,7 @@ export function bibPublishers(record) {
|
|
|
358
377
|
|
|
359
378
|
function getPublisher(record) {
|
|
360
379
|
//debugData(record);
|
|
361
|
-
const [field] = record.get(/^
|
|
380
|
+
const [field] = record.get(/^26[04]$/u).filter(f => f.subfields.some(sf => sf.code === 'b')); // Filter removes copyright-only fields
|
|
362
381
|
//debugData(field);
|
|
363
382
|
|
|
364
383
|
if (field) {
|
|
@@ -367,7 +386,8 @@ export function bibPublishers(record) {
|
|
|
367
386
|
.map(({value}) => testStringOrNumber(value) ? String(value) : '')
|
|
368
387
|
.filter(value => value)
|
|
369
388
|
// In Melinda's index subfield separators are indexed as ' '
|
|
370
|
-
.join(' ')
|
|
389
|
+
.join(' ')
|
|
390
|
+
.trim();
|
|
371
391
|
return publisherString;
|
|
372
392
|
}
|
|
373
393
|
return false;
|
|
@@ -387,14 +407,17 @@ export function bibYear(record) {
|
|
|
387
407
|
|
|
388
408
|
function getYear(record) {
|
|
389
409
|
const [f008] = record.get(/^008$/u);
|
|
390
|
-
if (f008
|
|
391
|
-
debug('f008 missing');
|
|
410
|
+
if (!f008 || !f008.value || f008.value.length < 11) {
|
|
411
|
+
debug('f008 missing/crappy');
|
|
392
412
|
return false;
|
|
393
413
|
}
|
|
394
414
|
|
|
395
415
|
debugData(`f008: ${JSON.stringify(f008)}`);
|
|
396
416
|
const {value} = f008;
|
|
397
|
-
|
|
417
|
+
if (value === '||||' || value === ' ') { // Meaningless values
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
return value.slice(7, 11);
|
|
398
421
|
}
|
|
399
422
|
}
|
|
400
423
|
|
|
@@ -1,40 +1,117 @@
|
|
|
1
1
|
import createDebugLogger from 'debug';
|
|
2
2
|
import {promisify} from 'util';
|
|
3
3
|
import {toQueries} from '../candidate-search-utils.js';
|
|
4
|
-
import {getSubfieldValues, testStringOrNumber, toMelindaIds} from '../../matching-utils.js';
|
|
4
|
+
import {getSubfieldValues, stringAfter, stringBefore, testStringOrNumber, toMelindaIds} from '../../matching-utils.js';
|
|
5
|
+
import {fieldToString, uniqArray} from '@natlibfi/marc-record-validators-melinda/dist/utils.js';
|
|
5
6
|
|
|
6
7
|
const setTimeoutPromise = promisify(setTimeout); // eslint-disable-line
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
debug(`Creating query for the Melinda Id host`);
|
|
9
|
+
const debug = createDebugLogger('@natlibfi/melinda-record-matching:candidate-search:query:component');
|
|
10
|
+
const debugData = debug.extend('data'); // eslint-disable-line
|
|
11
|
+
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
|
|
14
|
+
export function parse773g(field) {
|
|
15
|
+
const g = field?.subfields?.find(sf => sf.code === 'g'); // NB! returns the first $g
|
|
16
|
+
|
|
17
|
+
if (!g) {
|
|
18
|
+
return {};
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
return [];
|
|
21
|
+
const value = normalizeValue(g.value);
|
|
20
22
|
|
|
21
|
-
|
|
22
|
-
//
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
return {
|
|
24
|
+
// NB! We are not currently interested in volume (vuosikerta)
|
|
25
|
+
year: gToYear(value) || null,
|
|
26
|
+
number: gToNumber(value) || null,
|
|
27
|
+
pages: gToPages(value) || null // Might also be eg. Raita 5. However, returns only the number part
|
|
28
|
+
}
|
|
25
29
|
|
|
26
|
-
|
|
27
|
-
|
|
30
|
+
function normalizeValue(value) {
|
|
31
|
+
return value.replace(/(?:\. -|\.)$/u, '');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function gToNumber(value) {
|
|
35
|
+
// Allow up to 3-digit numbers
|
|
36
|
+
const extract1 = value.replace(/^.*: ([1-9][0-9]?[0-9]), (?:p\.|page|pp\.|s\.|Seite|sida|sidor|sivu).*$/ui, '$1');
|
|
37
|
+
if (extract1 !== value) {
|
|
38
|
+
return extract1;
|
|
28
39
|
}
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
29
42
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
43
|
+
function gToPages(value) {
|
|
44
|
+
if (value.match(/^[0-9]+(?:-[0-9]+)?(?:, [0-9]+(?:-[0-9]+)?)*$/u)) {
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
const numberPartOnly = value.replace(/^.*(?:p\.|raidat|raita|s\.|Seite|sivut?|pages?) /ui, '');
|
|
48
|
+
if (numberPartOnly !== value) {
|
|
49
|
+
return gToPages(numberPartOnly);
|
|
50
|
+
}
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
function gToYear(value) {
|
|
56
|
+
debug(`EXTRACT YEAR FROM '${value}`);
|
|
57
|
+
// extract year from within parentheses: $g vsk (yyyy) AND $g (YYYY)
|
|
58
|
+
if (value.match(/^[1-9][0-9]?[0-9]? ?\((?:20[012][0-9]|19[0-9][0-9])\)/u) || value.match(/^\((?:20[012][0-9]|19[0-9][0-9])\)/u) ) {
|
|
59
|
+
return stringBefore(stringAfter(value, '('), ')');
|
|
60
|
+
}
|
|
61
|
+
// If volume is missing, the year often seems to come without them parentheses:
|
|
62
|
+
if (value.match(/^(?:20[012][0-9]|19[0-9][0-9]) :/u)) {
|
|
63
|
+
return stringBefore(value, ' ');
|
|
64
|
+
}
|
|
65
|
+
// Seen in SB $g (vsk)year joulukuu
|
|
66
|
+
if (value.match(/^\([1-9][0-9]?\) ?(?:20[012][0-9]|19[0-9][0-9])[^0-9]/)) {
|
|
67
|
+
return value.replace(/^\([0-9]+\) ?/u, '').replace(/[^0-9].*$/u, '');
|
|
68
|
+
}
|
|
69
|
+
// Some magazines use DD.MM.YYYY
|
|
70
|
+
if (value.match(/^[^0-9]*(?:[1-9]|[12][0-9]|30|31)\.(?:1[012]|[1-9])\. ?(?:20[012][0-9]|19[0-9][0-9])[^0-9]/u)) {
|
|
71
|
+
return value.replace(/^[^0-9]*[0-9]+\.[0-9]+\. ?/u, '').replace(/[^0-9].*$/u, '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function extractPublicationYearFrom773(field) {
|
|
79
|
+
const data = parse773g(field);
|
|
80
|
+
debug(`Extracted year ${data.year} from f773$g ${fieldToString(field)}`);
|
|
81
|
+
return data.year;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isMelindaSubfieldW(subfield) {
|
|
85
|
+
if (subfield.code !== 'w') {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
return (/^\((?:FI-MELINDA|FIN01)\)0[0-9]{8}$/ui).test(subfield.value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function removeMelindaPrefixFromValue(value) {
|
|
92
|
+
return value.replace(/^(?:\(FI-MELINDA\)|\(FIN01\))/, '');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getHostMelindaFields(record) {
|
|
96
|
+
return record.get(/^773$/u).filter(f => f.subfields.some(sf => isMelindaSubfieldW(sf)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function extractHostMelindaIdsFromField(field) {
|
|
100
|
+
return field.subfields.filter(sf => isMelindaSubfieldW(sf)).map(sf => removeMelindaPrefixFromValue(sf.value));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function getHostMelindaIds(record) {
|
|
104
|
+
return uniqArray(getHostMelindaFields(record).map(f => extractHostMelindaIdsFromField(f)).flat());
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function hostIdMelinda(record) { // old function with replaced code
|
|
108
|
+
debug(`Creating query for the Melinda Id host`);
|
|
109
|
+
const ids = getHostMelindaIds(record);
|
|
110
|
+
if (ids.length === 0) {
|
|
111
|
+
debug(`No valid Melinda Id host found`);
|
|
112
|
+
return [];
|
|
37
113
|
}
|
|
114
|
+
return toQueries(getHostMelindaIds(record), 'melinda.partsofhost');
|
|
38
115
|
}
|
|
39
116
|
|
|
40
117
|
export async function hostIdOtherSource(record, client) {
|
package/src/cli.js
CHANGED
|
@@ -76,7 +76,8 @@ async function cli() {
|
|
|
76
76
|
if (['IDS'].includes(searchType)) {
|
|
77
77
|
return [
|
|
78
78
|
matchDetection.features.bib.melindaId(),
|
|
79
|
-
matchDetection.features.bib.allSourceIds()
|
|
79
|
+
matchDetection.features.bib.allSourceIds(),
|
|
80
|
+
matchDetection.features.bib.f773() // Host data
|
|
80
81
|
];
|
|
81
82
|
}
|
|
82
83
|
|
|
@@ -101,7 +102,8 @@ async function cli() {
|
|
|
101
102
|
// normal publicationTime and a reprintPublication time
|
|
102
103
|
matchDetection.features.bib.publicationTimeAllowConsYearsMulti(),
|
|
103
104
|
matchDetection.features.bib.language(),
|
|
104
|
-
matchDetection.features.bib.bibliographicLevel()
|
|
105
|
+
matchDetection.features.bib.bibliographicLevel(),
|
|
106
|
+
matchDetection.features.bib.f773() // Host data
|
|
105
107
|
];
|
|
106
108
|
}
|
|
107
109
|
|
|
@@ -113,7 +115,8 @@ async function cli() {
|
|
|
113
115
|
matchDetection.features.bib.title(),
|
|
114
116
|
matchDetection.features.bib.language(),
|
|
115
117
|
matchDetection.features.bib.authors(),
|
|
116
|
-
matchDetection.features.bib.bibliographicLevel()
|
|
118
|
+
matchDetection.features.bib.bibliographicLevel(),
|
|
119
|
+
matchDetection.features.bib.f773() // Host data
|
|
117
120
|
];
|
|
118
121
|
}
|
|
119
122
|
|
|
@@ -128,7 +131,8 @@ async function cli() {
|
|
|
128
131
|
matchDetection.features.bib.recordType(),
|
|
129
132
|
matchDetection.features.bib.publicationTime(),
|
|
130
133
|
matchDetection.features.bib.language(),
|
|
131
|
-
matchDetection.features.bib.bibliographicLevel()
|
|
134
|
+
matchDetection.features.bib.bibliographicLevel(),
|
|
135
|
+
matchDetection.features.bib.f773() // Host data
|
|
132
136
|
];
|
|
133
137
|
}
|
|
134
138
|
|