@natlibfi/melinda-record-matching 5.0.1-alpha.1 → 5.0.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.
Files changed (40) hide show
  1. package/dist/candidate-search/index.js +6 -2
  2. package/dist/candidate-search/index.js.map +2 -2
  3. package/dist/candidate-search/query-list/bib.js +10 -2
  4. package/dist/candidate-search/query-list/bib.js.map +3 -3
  5. package/dist/cli.js +1 -1
  6. package/dist/cli.js.map +2 -2
  7. package/dist/index.js +2 -2
  8. package/dist/index.js.map +2 -2
  9. package/dist/match-detection/features/bib/authors.js +2 -2
  10. package/dist/match-detection/features/bib/authors.js.map +2 -2
  11. package/dist/match-detection/features/bib/bibliographic-level.js +12 -1
  12. package/dist/match-detection/features/bib/bibliographic-level.js.map +2 -2
  13. package/dist/match-detection/features/bib/host-component.js +1 -1
  14. package/dist/match-detection/features/bib/host-component.js.map +2 -2
  15. package/dist/match-detection/features/bib/isbn.js +122 -15
  16. package/dist/match-detection/features/bib/isbn.js.map +2 -2
  17. package/dist/match-detection/features/bib/issn.js +62 -5
  18. package/dist/match-detection/features/bib/issn.js.map +2 -2
  19. package/dist/match-detection/features/bib/language.js +176 -59
  20. package/dist/match-detection/features/bib/language.js.map +3 -3
  21. package/dist/match-detection/features/bib/title-version-original.js +2 -2
  22. package/dist/match-detection/features/bib/title-version-original.js.map +2 -2
  23. package/dist/match-detection/features/bib/title.js +2 -2
  24. package/dist/match-detection/features/bib/title.js.map +2 -2
  25. package/dist/match-detection/index.js +5 -5
  26. package/dist/match-detection/index.js.map +2 -2
  27. package/package.json +9 -4
  28. package/src/candidate-search/index.js +7 -2
  29. package/src/candidate-search/query-list/bib.js +22 -15
  30. package/src/cli.js +1 -1
  31. package/src/index.js +3 -3
  32. package/src/match-detection/features/bib/authors.js +2 -2
  33. package/src/match-detection/features/bib/bibliographic-level.js +13 -1
  34. package/src/match-detection/features/bib/host-component.js +1 -1
  35. package/src/match-detection/features/bib/isbn.js +162 -14
  36. package/src/match-detection/features/bib/issn.js +83 -1
  37. package/src/match-detection/features/bib/language.js +261 -73
  38. package/src/match-detection/features/bib/title-version-original.js +2 -2
  39. package/src/match-detection/features/bib/title.js +5 -4
  40. package/src/match-detection/index.js +5 -5
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/match-detection/features/bib/host-component.js"],
4
- "sourcesContent": ["\nexport default () => ({\n name: 'Host/Component record',\n extract: ({record}) => record.get(/^773$/u).length > 0 ? ['component'] : ['host'],\n compare: (a, b) => a[0] === b[0] ? 0.0 : -1.0\n});\n"],
5
- "mappings": "AACA,eAAe,OAAO;AAAA,EACpB,MAAM;AAAA,EACN,SAAS,CAAC,EAAC,OAAM,MAAM,OAAO,IAAI,QAAQ,EAAE,SAAS,IAAI,CAAC,WAAW,IAAI,CAAC,MAAM;AAAA,EAChF,SAAS,CAAC,GAAG,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,IAAM;AAC3C;",
4
+ "sourcesContent": ["\nexport default () => ({\n name: 'Host/Component record',\n extract: ({record}) => record.get(/^[79]73$/u).length > 0 ? ['component'] : ['host'],\n compare: (a, b) => a[0] === b[0] ? 0.0 : -1.0\n});\n"],
5
+ "mappings": "AACA,eAAe,OAAO;AAAA,EACpB,MAAM;AAAA,EACN,SAAS,CAAC,EAAC,OAAM,MAAM,OAAO,IAAI,WAAW,EAAE,SAAS,IAAI,CAAC,WAAW,IAAI,CAAC,MAAM;AAAA,EACnF,SAAS,CAAC,GAAG,MAAM,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,IAAM;AAC3C;",
6
6
  "names": []
7
7
  }
@@ -1,21 +1,128 @@
1
1
  import createDebugLogger from "debug";
2
2
  import { parse as isbnParse } from "isbn3";
3
- import createInterface from "./standard-identifier-factory.js";
3
+ import { uniqArray } from "./issn.js";
4
+ import { isComponentRecord } from "@natlibfi/melinda-commons";
4
5
  const debug = createDebugLogger(`@natlibfi/melinda-record-matching:match-detection:features:standard-identifiers:ISBN`);
5
6
  const debugData = debug.extend("data");
6
- export default () => {
7
- const IDENTIFIER_NAME = "ISBN";
8
- function validatorAndNormalizer(string) {
9
- const isbnParseResult = isbnParse(string);
10
- debugData(`isbnParseResult: ${JSON.stringify(isbnParseResult)}`);
11
- if (isbnParseResult === null) {
12
- debug(`Not parseable ISBN, just removing hyphens`);
13
- return { valid: false, value: string.replace(/-/ug, "") };
14
- }
15
- debug(`Parseable ISBN, normalizing to ISBN-13`);
16
- return { valid: true, value: isbnParseResult.isbn13 };
7
+ const MAX_SCORE = 0.75;
8
+ export default () => ({
9
+ name: "ISBN",
10
+ extract: ({
11
+ record
12
+ /*, recordExternal*/
13
+ }) => {
14
+ const host = !isComponentRecord(record, false, []);
15
+ if (host) {
16
+ return record.get("020").filter((f) => f.subfields?.some((sf) => ["a", "z"].includes(sf.code) && sf.value));
17
+ }
18
+ return record.get("773").filter((f) => f.subfields?.some((sf) => ["z"].includes(sf.code) && sf.value));
19
+ },
20
+ // eslint-disable-next-line max-statements
21
+ compare: (aa, bb) => {
22
+ debugData(`Comparing ISBN sets ${JSON.stringify(aa)} and ${JSON.stringify(bb)}`);
23
+ if (aa.length === 0 || bb.length === 0) {
24
+ return 0;
25
+ }
26
+ const [subfieldCodeForGoodValues, subfieldCodeForBadValues] = getSubfieldCodes(aa[0].tag);
27
+ const [goodValuesA, badValuesA] = getValues(aa);
28
+ if (goodValuesA.length) {
29
+ debug(`GOOD VALUES (A): ${goodValuesA.join(", ")}`);
30
+ }
31
+ if (badValuesA.length) {
32
+ debug(`BAD VALUES (A): ${badValuesA.join(", ")}`);
33
+ }
34
+ const [goodValuesB, badValuesB] = getValues(bb);
35
+ if (goodValuesB.length) {
36
+ debug(`GOOD VALUES (B): ${goodValuesB.join(", ")}`);
37
+ }
38
+ if (badValuesB.length) {
39
+ debug(`BAD VALUES (B): ${badValuesB.join(", ")}`);
40
+ }
41
+ const [sharedGoodValues, goodValuesAOnly, goodValuesBOnly] = getUnionData(goodValuesA, goodValuesB);
42
+ const hitScore = scoreHit();
43
+ function scoreHit() {
44
+ if (sharedGoodValues.length > 0) {
45
+ return MAX_SCORE;
46
+ }
47
+ if (goodValuesA.some((valA) => badValuesB.includes(valA)) || goodValuesB.some((valB) => badValuesA.includes(valB))) {
48
+ return MAX_SCORE;
49
+ }
50
+ if (badValuesA.some((valA) => looksGood(valA) && badValuesB.includes(valA)) || badValuesB.some((valB) => looksGood(valB) && badValuesA.includes(valB))) {
51
+ return MAX_SCORE * 2 / 3;
52
+ }
53
+ return 0;
54
+ }
55
+ if (sharedGoodValues.length > 0) {
56
+ return scoreData(hitScore, 0.8, goodValuesAOnly.length + goodValuesBOnly.length);
57
+ }
58
+ if (hitScore === MAX_SCORE) {
59
+ return scoreData(hitScore, 0.8, goodValuesAOnly.length + goodValuesBOnly.length - 1);
60
+ }
61
+ if (hitScore > 0) {
62
+ return scoreData(hitScore, 0.8, goodValuesAOnly.length + goodValuesBOnly.length);
63
+ }
64
+ if (goodValuesA.length === 0 || goodValuesB === 0) {
65
+ return 0;
66
+ }
67
+ return -0.75;
68
+ function getSubfieldCodes(tag) {
69
+ if (tag === "773") {
70
+ return ["z", void 0];
71
+ }
72
+ return ["a", "z"];
73
+ }
74
+ function looksGood(val) {
75
+ if (/^([0-9]-?){9}[0-9X]$/u.test(val)) {
76
+ return true;
77
+ }
78
+ if (/^([0-9]-?){12}[0-9]$/u.test(val)) {
79
+ return true;
80
+ }
81
+ return false;
82
+ }
83
+ function getValues(fields) {
84
+ const goodValues = fields.flatMap((f) => f.subfields.filter((sf) => sf.code === subfieldCodeForGoodValues)).map((sf) => validatorAndNormalizer(sf.value));
85
+ const trueGoodValues = goodValues.filter((val) => val.valid).map((val) => val.value);
86
+ const wannabeGoodValues = goodValues.filter((val) => !val.valid).map((val) => val.value);
87
+ if (!subfieldCodeForBadValues) {
88
+ return [trueGoodValues, wannabeGoodValues];
89
+ }
90
+ const badValues = fields.flatMap((f) => f.subfields.filter((sf) => sf.code === subfieldCodeForBadValues)).map((sf) => sf.value);
91
+ return [uniqArray(trueGoodValues), uniqArray([...wannabeGoodValues, ...badValues])];
92
+ }
93
+ function validatorAndNormalizer(string) {
94
+ const string2 = string.replace(/\. -$/u, "");
95
+ const string3 = string2.replace(/ .*$/u, "");
96
+ if (string2 !== string3) {
97
+ const altResult = validatorAndNormalizer(string3);
98
+ if (altResult.valid) {
99
+ return altResult;
100
+ }
101
+ }
102
+ const isbnParseResult = isbnParse(string2, "") || "";
103
+ debugData(`isbnParseResult: ${JSON.stringify(isbnParseResult)}`);
104
+ if (!isbnParseResult.isValid) {
105
+ debug(`Not parseable ISBN '${string2}', just removing hyphens`);
106
+ return { valid: false, value: string2.replace(/-/ug, "") };
107
+ }
108
+ debug(`Parseable ISBN '${string2}', normalizing to ISBN-13 '${isbnParseResult.isbn13}'`);
109
+ return { valid: true, value: isbnParseResult.isbn13 };
110
+ }
111
+ }
112
+ });
113
+ function getUnionData(set1, set2) {
114
+ const shared = set1.filter((val) => set2.includes(val));
115
+ const onlyInSet1 = set1.filter((val) => !shared.includes(val));
116
+ const onlyInSet2 = set2.filter((val) => !shared.includes(val));
117
+ return [shared, onlyInSet1, onlyInSet2];
118
+ }
119
+ function scoreData(score, factor, n) {
120
+ return innerScoreData(score, n);
121
+ function innerScoreData(currScore, remaining) {
122
+ if (remaining > 0) {
123
+ return innerScoreData(currScore * factor, remaining - 1);
124
+ }
125
+ return Math.round(currScore * 100) / 100;
17
126
  }
18
- const { extract, compare } = createInterface({ identifier: IDENTIFIER_NAME, pattern: /^020$/u, subfieldCodes: ["a", "z"], validIdentifierSubfieldCodes: ["a"], invalidIdentifierSubfieldCodes: ["z"], validatorAndNormalizer });
19
- return { extract, compare, name: IDENTIFIER_NAME };
20
- };
127
+ }
21
128
  //# sourceMappingURL=isbn.js.map
@@ -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';\nimport createInterface from './standard-identifier-factory.js';\n\nconst debug = createDebugLogger(`@natlibfi/melinda-record-matching:match-detection:features:standard-identifiers:ISBN`);\nconst debugData = debug.extend('data');\n\nexport default () => {\n const IDENTIFIER_NAME = 'ISBN';\n\n function validatorAndNormalizer(string) {\n const isbnParseResult = isbnParse(string);\n debugData(`isbnParseResult: ${JSON.stringify(isbnParseResult)}`);\n if (isbnParseResult === null) {\n debug(`Not parseable ISBN, just removing hyphens`);\n return {valid: false, value: string.replace(/-/ug, '')};\n }\n debug(`Parseable ISBN, normalizing to ISBN-13`);\n return {valid: true, value: isbnParseResult.isbn13};\n }\n\n const {extract, compare} = createInterface({identifier: IDENTIFIER_NAME, pattern: /^020$/u, subfieldCodes: ['a', 'z'], validIdentifierSubfieldCodes: ['a'], invalidIdentifierSubfieldCodes: ['z'], validatorAndNormalizer});\n return {extract, compare, name: IDENTIFIER_NAME};\n};\n\n"],
5
- "mappings": "AACA,OAAO,uBAAuB;AAC9B,SAAQ,SAAS,iBAAgB;AACjC,OAAO,qBAAqB;AAE5B,MAAM,QAAQ,kBAAkB,sFAAsF;AACtH,MAAM,YAAY,MAAM,OAAO,MAAM;AAErC,eAAe,MAAM;AACnB,QAAM,kBAAkB;AAExB,WAAS,uBAAuB,QAAQ;AACtC,UAAM,kBAAkB,UAAU,MAAM;AACxC,cAAU,oBAAoB,KAAK,UAAU,eAAe,CAAC,EAAE;AAC/D,QAAI,oBAAoB,MAAM;AAC5B,YAAM,2CAA2C;AACjD,aAAO,EAAC,OAAO,OAAO,OAAO,OAAO,QAAQ,OAAO,EAAE,EAAC;AAAA,IACxD;AACA,UAAM,wCAAwC;AAC9C,WAAO,EAAC,OAAO,MAAM,OAAO,gBAAgB,OAAM;AAAA,EACpD;AAEA,QAAM,EAAC,SAAS,QAAO,IAAI,gBAAgB,EAAC,YAAY,iBAAiB,SAAS,UAAU,eAAe,CAAC,KAAK,GAAG,GAAG,8BAA8B,CAAC,GAAG,GAAG,gCAAgC,CAAC,GAAG,GAAG,uBAAsB,CAAC;AAC1N,SAAO,EAAC,SAAS,SAAS,MAAM,gBAAe;AACjD;",
4
+ "sourcesContent": ["\nimport createDebugLogger from 'debug';\nimport {parse as isbnParse} from 'isbn3';\n\nimport {uniqArray} from './issn.js';\nimport {isComponentRecord} from '@natlibfi/melinda-commons';\n\n\nconst debug = createDebugLogger(`@natlibfi/melinda-record-matching:match-detection:features:standard-identifiers:ISBN`);\nconst debugData = debug.extend('data');\n\nconst MAX_SCORE = 0.75;\n\nexport default () => ({\n name: 'ISBN',\n extract: ({record/*, recordExternal*/}) => {\n //const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n\n const host = !isComponentRecord(record, false, []);\n\n if (host) {\n return record.get('020').filter(f => f.subfields?.some(sf => ['a', 'z'].includes(sf.code) && sf.value));\n }\n return record.get('773').filter(f => f.subfields?.some(sf => ['z'].includes(sf.code) && sf.value));\n },\n // eslint-disable-next-line max-statements\n compare: (aa, bb) => {\n debugData(`Comparing ISBN sets ${JSON.stringify(aa)} and ${JSON.stringify(bb)}`);\n if (aa.length === 0 || bb.length === 0) {\n // No data for decision\n return 0;\n }\n\n const [subfieldCodeForGoodValues, subfieldCodeForBadValues] = getSubfieldCodes(aa[0].tag);\n\n const [goodValuesA, badValuesA] = getValues(aa);\n if (goodValuesA.length) {\n debug(`GOOD VALUES (A): ${goodValuesA.join(', ')}`);\n }\n if (badValuesA.length) {\n debug(`BAD VALUES (A): ${badValuesA.join(', ')}`);\n }\n\n const [goodValuesB, badValuesB] = getValues(bb);\n if (goodValuesB.length) {\n debug(`GOOD VALUES (B): ${goodValuesB.join(', ')}`);\n }\n if (badValuesB.length) {\n debug(`BAD VALUES (B): ${badValuesB.join(', ')}`);\n }\n\n const [sharedGoodValues, goodValuesAOnly, goodValuesBOnly] = getUnionData(goodValuesA, goodValuesB);\n\n //debug(`GOOD\\tBOTH: ${sharedGoodValues.length}, A only: ${goodValuesAOnly.length}, B only: ${goodValuesBOnly.length}`);\n\n const hitScore = scoreHit();\n\n function scoreHit() {\n if (sharedGoodValues.length > 0) {\n return MAX_SCORE;\n }\n // One record consider ISBN good and the other record considered it's canceled:\n if (goodValuesA.some(valA => badValuesB.includes(valA)) || goodValuesB.some(valB => badValuesA.includes(valB))) {\n return MAX_SCORE;\n }\n\n // Value is bad, but looks isbn-ish to a human eye (not validating the isbn again, note than invalid isbns in 020$a are considered bad):\n // Could happen for two canceled ISBNs for example. I'll give this two thirds of the full score\n if (badValuesA.some(valA => looksGood(valA) && badValuesB.includes(valA)) || badValuesB.some(valB => looksGood(valB) && badValuesA.includes(valB))) {\n return MAX_SCORE * 2 / 3;\n }\n\n return 0;\n }\n\n if (sharedGoodValues.length > 0) {\n // Third argument (aka 'N times') is >= 0\n return scoreData(hitScore, 0.8, goodValuesAOnly.length + goodValuesBOnly.length);\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 (goodValuesA.length === 0 || goodValuesB === 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\n return -0.75; // Has good ISBNs on both records, but they did not match\n\n\n function getSubfieldCodes(tag) {\n if (tag === '773') {\n return ['z', undefined];\n }\n return ['a', 'z'];\n }\n\n function looksGood(val) {\n // isbn10 can end in X:\n if (/^([0-9]-?){9}[0-9X]$/u.test(val)) {\n return true;\n }\n // isbn13 can not:\n if (/^([0-9]-?){12}[0-9]$/u.test(val)) {\n return true;\n }\n return false;\n }\n\n function getValues(fields) {\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 => sf.value);\n return [uniqArray(trueGoodValues), uniqArray([...wannabeGoodValues, ...badValues])];\n }\n\n function validatorAndNormalizer(string) {\n const string2 = string.replace(/\\. -$/u, ''); // Remove punctuation (773$z)\n\n // Hack: Historically we 020$a \"1234567890 sidottu\" etc. Try the LHS alone:\n const string3 = string2.replace(/ .*$/u, '');\n if (string2 !== string3) {\n const altResult = validatorAndNormalizer(string3);\n if (altResult.valid) {\n return altResult;\n }\n }\n\n const isbnParseResult = isbnParse(string2, '') || '';\n debugData(`isbnParseResult: ${JSON.stringify(isbnParseResult)}`);\n if (!isbnParseResult.isValid) {\n debug(`Not parseable ISBN '${string2}', just removing hyphens`);\n return {valid: false, value: string2.replace(/-/ug, '')};\n }\n\n debug(`Parseable ISBN '${string2}', normalizing to ISBN-13 '${isbnParseResult.isbn13}'`);\n return {valid: true, value: isbnParseResult.isbn13};\n\n\n }\n }\n});\n\n// These are outside the default function as I'll probably want to export these later on\n\nfunction getUnionData(set1, set2) {\n const shared = set1.filter(val => set2.includes(val));\n const onlyInSet1 = set1.filter(val => !shared.includes(val));\n const onlyInSet2 = set2.filter(val => !shared.includes(val));\n return [shared, onlyInSet1, onlyInSet2];\n}\n\nfunction scoreData(score, factor, n) {\n return innerScoreData(score, n);\n function innerScoreData(currScore, remaining) {\n if (remaining > 0) {\n return innerScoreData(currScore * factor, remaining-1);\n }\n return Math.round(currScore * 100)/100; // 0.600000000001 => 0.6\n }\n}"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,63 @@
1
- import createInterface from "./standard-identifier-factory.js";
2
- export default () => {
3
- const { extract, compare } = createInterface({ pattern: /^022$/u, subfieldCodes: ["a", "z", "y"] });
4
- return { extract, compare, name: "ISSN" };
5
- };
1
+ import createDebugLogger from "debug";
2
+ import { isComponentRecord } from "@natlibfi/melinda-commons";
3
+ const debug = createDebugLogger("@natlibfi/melinda-record-matching:match-detection:features:issn");
4
+ const debugData = debug.extend("data");
5
+ export default () => ({
6
+ name: "ISSN",
7
+ extract: ({
8
+ record
9
+ /*, recordExternal*/
10
+ }) => {
11
+ const isHost = !isComponentRecord(record, false, []);
12
+ return getIssns();
13
+ function getIssns() {
14
+ const fields = getRelevantFields();
15
+ if (fields.length === 0) {
16
+ return [];
17
+ }
18
+ debug(` ${fields.length} potential ISSN fields (${fields[0].tag})`);
19
+ const subfieldCodes = getRelevantSubfieldCodes();
20
+ const subfieldValues = fields.flatMap((f) => f.subfields.filter((sf) => subfieldCodes.includes(sf.code)).map((sf) => sf.value));
21
+ const validSubfieldValues = subfieldValues?.map((val) => normalizeSubfieldValue(val)).filter((val) => isValidIssn(val));
22
+ if (!validSubfieldValues) {
23
+ return [];
24
+ }
25
+ return uniqArray(validSubfieldValues);
26
+ }
27
+ function getRelevantFields() {
28
+ if (isHost) {
29
+ return record.get(/^022$/u);
30
+ }
31
+ return record.get(/^[79]73$/u);
32
+ }
33
+ function normalizeSubfieldValue(val) {
34
+ return val.replace(/[., :;].*$/u, "");
35
+ }
36
+ function isValidIssn(val) {
37
+ return /^[0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9X]$/u.test(val);
38
+ }
39
+ function getRelevantSubfieldCodes() {
40
+ if (isHost) {
41
+ return ["a", "y", "z"];
42
+ }
43
+ return ["x"];
44
+ }
45
+ },
46
+ // eslint-disable-next-line max-statements
47
+ compare: (aa, bb) => {
48
+ debugData(`Comparing ISSN sets ${JSON.stringify(aa)} and ${JSON.stringify(bb)}`);
49
+ if (aa.length === 0 || bb.length === 0) {
50
+ return 0;
51
+ }
52
+ const firstSharedIssn = aa.find((val) => bb.includes(val));
53
+ if (firstSharedIssn) {
54
+ debug(` Shared ISSN found: '${firstSharedIssn}'`);
55
+ return 0.2;
56
+ }
57
+ return -0.2;
58
+ }
59
+ });
60
+ export function uniqArray(arr) {
61
+ return arr.filter((val, i) => arr.indexOf(val) === i);
62
+ }
6
63
  //# 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": ["\nimport createInterface from './standard-identifier-factory.js';\n\nexport default () => {\n const {extract, compare} = createInterface({pattern: /^022$/u, subfieldCodes: ['a', 'z', 'y']});\n return {extract, compare, name: 'ISSN'};\n};\n"],
5
- "mappings": "AACA,OAAO,qBAAqB;AAE5B,eAAe,MAAM;AACnB,QAAM,EAAC,SAAS,QAAO,IAAI,gBAAgB,EAAC,SAAS,UAAU,eAAe,CAAC,KAAK,KAAK,GAAG,EAAC,CAAC;AAC9F,SAAO,EAAC,SAAS,SAAS,MAAM,OAAM;AACxC;",
4
+ "sourcesContent": ["/*\n//import createInterface from './standard-identifier-factory.js';\n\n\nexport default () => {\n const {extract, compare} = createInterface({pattern: /^022$/u, subfieldCodes: ['a', 'z', 'y']});\n return {extract, compare, name: 'ISSN'};\n};\n*/\n\n\nimport createDebugLogger from 'debug';\n\nimport {isComponentRecord} from '@natlibfi/melinda-commons';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:issn');\nconst debugData = debug.extend('data');\n\nexport default () => ({\n name: 'ISSN',\n extract: ({record/*, recordExternal*/}) => {\n //const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n\n const isHost = !isComponentRecord(record, false, []);\n\n // debug(`\\t Is HOST: '${isHost}'`);\n return getIssns();\n\n function getIssns() {\n const fields = getRelevantFields();\n if (fields.length === 0) {\n return [];\n }\n debug(`\\t ${fields.length} potential ISSN fields (${fields[0].tag})`);\n const subfieldCodes = getRelevantSubfieldCodes();\n //debug(`\\t subfield codes: '${subfieldCodes.join(\"', '\")}'`);\n const subfieldValues = fields.flatMap(f => f.subfields.filter(sf => subfieldCodes.includes(sf.code)).map(sf => sf.value));\n //debug(`\\t cand values: '${subfieldValues.join(\"', '\")}'`);\n // Stripping punctuaction with substring here is pretty quick and dirty approach...\n const validSubfieldValues = subfieldValues?.map(val => normalizeSubfieldValue(val)).filter(val => isValidIssn(val));\n if (!validSubfieldValues) {\n return [];\n }\n return uniqArray(validSubfieldValues);\n }\n\n function getRelevantFields() {\n if (isHost) {\n return record.get(/^022$/u);\n }\n return record.get(/^[79]73$/u);\n }\n\n function normalizeSubfieldValue(val) {\n return val.replace(/[., :;].*$/u, '');\n }\n\n function isValidIssn(val) {\n return /^[0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9X]$/u.test(val);\n }\n\n function getRelevantSubfieldCodes() {\n if (isHost) {\n return ['a', 'y', 'z'];\n }\n return ['x'];\n }\n },\n // eslint-disable-next-line max-statements\n compare: (aa, bb) => {\n debugData(`Comparing ISSN sets ${JSON.stringify(aa)} and ${JSON.stringify(bb)}`);\n if (aa.length === 0 || bb.length === 0) {\n // No data for decision\n return 0;\n }\n const firstSharedIssn = aa.find(val => bb.includes(val));\n if (firstSharedIssn) {\n debug(`\\t Shared ISSN found: '${firstSharedIssn}'`);\n // Maybe less for comps?\n return 0.2;\n }\n return -0.2;\n\n }\n});\n\nexport function uniqArray(arr) {\n return arr.filter((val, i) => arr.indexOf(val) === i);\n}"],
5
+ "mappings": "AAWA,OAAO,uBAAuB;AAE9B,SAAQ,yBAAwB;AAEhC,MAAM,QAAQ,kBAAkB,iEAAiE;AACjG,MAAM,YAAY,MAAM,OAAO,MAAM;AAErC,eAAe,OAAO;AAAA,EACpB,MAAM;AAAA,EACN,SAAS,CAAC;AAAA,IAAC;AAAA;AAAA,EAA0B,MAAM;AAGzC,UAAM,SAAS,CAAC,kBAAkB,QAAQ,OAAO,CAAC,CAAC;AAGnD,WAAO,SAAS;AAEhB,aAAS,WAAW;AAClB,YAAM,SAAS,kBAAkB;AACjC,UAAI,OAAO,WAAW,GAAG;AACvB,eAAO,CAAC;AAAA,MACV;AACA,YAAM,KAAM,OAAO,MAAM,2BAA2B,OAAO,CAAC,EAAE,GAAG,GAAG;AACpE,YAAM,gBAAgB,yBAAyB;AAE/C,YAAM,iBAAiB,OAAO,QAAQ,OAAK,EAAE,UAAU,OAAO,QAAM,cAAc,SAAS,GAAG,IAAI,CAAC,EAAE,IAAI,QAAM,GAAG,KAAK,CAAC;AAGxH,YAAM,sBAAsB,gBAAgB,IAAI,SAAO,uBAAuB,GAAG,CAAC,EAAE,OAAO,SAAO,YAAY,GAAG,CAAC;AAClH,UAAI,CAAC,qBAAqB;AACxB,eAAO,CAAC;AAAA,MACV;AACA,aAAO,UAAU,mBAAmB;AAAA,IACtC;AAEA,aAAS,oBAAoB;AAC3B,UAAI,QAAQ;AACV,eAAO,OAAO,IAAI,QAAQ;AAAA,MAC5B;AACA,aAAO,OAAO,IAAI,WAAW;AAAA,IAC/B;AAEA,aAAS,uBAAuB,KAAK;AACnC,aAAO,IAAI,QAAQ,eAAe,EAAE;AAAA,IACtC;AAEA,aAAS,YAAY,KAAK;AACxB,aAAO,gDAAgD,KAAK,GAAG;AAAA,IACjE;AAEA,aAAS,2BAA2B;AAClC,UAAI,QAAQ;AACV,eAAO,CAAC,KAAK,KAAK,GAAG;AAAA,MACvB;AACA,aAAO,CAAC,GAAG;AAAA,IACb;AAAA,EACF;AAAA;AAAA,EAEA,SAAS,CAAC,IAAI,OAAO;AACnB,cAAU,uBAAuB,KAAK,UAAU,EAAE,CAAC,QAAQ,KAAK,UAAU,EAAE,CAAC,EAAE;AAC/E,QAAI,GAAG,WAAW,KAAK,GAAG,WAAW,GAAG;AAEtC,aAAO;AAAA,IACT;AACA,UAAM,kBAAkB,GAAG,KAAK,SAAO,GAAG,SAAS,GAAG,CAAC;AACvD,QAAI,iBAAiB;AACnB,YAAM,yBAA0B,eAAe,GAAG;AAElD,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EAET;AACF;AAEO,gBAAS,UAAU,KAAK;AAC7B,SAAO,IAAI,OAAO,CAAC,KAAK,MAAM,IAAI,QAAQ,GAAG,MAAM,CAAC;AACtD;",
6
6
  "names": []
7
7
  }
@@ -1,4 +1,6 @@
1
1
  import createDebugLogger from "debug";
2
+ import { MarcRecord } from "@natlibfi/marc-record";
3
+ import { FixSami041, Remove041zxx } from "@natlibfi/marc-record-validators-melinda";
2
4
  import { getMatchCounts } from "../../../matching-utils.js";
3
5
  const debug = createDebugLogger("@natlibfi/melinda-record-matching:match-detection:features:language");
4
6
  const debugData = debug.extend("data");
@@ -6,75 +8,190 @@ export default () => ({
6
8
  name: "Language",
7
9
  extract: ({ record, recordExternal }) => {
8
10
  const label = recordExternal && recordExternal.label ? recordExternal.label : "record";
9
- const value008 = get008Value();
10
- const values041 = get041Values();
11
- debugData(`${label}: 008: ${JSON.stringify(value008)}, 041: ${JSON.stringify(values041)}`);
12
- if (!value008 && values041.length < 1) {
13
- debugData(`${label}: No actual values found`);
14
- return [];
11
+ const clonedRecord = new MarcRecord(record, record._validationOptions);
12
+ FixSami041().fix(clonedRecord);
13
+ Remove041zxx().fix(clonedRecord);
14
+ return [{ leader: clonedRecord.leader, fields: clonedRecord.fields }, label];
15
+ },
16
+ // eslint-disable-next-line max-statements
17
+ compare: (aa, bb) => {
18
+ const [a, aLabel] = aa;
19
+ const [b, bLabel] = bb;
20
+ debugData(`Comparing language ${JSON.stringify(a)} and ${JSON.stringify(b)}`);
21
+ const score008 = compare008();
22
+ const score041 = compare041();
23
+ return applyLimits(score008 + score041);
24
+ function applyLimits(score) {
25
+ if (score > 0.1) {
26
+ return 0.1;
27
+ }
28
+ if (score < -1) {
29
+ return -1;
30
+ }
31
+ return score;
15
32
  }
16
- const allValues = value008 === void 0 ? values041 : values041.concat(value008);
17
- const uniqueSortedValues = [...new Set(allValues)].sort();
18
- return uniqueSortedValues;
19
- function get008Value() {
20
- const value = record.get(/^008$/u)?.[0]?.value || void 0;
21
- debugData(`${label}: 008 value: ${value}`);
22
- if (!value) {
23
- return void 0;
24
- }
25
- const code = value.slice(35, 38);
26
- debugData(`${label}: 008 code: ${code}`);
27
- return isLangCodeForALanguage(code) ? code : void 0;
33
+ function compare008() {
34
+ const a008 = get008Value(a, aLabel);
35
+ const b008 = get008Value(b, bLabel);
36
+ if (a008 === void 0 || b008 === void 0) {
37
+ return 0;
38
+ }
39
+ if (containsNoData(a008) || containsNoData(b008)) {
40
+ return 0;
41
+ }
42
+ if (a008 === b008) {
43
+ return 0.05;
44
+ }
45
+ if (a008 === "mul" || b008 === "mul") {
46
+ return 0;
47
+ }
48
+ return -0.2;
28
49
  }
29
- function get041Values() {
30
- return record.get(/^041$/u).filter(({ ind2 }) => ind2 === " ").map(({ subfields }) => subfields).flat().filter(({ code }) => code === "a" || code === "d").filter(({ value }) => value && isLangCodeForALanguage(value)).map(({ value }) => value);
50
+ function compare041() {
51
+ const a041s = getFields041(a);
52
+ const b041s = getFields041(b);
53
+ if (a041s.length === 0 || b041s.length === 0) {
54
+ return 0;
55
+ }
56
+ if (a041s.length === 1 && b041s.length === 1) {
57
+ const sourcePenalty = getSourceOfLanguageCode(a041s[0]) === getSourceOfLanguageCode(b041s[0]) ? 0 : -0.05;
58
+ const mainPenalty = compareLanguageCodeLists(getFieldsLanguageCodes(a041s[0]), getFieldsLanguageCodes(b041s[0]));
59
+ const scoreInd1 = mainPenalty === 0 ? 0 : indicator1Penalty(a041s[0], b041s[0]);
60
+ return mainPenalty + sourcePenalty + scoreInd1;
61
+ }
62
+ return compareLanguageCodeLists(getLanguageCodesFrom041Fields(a), getLanguageCodesFrom041Fields(b));
63
+ function getFields041(record) {
64
+ if (!record.fields) {
65
+ return [];
66
+ }
67
+ return record.fields.filter((f) => f.tag === "041");
68
+ }
31
69
  }
32
- function isLangCodeForALanguage(code) {
33
- if (code.length !== 3) {
34
- debugData(`Code ${code} is not correct length (3) for a language code.`);
35
- return false;
70
+ function getSourceOfLanguageCode(field) {
71
+ const sf2 = field.subfields.find((sf) => sf.code === "2");
72
+ if (!sf2) {
73
+ return "MARC";
36
74
  }
37
- if (code === "|||" || code === " " || code === "^^^" || code === "mul" || code === "zxx") {
38
- debugData(`Code ${code} is not code for a spesific language.`);
39
- return false;
75
+ if (sf2.value.match(/639.2/u)) {
76
+ return "ISO 639-2";
40
77
  }
41
- const langCodePattern = /^[a-z][a-z][a-z]$/ui;
42
- if (!langCodePattern.test(code)) {
43
- debugData(`Code ${code} is not valid as a language code`);
44
- return false;
78
+ if (sf2.value.match(/639.3/u)) {
79
+ return "ISO 639-3";
45
80
  }
46
- return true;
81
+ return sf2.value;
47
82
  }
48
- },
49
- // eslint-disable-next-line max-statements
50
- compare: (a, b) => {
51
- debugData(`Comparing ${JSON.stringify(a)} and ${JSON.stringify(b)}`);
52
- if (a.length === 0 || b.length === 0) {
53
- debugData(`No language to compare`);
54
- return 0;
83
+ function indicator1Penalty(aField, bField) {
84
+ if (aField.ind1 === " " || bField.ind1 === " " || aField.ind1 === bField.ind1) {
85
+ return 0;
86
+ }
87
+ debug(` Indicator penalty: '${aField.ind1}' vs '${bField.ind1}'`);
88
+ return -0.1;
55
89
  }
56
- if (a.length === b.length && a.every((element, index) => element === b[index])) {
57
- debugData(`All languages match`);
58
- return 0.1;
90
+ function containsNoData(languageCode) {
91
+ return [" ", "|||", "und"].includes(languageCode);
59
92
  }
60
- const { matchingValues, possibleMatchValues, maxValues } = getMatchCounts(a, b);
61
- if (matchingValues < 1) {
62
- debug(`Both have languages, but none of these match.`);
63
- return -1;
93
+ function compareLanguageCodeLists(a2, b2) {
94
+ if (a2.length === 0 || b2.length === 0) {
95
+ debugData(`No language to compare`);
96
+ return 0;
97
+ }
98
+ const samiLanguageCodes = ["sma", "sme", "smj", "smn", "sms"];
99
+ if (a2.includes("smi") && b2.includes("smi")) {
100
+ if (a2.some((code) => samiLanguageCodes.includes(code)) && !b2.some((code) => samiLanguageCodes.includes(code))) {
101
+ return compareLanguageCodeLists(a2.filter((val) => !samiLanguageCodes.includes(val)), b2);
102
+ }
103
+ if (b2.some((code) => samiLanguageCodes.includes(code)) && !a2.some((code) => samiLanguageCodes.includes(code))) {
104
+ return compareLanguageCodeLists(a2, b2.filter((val) => !samiLanguageCodes.includes(val)));
105
+ }
106
+ }
107
+ if (a2.length === b2.length && a2.every((element, index) => element === b2[index])) {
108
+ debugData(`All languages match`);
109
+ return 0.1;
110
+ }
111
+ if (a2.length === 1 && a2[0] === "mul" && b2.length > 1) {
112
+ return 0;
113
+ }
114
+ if (b2.length === 1 && b2[0] === "mul" && a2.length > 1) {
115
+ return 0;
116
+ }
117
+ const sharedValues = getSharedValues(a2, b2);
118
+ const aOnly = a2.filter((val) => !sharedValues.includes(val));
119
+ const bOnly = b2.filter((val) => !sharedValues.includes(val));
120
+ const hasUnd = [...aOnly, ...bOnly].includes("und");
121
+ if (sharedValues.length < 1) {
122
+ console.info(`NV: ${sharedValues.join(", ")}`);
123
+ if (aOnly.length === 1 && bOnly.length === 1 && hasUnd) {
124
+ debug(`Both have languages, but none of these match. However, the benefit of doubt is given: '${aOnly[0]}' and '${bOnly[0]}' might mean the same}`);
125
+ return 0;
126
+ }
127
+ debug(`Both have languages, but none of these match.`);
128
+ return -1;
129
+ }
130
+ const { matchingValues, possibleMatchValues, maxValues } = getMatchCounts(a2, b2);
131
+ debug(`Both have languages, ${matchingValues}/${possibleMatchValues} valid languages match.`);
132
+ debug(`Possible matches: ${possibleMatchValues}/${maxValues}`);
133
+ const missingCount = maxValues - possibleMatchValues;
134
+ const misMatchCount = possibleMatchValues - matchingValues;
135
+ debug(` missing: ${missingCount}`);
136
+ debug(` mismatches: ${misMatchCount}`);
137
+ const penaltyForMissing = 0.02 * (maxValues - possibleMatchValues);
138
+ const penaltyForMisMatch = 0.05 * (possibleMatchValues - matchingValues);
139
+ debug(` points: penaltyForMissing: ${penaltyForMissing}`);
140
+ debug(` points: penaltyForMisMatch: ${penaltyForMisMatch}`);
141
+ const points = Number(Number(0.1 - penaltyForMisMatch - penaltyForMissing).toFixed(2));
142
+ debug(`Total points: ${points}`);
143
+ return points;
64
144
  }
65
- debug(`Both have languages, ${matchingValues}/${possibleMatchValues} valid languages match.`);
66
- debug(`Possible matches: ${possibleMatchValues}/${maxValues}`);
67
- const missingCount = maxValues - possibleMatchValues;
68
- const misMatchCount = possibleMatchValues - matchingValues;
69
- debug(` missing: ${missingCount}`);
70
- debug(` mismatches: ${misMatchCount}`);
71
- const penaltyForMissing = 0.02 * (maxValues - possibleMatchValues);
72
- const penaltyForMisMatch = 0.05 * (possibleMatchValues - matchingValues);
73
- debug(` points: penaltyForMissing: ${penaltyForMissing}`);
74
- debug(` points: penaltyForMisMatch: ${penaltyForMisMatch}`);
75
- const points = Number(Number(0.1 - penaltyForMisMatch - penaltyForMissing).toFixed(2));
76
- debug(`Total points: ${points}`);
77
- return points;
78
145
  }
79
146
  });
147
+ function get008Value(record, label = "record") {
148
+ if (!record || !record.fields) {
149
+ return;
150
+ }
151
+ const f008 = record.fields.find((f) => f.tag === "008");
152
+ if (!f008) {
153
+ return void 0;
154
+ }
155
+ const value = f008.value || defaultValue;
156
+ if (!value) {
157
+ debugData(`${label}: Failed to extact 008/35-37 value from '${value}'`);
158
+ return defaultValue;
159
+ }
160
+ const code = value.slice(35, 38);
161
+ debugData(`${label}: 008 code: '${code}'`);
162
+ return code;
163
+ }
164
+ function isLangCodeForALanguage(code, label = "record", encoding = void 0) {
165
+ if (!code) {
166
+ return false;
167
+ }
168
+ if ([void 0, "ISO 639-2", "ISO-639-3"].includes(encoding) && code.length !== 3) {
169
+ debugData(`${label}: Code ${code} is not correct length (3) for a language code.`);
170
+ return false;
171
+ }
172
+ if (!encoding) {
173
+ if (code === "|||" || code === " ") {
174
+ debugData(`${label}: Code ${code} is not code for a spesific language.`);
175
+ return false;
176
+ }
177
+ }
178
+ const langCodePattern = /^[a-z][a-z][a-z]$/ui;
179
+ if (!langCodePattern.test(code)) {
180
+ debugData(`${label}: Code ${code} is not valid as a language code`);
181
+ return false;
182
+ }
183
+ return true;
184
+ }
185
+ function getLanguageCodesFrom041Fields(record) {
186
+ const values = record.fields.filter((f) => f.tag === "041").flatMap((f) => getFieldsLanguageCodes(f));
187
+ return [...new Set(values)].sort();
188
+ }
189
+ function getFieldsLanguageCodes(field) {
190
+ const relevantSubfields = field.subfields.filter((sf) => sf.code === "a" || sf.code === "d").filter((sf) => isLangCodeForALanguage(sf.value, "field"));
191
+ const values = relevantSubfields.map((sf) => sf.value).flat();
192
+ return [...new Set(values)].sort();
193
+ }
194
+ function getSharedValues(list1, list2) {
195
+ return list1.filter((val) => list2.includes(val));
196
+ }
80
197
  //# sourceMappingURL=language.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/match-detection/features/bib/language.js"],
4
- "sourcesContent": ["\nimport createDebugLogger from 'debug';\nimport {getMatchCounts} from '../../../matching-utils.js';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:language');\nconst debugData = debug.extend('data');\n\nexport default () => ({\n name: 'Language',\n extract: ({record, recordExternal}) => {\n const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n\n const value008 = get008Value();\n const values041 = get041Values();\n debugData(`${label}: 008: ${JSON.stringify(value008)}, 041: ${JSON.stringify(values041)}`);\n\n if (!value008 && values041.length < 1) {\n debugData(`${label}: No actual values found`);\n return [];\n }\n\n const allValues = value008 === undefined ? values041 : values041.concat(value008);\n const uniqueSortedValues = [...new Set(allValues)].sort();\n\n return uniqueSortedValues;\n\n function get008Value() {\n const value = record.get(/^008$/u)?.[0]?.value || undefined;\n debugData(`${label}: 008 value: ${value}`);\n\n if (!value) {\n return undefined;\n }\n\n const code = value.slice(35, 38);\n debugData(`${label}: 008 code: ${code}`);\n return isLangCodeForALanguage(code) ? code : undefined;\n }\n\n // Uses only f041s that have 2nd ind ' ', which means that the codes used are MARC 21 language codes\n\n function get041Values() {\n return record.get(/^041$/u)\n .filter(({ind2}) => ind2 === ' ')\n .map(({subfields}) => subfields)\n .flat()\n .filter(({code}) => code === 'a' || code === 'd')\n .filter(({value}) => value && isLangCodeForALanguage(value))\n .map(({value}) => value);\n }\n\n // Check if a string is a possible, validly formed language code for a single language\n // Currently accept also codes in capitals\n function isLangCodeForALanguage(code) {\n if (code.length !== 3) {\n debugData(`Code ${code} is not correct length (3) for a language code.`);\n return false;\n }\n if (code === '|||' || code === ' ' || code === '^^^' || code === 'mul' || code === 'zxx') {\n debugData(`Code ${code} is not code for a spesific language.`);\n return false;\n }\n const langCodePattern = /^[a-z][a-z][a-z]$/ui;\n if (!langCodePattern.test(code)) {\n debugData(`Code ${code} is not valid as a language code`);\n return false;\n }\n return true;\n }\n\n },\n // eslint-disable-next-line max-statements\n compare: (a, b) => {\n debugData(`Comparing ${JSON.stringify(a)} and ${JSON.stringify(b)}`);\n\n if (a.length === 0 || b.length === 0) {\n debugData(`No language to compare`);\n return 0;\n }\n\n if (a.length === b.length && a.every((element, index) => element === b[index])) {\n debugData(`All languages match`);\n return 0.1;\n }\n\n const {matchingValues, possibleMatchValues, maxValues} = getMatchCounts(a, b);\n\n if (matchingValues < 1) {\n debug(`Both have languages, but none of these match.`);\n return -1.0;\n }\n debug(`Both have languages, ${matchingValues}/${possibleMatchValues} valid languages match.`);\n // ignore non-matches if there is mismatching amount of values\n debug(`Possible matches: ${possibleMatchValues}/${maxValues}`);\n //we give some kind of penalty for mismatching amount of values instead of simple divide?\n const missingCount = maxValues - possibleMatchValues;\n const misMatchCount = possibleMatchValues - matchingValues;\n debug(`\\t missing: ${missingCount}`);\n debug(`\\t mismatches: ${misMatchCount}`);\n\n const penaltyForMissing = 0.02 * (maxValues - possibleMatchValues);\n const penaltyForMisMatch = 0.05 * (possibleMatchValues - matchingValues);\n debug(`\\t points: penaltyForMissing: ${penaltyForMissing}`);\n debug(`\\t points: penaltyForMisMatch: ${penaltyForMisMatch}`);\n\n const points = Number(Number(0.1 - penaltyForMisMatch - penaltyForMissing).toFixed(2));\n debug(`Total points: ${points}`);\n\n return points;\n }\n});\n"],
5
- "mappings": "AACA,OAAO,uBAAuB;AAC9B,SAAQ,sBAAqB;AAE7B,MAAM,QAAQ,kBAAkB,qEAAqE;AACrG,MAAM,YAAY,MAAM,OAAO,MAAM;AAErC,eAAe,OAAO;AAAA,EACpB,MAAM;AAAA,EACN,SAAS,CAAC,EAAC,QAAQ,eAAc,MAAM;AACrC,UAAM,QAAQ,kBAAkB,eAAe,QAAQ,eAAe,QAAQ;AAE9E,UAAM,WAAW,YAAY;AAC7B,UAAM,YAAY,aAAa;AAC/B,cAAU,GAAG,KAAK,UAAU,KAAK,UAAU,QAAQ,CAAC,UAAU,KAAK,UAAU,SAAS,CAAC,EAAE;AAEzF,QAAI,CAAC,YAAY,UAAU,SAAS,GAAG;AACrC,gBAAU,GAAG,KAAK,0BAA0B;AAC5C,aAAO,CAAC;AAAA,IACV;AAEA,UAAM,YAAY,aAAa,SAAY,YAAY,UAAU,OAAO,QAAQ;AAChF,UAAM,qBAAqB,CAAC,GAAG,IAAI,IAAI,SAAS,CAAC,EAAE,KAAK;AAExD,WAAO;AAEP,aAAS,cAAc;AACrB,YAAM,QAAQ,OAAO,IAAI,QAAQ,IAAI,CAAC,GAAG,SAAS;AAClD,gBAAU,GAAG,KAAK,gBAAgB,KAAK,EAAE;AAEzC,UAAI,CAAC,OAAO;AACV,eAAO;AAAA,MACT;AAEA,YAAM,OAAO,MAAM,MAAM,IAAI,EAAE;AAC/B,gBAAU,GAAG,KAAK,eAAe,IAAI,EAAE;AACvC,aAAO,uBAAuB,IAAI,IAAI,OAAO;AAAA,IAC/C;AAIA,aAAS,eAAe;AACtB,aAAO,OAAO,IAAI,QAAQ,EACvB,OAAO,CAAC,EAAC,KAAI,MAAM,SAAS,GAAG,EAC/B,IAAI,CAAC,EAAC,UAAS,MAAM,SAAS,EAC9B,KAAK,EACL,OAAO,CAAC,EAAC,KAAI,MAAM,SAAS,OAAO,SAAS,GAAG,EAC/C,OAAO,CAAC,EAAC,MAAK,MAAM,SAAS,uBAAuB,KAAK,CAAC,EAC1D,IAAI,CAAC,EAAC,MAAK,MAAM,KAAK;AAAA,IAC3B;AAIA,aAAS,uBAAuB,MAAM;AACpC,UAAI,KAAK,WAAW,GAAG;AACrB,kBAAU,QAAQ,IAAI,iDAAiD;AACvE,eAAO;AAAA,MACT;AACA,UAAI,SAAS,SAAS,SAAS,SAAS,SAAS,SAAS,SAAS,SAAS,SAAS,OAAO;AAC1F,kBAAU,QAAQ,IAAI,uCAAuC;AAC7D,eAAO;AAAA,MACT;AACA,YAAM,kBAAkB;AACxB,UAAI,CAAC,gBAAgB,KAAK,IAAI,GAAG;AAC/B,kBAAU,QAAQ,IAAI,kCAAkC;AACxD,eAAO;AAAA,MACT;AACA,aAAO;AAAA,IACT;AAAA,EAEF;AAAA;AAAA,EAEA,SAAS,CAAC,GAAG,MAAM;AACjB,cAAU,aAAa,KAAK,UAAU,CAAC,CAAC,QAAQ,KAAK,UAAU,CAAC,CAAC,EAAE;AAEnE,QAAI,EAAE,WAAW,KAAK,EAAE,WAAW,GAAG;AACpC,gBAAU,wBAAwB;AAClC,aAAO;AAAA,IACT;AAEA,QAAI,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,CAAC,SAAS,UAAU,YAAY,EAAE,KAAK,CAAC,GAAG;AAC9E,gBAAU,qBAAqB;AAC/B,aAAO;AAAA,IACT;AAEA,UAAM,EAAC,gBAAgB,qBAAqB,UAAS,IAAI,eAAe,GAAG,CAAC;AAE5E,QAAI,iBAAiB,GAAG;AACtB,YAAM,+CAA+C;AACrD,aAAO;AAAA,IACT;AACA,UAAM,wBAAwB,cAAc,IAAI,mBAAmB,yBAAyB;AAE5F,UAAM,qBAAqB,mBAAmB,IAAI,SAAS,EAAE;AAE7D,UAAM,eAAe,YAAY;AACjC,UAAM,gBAAgB,sBAAsB;AAC5C,UAAM,cAAe,YAAY,EAAE;AACnC,UAAM,iBAAkB,aAAa,EAAE;AAEvC,UAAM,oBAAoB,QAAQ,YAAY;AAC9C,UAAM,qBAAqB,QAAQ,sBAAsB;AACzD,UAAM,gCAAiC,iBAAiB,EAAE;AAC1D,UAAM,iCAAkC,kBAAkB,EAAE;AAE5D,UAAM,SAAS,OAAO,OAAO,MAAM,qBAAqB,iBAAiB,EAAE,QAAQ,CAAC,CAAC;AACrF,UAAM,iBAAiB,MAAM,EAAE;AAE/B,WAAO;AAAA,EACT;AACF;",
6
- "names": []
4
+ "sourcesContent": ["\nimport createDebugLogger from 'debug';\n\nimport {MarcRecord} from '@natlibfi/marc-record';\n\nimport {FixSami041, Remove041zxx} from '@natlibfi/marc-record-validators-melinda';\n\nimport {getMatchCounts} from '../../../matching-utils.js';\n\nconst debug = createDebugLogger('@natlibfi/melinda-record-matching:match-detection:features:language');\nconst debugData = debug.extend('data');\n\n// NB! 2025-12-17: I changed the logic drastically. However, I tried to keep the scores as same as possible.\n// 'extract' no longer extracts anything. Instead it clones the record, and does some preprocessing (using validators) instead.\n// 'compare' had multiple changes\n// - f041 ind1 differences ('0' vs '1') now result in a small penalty\n// - Two sami languages related changes:\n// -- validator adds a 'smi' subfield before a corresponding sma/sme/...subfield, if needed (national)\n// -- 'smi' only vs 'smi'+'sma' does not cause penalty\n// - 'mul' vs 'fin'+'swe' does not cause a penalty. However, 'mul' vs 'fin' alone triggers penalty.\n// - 008/35-37 and f041$a/$d are calculated separately\n// - a threshold is applied: return value is always between -1.0 and +0.1 (as it was before)\n// - we try to handle 041 $2 ISO 639-2 and ISO 639-3 as well.\n// - 'und' vs one other language does not cause a penalty in certain contexts. In other contexts it's treated as a normal \"language code\". (Tune/alleviate penalties later on.)\n\nexport default () => ({\n name: 'Language',\n extract: ({record, recordExternal}) => {\n const label = recordExternal && recordExternal.label ? recordExternal.label : 'record';\n const clonedRecord = new MarcRecord(record, record._validationOptions); // clone(record); // NB! This loses record.get()...\n\n FixSami041().fix(clonedRecord); // Handle 'smi' adding if needed\n Remove041zxx().fix(clonedRecord); // Remove 'zxx' from f041s\n // Add other language code normalizations?\n\n // Should we return all the fields, or just the relevant fields?\n return [{leader: clonedRecord.leader, fields: clonedRecord.fields}, label];\n },\n // eslint-disable-next-line max-statements\n compare: (aa, bb) => {\n const [a, aLabel] = aa;\n const [b, bLabel] = bb;\n debugData(`Comparing language ${JSON.stringify(a)} and ${JSON.stringify(b)}`);\n\n const score008 = compare008();\n const score041 = compare041();\n return applyLimits(score008 + score041);\n\n function applyLimits(score) {\n if (score > 0.1) { // Keeps the original max, we might have 0.10 (041) + 0.05 (008/35-37) = 0.15 now\n return 0.1;\n }\n if (score < -1) {\n return -1.0;\n }\n return score;\n }\n\n function compare008() {\n const a008 = get008Value(a, aLabel);\n const b008 = get008Value(b, bLabel);\n // Something seriously wrong with 008:\n if (a008 === undefined || b008 === undefined) {\n return 0.0; // Punish for generic badness?\n }\n\n if (containsNoData(a008) || containsNoData(b008)){\n // Nothing to compare\n return 0.0;\n }\n\n if (a008 === b008) {\n return 0.05;\n }\n\n if (a008 === 'mul' || b008 === 'mul') {\n return 0.0;\n }\n\n return -0.2;\n }\n\n\n\n\n\n function compare041() {\n const a041s = getFields041(a);\n const b041s = getFields041(b);\n if (a041s.length === 0 || b041s.length === 0) {\n return 0.0; // Should we punish these for badness?\n }\n if (a041s.length === 1 && b041s.length === 1) {\n // The language codes are typically same between different sources. Incur a small penalty for differences though...\n const sourcePenalty = getSourceOfLanguageCode(a041s[0]) === getSourceOfLanguageCode(b041s[0]) ? 0.0 : -0.05;\n const mainPenalty = compareLanguageCodeLists(getFieldsLanguageCodes(a041s[0]), getFieldsLanguageCodes(b041s[0]));\n const scoreInd1 = mainPenalty === 0.0 ? 0.0 : indicator1Penalty(a041s[0], b041s[0]);\n return mainPenalty + sourcePenalty + scoreInd1;\n }\n // Things are already complicated (likely to pe penalized, so no nedd to worrty about language code sources, I daresay.\n return compareLanguageCodeLists(getLanguageCodesFrom041Fields(a), getLanguageCodesFrom041Fields(b));\n\n function getFields041(record) {\n if (!record.fields) {\n return [];\n }\n return record.fields.filter(f => f.tag === '041');\n }\n\n }\n\n function getSourceOfLanguageCode(field) {\n const sf2 = field.subfields.find(sf => sf.code === '2');\n if (!sf2) {\n return 'MARC';\n }\n // Normalize the two relevant language code names:\n if (sf2.value.match(/639.2/u)) {\n return 'ISO 639-2';\n }\n if (sf2.value.match(/639.3/u)) {\n return 'ISO 639-3';\n }\n return sf2.value; // We don't actually have anything else in Melinda though\n }\n\n function indicator1Penalty(aField, bField) {\n if (aField.ind1 === ' ' || bField.ind1 === ' '|| aField.ind1 === bField.ind1 ) {\n return 0.0;\n }\n debug(`\\t Indicator penalty: '${aField.ind1}' vs '${bField.ind1}'`);\n return -0.1;\n }\n\n\n\n function containsNoData(languageCode) {\n return [' ', '|||', 'und'].includes(languageCode);\n }\n\n function compareLanguageCodeLists(a, b) {\n if (a.length === 0 || b.length === 0) {\n debugData(`No language to compare`);\n return 0;\n }\n\n // If either one of the sets has only the generic 'smi', remove the specific sami languages codes from the other as well:\n const samiLanguageCodes = ['sma', 'sme', 'smj', 'smn', 'sms']; // NB! Don't put generic 'smi' here!\n\n if (a.includes('smi') && b.includes('smi')) {\n if (a.some(code => samiLanguageCodes.includes(code)) && !b.some(code => samiLanguageCodes.includes(code))) {\n return compareLanguageCodeLists(a.filter(val => !samiLanguageCodes.includes(val)), b); // recurse\n }\n if (b.some(code => samiLanguageCodes.includes(code)) && !a.some(code => samiLanguageCodes.includes(code))) {\n return compareLanguageCodeLists(a, b.filter(val => !samiLanguageCodes.includes(val))); // recurse\n }\n }\n\n\n\n\n if (a.length === b.length && a.every((element, index) => element === b[index])) {\n debugData(`All languages match`);\n return 0.1;\n }\n\n // Handle 'mul'. (Should we check whether the other array contains mul as well, probably not...)\n // (If 'mul' does not appear alone, then it is very iffy... )\n if (a.length === 1 && a[0] === 'mul' && b.length > 1) {\n return 0;\n }\n if (b.length === 1 && b[0] === 'mul' && a.length > 1) {\n return 0;\n }\n\n\n\n // Damage control:\n\n // Not using the generic solution here as eg. 'und' needs a special treatment\n const sharedValues = getSharedValues(a, b);\n const aOnly = a.filter(val => !sharedValues.includes(val));\n const bOnly = b.filter(val => !sharedValues.includes(val));\n const hasUnd = [...aOnly, ...bOnly].includes('und');\n\n if (sharedValues.length < 1) {\n console.info(`NV: ${sharedValues.join(\", \")}`);\n if (aOnly.length === 1 && bOnly.length === 1 && hasUnd) {\n debug(`Both have languages, but none of these match. However, the benefit of doubt is given: '${aOnly[0]}' and '${bOnly[0]}' might mean the same}`);\n return 0;\n }\n debug(`Both have languages, but none of these match.`); // includes 'mul' vs a single language code\n return -1.0;\n }\n\n const {matchingValues, possibleMatchValues, maxValues} = getMatchCounts(a, b);\n\n debug(`Both have languages, ${matchingValues}/${possibleMatchValues} valid languages match.`);\n // ignore non-matches if there is mismatching amount of values\n debug(`Possible matches: ${possibleMatchValues}/${maxValues}`);\n // we give some kind of penalty for mismatching amount of values instead of simple divide?\n const missingCount = maxValues - possibleMatchValues;\n const misMatchCount = possibleMatchValues - matchingValues;\n debug(`\\t missing: ${missingCount}`);\n debug(`\\t mismatches: ${misMatchCount}`);\n\n const penaltyForMissing = 0.02 * (maxValues - possibleMatchValues);\n const penaltyForMisMatch = 0.05 * (possibleMatchValues - matchingValues);\n debug(`\\t points: penaltyForMissing: ${penaltyForMissing}`);\n debug(`\\t points: penaltyForMisMatch: ${penaltyForMisMatch}`);\n\n const points = Number(Number(0.1 - penaltyForMisMatch - penaltyForMissing).toFixed(2));\n debug(`Total points: ${points}`);\n\n return points;\n }\n }\n\n});\n\nfunction get008Value(record, label = 'record') {\n if (!record || !record.fields) {\n return;\n }\n const f008 = record.fields.find(f => f.tag === '008');\n if (!f008) {\n return undefined;\n }\n const value = f008.value || defaultValue;\n\n if (!value) {\n debugData(`${label}: Failed to extact 008/35-37 value from '${value}'`);\n return defaultValue;\n }\n\n const code = value.slice(35, 38);\n debugData(`${label}: 008 code: '${code}'`);\n return code;\n}\n\n// Check if a string is a possible, validly formed language code for a single language\n// Currently accept also codes in capitals\nfunction isLangCodeForALanguage(code, label = 'record', encoding = undefined) {\n if (!code) {\n return false;\n }\n\n if ([undefined, 'ISO 639-2', 'ISO-639-3'].includes(encoding) && code.length !== 3) {\n debugData(`${label}: Code ${code} is not correct length (3) for a language code.`);\n return false;\n }\n if (!encoding) {\n // 'mul' should be passed as 'mul' can match 'fin' + 'swe' etc.\n // 'zxx' should be removed by a validator\n // '^^^' is Aleph-specific corruption, not supporting it anymore\n if (code === '|||' || code === ' ' ) { // || code === '^^^' || code === 'mul' || code === 'zxx') {\n debugData(`${label}: Code ${code} is not code for a spesific language.`);\n return false;\n }\n }\n // Marc, ISO 639-2 and ISO 639-3 are all three-letters long. Not other language codes seen in Melinda:\n const langCodePattern = /^[a-z][a-z][a-z]$/ui;\n if (!langCodePattern.test(code)) {\n debugData(`${label}: Code ${code} is not valid as a language code`);\n return false;\n }\n return true;\n}\n\n\n\nfunction getLanguageCodesFrom041Fields(record) {\n // NB! We brutally don't check $2 (language code source) as marc's language codes is practically a subset of ISO 639-2,\n // and also ISO 639-2 and ISO 639-3 overlap to a degree. If we ever run into a trouble with some ISO 639-2 vs 639-3 mismatch, then we'll work it out.\n // Also we could write a validator that converts ISO 639-2 to marc if applicable and maybe even partial support for ISO-639-3.\n // Note that ISO 639-2-B values correspond with marc beteer that ISO 639-2-T. Eg. code for Chinese 'zho' should/code be normalized to 'chi'!\n const values = record.fields.filter(f => f.tag === '041').flatMap(f => getFieldsLanguageCodes(f));\n /*\n // .filter(({ind2}) => ind2 === ' ')\n .map(({subfields}) => subfields)\n .flat()\n .filter(({code}) => code === 'a' || code === 'd')\n .filter(({value}) => value && isLangCodeForALanguage(value))\n .map(({value}) => value);\n */\n return [...new Set(values)].sort();\n}\n\nfunction getFieldsLanguageCodes(field) {\n const relevantSubfields = field.subfields.filter(sf => sf.code === 'a' || sf.code === 'd').filter(sf => isLangCodeForALanguage(sf.value, 'field'));\n\n const values = relevantSubfields.map(sf => sf.value).flat();\n\n return [...new Set(values)].sort();\n}\n\nfunction getSharedValues(list1, list2) {\n return list1.filter(val => list2.includes(val));\n}"],
5
+ "mappings": "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",
6
+ "names": ["a", "b"]
7
7
  }