mirror of
https://github.com/zotero/zotero.git
synced 2026-08-28 05:25:31 +00:00
Match duplicate books by ISBN-10/13 equivalence, veto book sections by container ISBN
ISBNs were compared as raw cleaned strings, so a book catalogued with ISBN-10 and a duplicate catalogued with the equivalent ISBN-13 never matched. Canonicalize to ISBN-13 (Zotero.Utilities.toISBN13) before comparing. Also cache ISBNs for book sections. A section's ISBN identifies its containing book, not the section, so sections aren't unioned directly by ISBN, but the cached value now lets the title/creator match veto false positives between same-titled sections from different books. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
786cdea884
commit
ae36d93653
2 changed files with 80 additions and 0 deletions
|
|
@ -211,6 +211,8 @@ Zotero.Duplicates.prototype._findDuplicates = async function () {
|
|||
let row = rows[i];
|
||||
let newVal = Zotero.Utilities.cleanISBN('' + row.value);
|
||||
if (!newVal) continue;
|
||||
// Canonicalize to ISBN-13 so an ISBN-10 and its ISBN-13 equivalent match
|
||||
newVal = Zotero.Utilities.toISBN13(newVal);
|
||||
isbnCache[row.itemID] = newVal;
|
||||
newRows.push({
|
||||
itemID: row.itemID,
|
||||
|
|
@ -221,6 +223,28 @@ Zotero.Duplicates.prototype._findDuplicates = async function () {
|
|||
processRows(newRows);
|
||||
}
|
||||
|
||||
// Also cache ISBNs of book sections. Their ISBN identifies the containing book rather
|
||||
// than the section itself, so book sections aren't unioned directly by ISBN, but the
|
||||
// cached value is still used below to veto false title/creator matches.
|
||||
var sql = "SELECT itemID, value FROM items JOIN itemData USING (itemID) "
|
||||
+ "JOIN itemDataValues USING (valueID) "
|
||||
+ "WHERE libraryID=? AND itemTypeID=? AND fieldID=? "
|
||||
+ "AND itemID NOT IN (SELECT itemID FROM deletedItems)";
|
||||
var rows = await Zotero.DB.queryAsync(
|
||||
sql,
|
||||
[
|
||||
this._libraryID,
|
||||
Zotero.ItemTypes.getID('bookSection'),
|
||||
Zotero.ItemFields.getID('ISBN')
|
||||
]
|
||||
);
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
let row = rows[i];
|
||||
let newVal = Zotero.Utilities.cleanISBN('' + row.value);
|
||||
if (!newVal) continue;
|
||||
isbnCache[row.itemID] = Zotero.Utilities.toISBN13(newVal);
|
||||
}
|
||||
|
||||
// DOI
|
||||
var sql = "SELECT itemID, value FROM items JOIN itemData USING (itemID) "
|
||||
+ "JOIN itemDataValues USING (valueID) "
|
||||
|
|
|
|||
|
|
@ -110,4 +110,60 @@ describe("Duplicate Items", function () {
|
|||
assert.sameMembers(item3.relatedItems, [item1.key]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("ISBN matching", function () {
|
||||
async function getDuplicateSets() {
|
||||
var duplicates = new Zotero.Duplicates(Zotero.Libraries.userLibraryID);
|
||||
var search = await duplicates.getSearchObject();
|
||||
return search.search();
|
||||
}
|
||||
|
||||
it("should match books with equivalent ISBN-10 and ISBN-13", async function () {
|
||||
var item1 = await createDataObject('item', { itemType: 'book', title: 'Effective Java' });
|
||||
item1.setField('ISBN', '0134685997');
|
||||
await item1.saveTx();
|
||||
|
||||
var item2 = await createDataObject('item', { itemType: 'book', title: 'Effective Java, 3rd Edition' });
|
||||
item2.setField('ISBN', '9780134685991');
|
||||
await item2.saveTx();
|
||||
|
||||
var ids = await getDuplicateSets();
|
||||
assert.include(ids, item1.id);
|
||||
assert.include(ids, item2.id);
|
||||
});
|
||||
|
||||
it("should not match books with different ISBNs and titles", async function () {
|
||||
var item1 = await createDataObject('item', { itemType: 'book', title: 'Clean Code' });
|
||||
item1.setField('ISBN', '0062316095');
|
||||
await item1.saveTx();
|
||||
|
||||
var item2 = await createDataObject('item', { itemType: 'book', title: 'Learning JavaScript' });
|
||||
item2.setField('ISBN', '0596009089');
|
||||
await item2.saveTx();
|
||||
|
||||
var ids = await getDuplicateSets();
|
||||
assert.notInclude(ids, item1.id);
|
||||
assert.notInclude(ids, item2.id);
|
||||
});
|
||||
|
||||
it("should not match book sections with the same title/creator but different container ISBNs", async function () {
|
||||
var item1 = await createDataObject(
|
||||
'item',
|
||||
{ itemType: 'bookSection', title: 'Introduction', creators: [{ lastName: 'Smith', firstName: 'John', creatorType: 'author' }] }
|
||||
);
|
||||
item1.setField('ISBN', '0123456789');
|
||||
await item1.saveTx();
|
||||
|
||||
var item2 = await createDataObject(
|
||||
'item',
|
||||
{ itemType: 'bookSection', title: 'Introduction', creators: [{ lastName: 'Smith', firstName: 'John', creatorType: 'author' }] }
|
||||
);
|
||||
item2.setField('ISBN', '0987654322');
|
||||
await item2.saveTx();
|
||||
|
||||
var ids = await getDuplicateSets();
|
||||
assert.notInclude(ids, item1.id);
|
||||
assert.notInclude(ids, item2.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue