From a1484b99d6c23c9c75e9556916d0e804f6d3a3dd Mon Sep 17 00:00:00 2001 From: Ira Hopkinson Date: Tue, 11 Jul 2023 17:50:07 +1200 Subject: [PATCH 1/2] Use object instead of static class for `Canon` --- src/canon.ts | 765 ++++++++++++++++++++++++++------------------------- 1 file changed, 386 insertions(+), 379 deletions(-) diff --git a/src/canon.ts b/src/canon.ts index b639171..932b741 100644 --- a/src/canon.ts +++ b/src/canon.ts @@ -3,395 +3,402 @@ * https://github.com/sillsdev/libpalaso/blob/master/SIL.Scripture/Canon.cs */ +type BookNumbers = Record; + /** - * Canon information. Also, contains static information on complete list of books and localization. + * Array of all book IDs. + * BE SURE TO UPDATE ISCANONICAL above whenever you change this array. */ -// TODO: This class is only partially converted. Work out if we should use an object or a module? -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export abstract class Canon { - /** - * Array of all book IDs. - * BE SURE TO UPDATE ISCANONICAL above whenever you change this array. - */ - static readonly allBookIds: string[] = [ - 'GEN', - 'EXO', - 'LEV', - 'NUM', - 'DEU', - 'JOS', - 'JDG', - 'RUT', - '1SA', - '2SA', // 10 - - '1KI', - '2KI', - '1CH', - '2CH', - 'EZR', - 'NEH', - 'EST', - 'JOB', - 'PSA', - 'PRO', // 20 - - 'ECC', - 'SNG', - 'ISA', - 'JER', - 'LAM', - 'EZK', - 'DAN', - 'HOS', - 'JOL', - 'AMO', // 30 - - 'OBA', - 'JON', - 'MIC', - 'NAM', - 'HAB', - 'ZEP', - 'HAG', - 'ZEC', - 'MAL', - 'MAT', // 40 - - 'MRK', - 'LUK', - 'JHN', - 'ACT', - 'ROM', - '1CO', - '2CO', - 'GAL', - 'EPH', - 'PHP', // 50 - - 'COL', - '1TH', - '2TH', - '1TI', - '2TI', - 'TIT', - 'PHM', - 'HEB', - 'JAS', - '1PE', // 60 - - '2PE', - '1JN', - '2JN', - '3JN', - 'JUD', - 'REV', - 'TOB', - 'JDT', - 'ESG', - 'WIS', // 70 - - 'SIR', - 'BAR', - 'LJE', - 'S3Y', - 'SUS', - 'BEL', - '1MA', - '2MA', - '3MA', - '4MA', // 80 - - '1ES', - '2ES', - 'MAN', - 'PS2', - 'ODA', - 'PSS', - 'JSA', // actual variant text for JOS, now in LXA text - 'JDB', // actual variant text for JDG, now in LXA text - 'TBS', // actual variant text for TOB, now in LXA text - 'SST', // actual variant text for SUS, now in LXA text // 90 - - 'DNT', // actual variant text for DAN, now in LXA text - 'BLT', // actual variant text for BEL, now in LXA text - 'XXA', - 'XXB', - 'XXC', - 'XXD', - 'XXE', - 'XXF', - 'XXG', - 'FRT', // 100 - - 'BAK', - 'OTH', - '3ES', // Used previously but really should be 2ES - 'EZA', // Used to be called 4ES, but not actually in any known project - '5EZ', // Used to be called 5ES, but not actually in any known project - '6EZ', // Used to be called 6ES, but not actually in any known project - 'INT', - 'CNC', - 'GLO', - 'TDX', // 110 - - 'NDX', - 'DAG', - 'PS3', - '2BA', - 'LBA', - 'JUB', - 'ENO', - '1MQ', - '2MQ', - '3MQ', // 120 - - 'REP', - '4BA', - 'LAO', - ]; - - /** Array of all non-canonical book IDs. */ - static readonly nonCanonicalIds: string[] = [ - 'XXA', - 'XXB', - 'XXC', - 'XXD', - 'XXE', - 'XXF', - 'XXG', - 'FRT', - 'BAK', - 'OTH', - 'INT', - 'CNC', - 'GLO', - 'TDX', - 'NDX', - ]; - - static readonly firstBook = 1; - static readonly lastBook = Canon.allBookIds.length; - - /** - * Array of the English names of all books. - */ - private static readonly allBookEnglishNames: string[] = [ - 'Genesis', - 'Exodus', - 'Leviticus', - 'Numbers', - 'Deuteronomy', - 'Joshua', - 'Judges', - 'Ruth', - '1 Samuel', - '2 Samuel', - - '1 Kings', - '2 Kings', - '1 Chronicles', - '2 Chronicles', - 'Ezra', - 'Nehemiah', - 'Esther (Hebrew)', - 'Job', - 'Psalms', - 'Proverbs', - - 'Ecclesiastes', - 'Song of Songs', - 'Isaiah', - 'Jeremiah', - 'Lamentations', - 'Ezekiel', - 'Daniel (Hebrew)', - 'Hosea', - 'Joel', - 'Amos', - - 'Obadiah', - 'Jonah', - 'Micah', - 'Nahum', - 'Habakkuk', - 'Zephaniah', - 'Haggai', - 'Zechariah', - 'Malachi', - 'Matthew', - - 'Mark', - 'Luke', - 'John', - 'Acts', - 'Romans', - '1 Corinthians', - '2 Corinthians', - 'Galatians', - 'Ephesians', - 'Philippians', - - 'Colossians', - '1 Thessalonians', - '2 Thessalonians', - '1 Timothy', - '2 Timothy', - 'Titus', - 'Philemon', - 'Hebrews', - 'James', - '1 Peter', - - '2 Peter', - '1 John', - '2 John', - '3 John', - 'Jude', - 'Revelation', - 'Tobit', - 'Judith', - 'Esther Greek', - 'Wisdom of Solomon', - - 'Sirach (Ecclesiasticus)', - 'Baruch', - 'Letter of Jeremiah', - 'Song of 3 Young Men', - 'Susanna', - 'Bel and the Dragon', - '1 Maccabees', - '2 Maccabees', - '3 Maccabees', - '4 Maccabees', - - '1 Esdras (Greek)', - '2 Esdras (Latin)', - 'Prayer of Manasseh', - 'Psalm 151', - 'Odes', - 'Psalms of Solomon', - // WARNING, if you change the spelling of the *obsolete* tag be sure to update - // IsObsolete routine - 'Joshua A. *obsolete*', - 'Judges B. *obsolete*', - 'Tobit S. *obsolete*', - 'Susanna Th. *obsolete*', - - 'Daniel Th. *obsolete*', - 'Bel Th. *obsolete*', - 'Extra A', - 'Extra B', - 'Extra C', - 'Extra D', - 'Extra E', - 'Extra F', - 'Extra G', - 'Front Matter', - - 'Back Matter', - 'Other Matter', - '3 Ezra *obsolete*', - 'Apocalypse of Ezra', - '5 Ezra (Latin Prologue)', - '6 Ezra (Latin Epilogue)', - 'Introduction', - 'Concordance ', - 'Glossary ', - 'Topical Index', - - 'Names Index', - 'Daniel Greek', - 'Psalms 152-155', - '2 Baruch (Apocalypse)', - 'Letter of Baruch', - 'Jubilees', - 'Enoch', - '1 Meqabyan', - '2 Meqabyan', - '3 Meqabyan', - 'Reproof (Proverbs 25-31)', - - '4 Baruch (Rest of Baruch)', - 'Laodiceans', - ]; - - // Used for fast look up of book IDs to the book number. - private static readonly bookNumbers: BookNumbers = Canon.createBookNumbers(); - - /** - * Gets the 1-based number of the specified book. - * This is a fairly performance-critical method. - * @param id - 3-letter book ID, e.g. `'MAT'`. - * @param ignoreCase - should case be ignored. Defaults to `true`. - * @returns book number, or 0 if ID doesn't exist. - */ - static bookIdToNumber(id: string, ignoreCase = true): number { - if (ignoreCase) { - id = id.toUpperCase(); - } - if (!(id in this.bookNumbers)) { - return 0; - } - return this.bookNumbers[id]; - } +export const allBookIds: string[] = [ + 'GEN', + 'EXO', + 'LEV', + 'NUM', + 'DEU', + 'JOS', + 'JDG', + 'RUT', + '1SA', + '2SA', // 10 + + '1KI', + '2KI', + '1CH', + '2CH', + 'EZR', + 'NEH', + 'EST', + 'JOB', + 'PSA', + 'PRO', // 20 + + 'ECC', + 'SNG', + 'ISA', + 'JER', + 'LAM', + 'EZK', + 'DAN', + 'HOS', + 'JOL', + 'AMO', // 30 + + 'OBA', + 'JON', + 'MIC', + 'NAM', + 'HAB', + 'ZEP', + 'HAG', + 'ZEC', + 'MAL', + 'MAT', // 40 + + 'MRK', + 'LUK', + 'JHN', + 'ACT', + 'ROM', + '1CO', + '2CO', + 'GAL', + 'EPH', + 'PHP', // 50 + + 'COL', + '1TH', + '2TH', + '1TI', + '2TI', + 'TIT', + 'PHM', + 'HEB', + 'JAS', + '1PE', // 60 + + '2PE', + '1JN', + '2JN', + '3JN', + 'JUD', + 'REV', + 'TOB', + 'JDT', + 'ESG', + 'WIS', // 70 + + 'SIR', + 'BAR', + 'LJE', + 'S3Y', + 'SUS', + 'BEL', + '1MA', + '2MA', + '3MA', + '4MA', // 80 + + '1ES', + '2ES', + 'MAN', + 'PS2', + 'ODA', + 'PSS', + 'JSA', // actual variant text for JOS, now in LXA text + 'JDB', // actual variant text for JDG, now in LXA text + 'TBS', // actual variant text for TOB, now in LXA text + 'SST', // actual variant text for SUS, now in LXA text // 90 + + 'DNT', // actual variant text for DAN, now in LXA text + 'BLT', // actual variant text for BEL, now in LXA text + 'XXA', + 'XXB', + 'XXC', + 'XXD', + 'XXE', + 'XXF', + 'XXG', + 'FRT', // 100 + + 'BAK', + 'OTH', + '3ES', // Used previously but really should be 2ES + 'EZA', // Used to be called 4ES, but not actually in any known project + '5EZ', // Used to be called 5ES, but not actually in any known project + '6EZ', // Used to be called 6ES, but not actually in any known project + 'INT', + 'CNC', + 'GLO', + 'TDX', // 110 + + 'NDX', + 'DAG', + 'PS3', + '2BA', + 'LBA', + 'JUB', + 'ENO', + '1MQ', + '2MQ', + '3MQ', // 120 + + 'REP', + '4BA', + 'LAO', +]; + +/** Array of all non-canonical book IDs. */ +export const nonCanonicalIds: string[] = [ + 'XXA', + 'XXB', + 'XXC', + 'XXD', + 'XXE', + 'XXF', + 'XXG', + 'FRT', + 'BAK', + 'OTH', + 'INT', + 'CNC', + 'GLO', + 'TDX', + 'NDX', +]; + +export const firstBook = 1; +export const lastBook = allBookIds.length; + +/** Array of the English names of all books. */ +const allBookEnglishNames: string[] = [ + 'Genesis', + 'Exodus', + 'Leviticus', + 'Numbers', + 'Deuteronomy', + 'Joshua', + 'Judges', + 'Ruth', + '1 Samuel', + '2 Samuel', + + '1 Kings', + '2 Kings', + '1 Chronicles', + '2 Chronicles', + 'Ezra', + 'Nehemiah', + 'Esther (Hebrew)', + 'Job', + 'Psalms', + 'Proverbs', + + 'Ecclesiastes', + 'Song of Songs', + 'Isaiah', + 'Jeremiah', + 'Lamentations', + 'Ezekiel', + 'Daniel (Hebrew)', + 'Hosea', + 'Joel', + 'Amos', + + 'Obadiah', + 'Jonah', + 'Micah', + 'Nahum', + 'Habakkuk', + 'Zephaniah', + 'Haggai', + 'Zechariah', + 'Malachi', + 'Matthew', + + 'Mark', + 'Luke', + 'John', + 'Acts', + 'Romans', + '1 Corinthians', + '2 Corinthians', + 'Galatians', + 'Ephesians', + 'Philippians', + + 'Colossians', + '1 Thessalonians', + '2 Thessalonians', + '1 Timothy', + '2 Timothy', + 'Titus', + 'Philemon', + 'Hebrews', + 'James', + '1 Peter', + + '2 Peter', + '1 John', + '2 John', + '3 John', + 'Jude', + 'Revelation', + 'Tobit', + 'Judith', + 'Esther Greek', + 'Wisdom of Solomon', + + 'Sirach (Ecclesiasticus)', + 'Baruch', + 'Letter of Jeremiah', + 'Song of 3 Young Men', + 'Susanna', + 'Bel and the Dragon', + '1 Maccabees', + '2 Maccabees', + '3 Maccabees', + '4 Maccabees', + + '1 Esdras (Greek)', + '2 Esdras (Latin)', + 'Prayer of Manasseh', + 'Psalm 151', + 'Odes', + 'Psalms of Solomon', + // WARNING, if you change the spelling of the *obsolete* tag be sure to update + // IsObsolete routine + 'Joshua A. *obsolete*', + 'Judges B. *obsolete*', + 'Tobit S. *obsolete*', + 'Susanna Th. *obsolete*', + + 'Daniel Th. *obsolete*', + 'Bel Th. *obsolete*', + 'Extra A', + 'Extra B', + 'Extra C', + 'Extra D', + 'Extra E', + 'Extra F', + 'Extra G', + 'Front Matter', + + 'Back Matter', + 'Other Matter', + '3 Ezra *obsolete*', + 'Apocalypse of Ezra', + '5 Ezra (Latin Prologue)', + '6 Ezra (Latin Epilogue)', + 'Introduction', + 'Concordance ', + 'Glossary ', + 'Topical Index', + + 'Names Index', + 'Daniel Greek', + 'Psalms 152-155', + '2 Baruch (Apocalypse)', + 'Letter of Baruch', + 'Jubilees', + 'Enoch', + '1 Meqabyan', + '2 Meqabyan', + '3 Meqabyan', + 'Reproof (Proverbs 25-31)', + + '4 Baruch (Rest of Baruch)', + 'Laodiceans', +]; + +// Used for fast look up of book IDs to the book number. +const bookNumbers: BookNumbers = createBookNumbers(); - /** - * Gets the ID of a book from its book number. - * @param number - Book number (this is 1-based, not an index). - * @param errorValue - The string to return if the book number does not correspond to a valid book. - * Defaults to `'***'`. - * @returns The 3-letter `bookId` if found, or the `errorValue` otherwise. - */ - static bookNumberToId(number: number, errorValue = '***'): string { - const index: number = number - 1; - - if (index < 0 || index >= Canon.allBookIds.length) { - return errorValue; - } - - return Canon.allBookIds[index]; +/** + * Gets the 1-based number of the specified book. + * This is a fairly performance-critical method. + * @param id - 3-letter book ID, e.g. `'MAT'`. + * @param ignoreCase - should case be ignored. Defaults to `true`. + * @returns book number, or 0 if ID doesn't exist. + */ +export function bookIdToNumber(id: string, ignoreCase = true): number { + if (ignoreCase) { + id = id.toUpperCase(); } - - /** - * Gets the English book name from its book number. - * @param number - Book number (this is 1-based, not an index). - * @returns The English name of the book if found, or `'******'` otherwise. - */ - static bookNumberToEnglishName(number: number): string { - if (number <= 0 || number > this.lastBook) { - return '******'; - } - - return Canon.allBookEnglishNames[number - 1]; + if (!(id in bookNumbers)) { + return 0; } + return bookNumbers[id]; +} - /** - * Gets the English book name from its book ID. - * @param id - 3-letter book ID, e.g. `'MAT'`. - * @returns The English name of the book if found, or `'******'` otherwise. - */ - static bookIdToEnglishName(id: string): string { - return this.bookNumberToEnglishName(this.bookIdToNumber(id)); +/** + * Gets the ID of a book from its book number. + * @param number - Book number (this is 1-based, not an index). + * @param errorValue - The string to return if the book number does not correspond to a valid book. + * Defaults to `'***'`. + * @returns The 3-letter `bookId` if found, or the `errorValue` otherwise. + */ +export function bookNumberToId(number: number, errorValue = '***'): string { + const index: number = number - 1; + + if (index < 0 || index >= allBookIds.length) { + return errorValue; } - /** - * - * @param bookNum - Book number (this is 1-based, not an index). - * @returns `true` if the book is obsolete, or `false` otherwise. - */ - static isObsolete(bookNum: number): boolean { - const name: string = this.allBookEnglishNames[bookNum - 1]; - return name.includes('*obsolete*'); + return allBookIds[index]; +} + +/** + * Gets the English book name from its book number. + * @param number - Book number (this is 1-based, not an index). + * @returns The English name of the book if found, or `'******'` otherwise. + */ +export function bookNumberToEnglishName(number: number): string { + if (number <= 0 || number > lastBook) { + return '******'; } - private static createBookNumbers(): BookNumbers { - const bookNumbers: BookNumbers = {}; - for (let i = 0; i < this.allBookIds.length; i++) { - bookNumbers[this.allBookIds[i]] = i + 1; - } - return bookNumbers; + return allBookEnglishNames[number - 1]; +} + +/** + * Gets the English book name from its book ID. + * @param id - 3-letter book ID, e.g. `'MAT'`. + * @returns The English name of the book if found, or `'******'` otherwise. + */ +export function bookIdToEnglishName(id: string): string { + return bookNumberToEnglishName(bookIdToNumber(id)); +} + +/** + * + * @param bookNum - Book number (this is 1-based, not an index). + * @returns `true` if the book is obsolete, or `false` otherwise. + */ +export function isObsolete(bookNum: number): boolean { + const name: string = allBookEnglishNames[bookNum - 1]; + return name.includes('*obsolete*'); +} + +function createBookNumbers(): BookNumbers { + const bookNumbers: BookNumbers = {}; + for (let i = 0; i < allBookIds.length; i++) { + bookNumbers[allBookIds[i]] = i + 1; } + return bookNumbers; } -type BookNumbers = Record; +/** + * Canon information. Also, contains static information on complete list of books and localization. + */ +export const Canon = { + allBookIds, + nonCanonicalIds, + firstBook, + lastBook, + bookIdToNumber, + bookNumberToId, + bookNumberToEnglishName, + bookIdToEnglishName, + isObsolete, +}; +export default Canon; From 1c9515ecd9cc2e2c02d7b4b3b23a8e13299c37d5 Mon Sep 17 00:00:00 2001 From: Ira Hopkinson Date: Fri, 14 Jul 2023 19:55:24 +1200 Subject: [PATCH 2/2] Improve `Canon` - port more items - add tests - bump version to publish - also fix security vulnerability --- .vscode/settings.json | 2 +- README.md | 31 +++++++-- package-lock.json | 28 ++++---- package.json | 2 +- src/canon.test.ts | 79 +++++++++++++++++++++++ src/canon.ts | 144 ++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 261 insertions(+), 25 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index da5d8d5..2fa8e5f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -12,5 +12,5 @@ "url": "http://json.schemastore.org/tsconfig" } ], - "cSpell.words": ["Parseable"] + "cSpell.words": ["deutero", "otnt", "parseable"] } diff --git a/README.md b/README.md index 2bf0e02..a1c9dc1 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ -TypeScript partial port of the C# library [libpalaso/SIL.Scripture][github-libpalaso-scripture]. These libraries are used by [Paratext](https://paratext.org/) to represent and support Scripture references. +TypeScript partial port of the C# library [libpalaso/SIL.Scripture][github-libpalaso-scripture]. These libraries are used by [Paratext](https://paratext.org/) and provides classes for working with Scripture data such as references and versifications. v1 is a minimal partial port in TypeScript that supports use on the frontend while still using the full C# version on the backend. @@ -18,8 +18,9 @@ v1 is a minimal partial port in TypeScript that supports use on the frontend whi - {class} Canon - Canon information. Also, contains static information on complete list of books. - {class} VerseRef - Stores a reference to a specific verse in Scripture. - Represents a single reference, e.g. `'GEN 2:3'`. - - Represents a reference range and segments, e.g. `'LUK 3:4b-5a'`. - - Represents a reference sequence and segments, e.g. `'GEN 1:1a-3b,5'`. + - Represents a reference range, e.g. `'LUK 3:4-5'`. + - Represents a reference sequence, e.g. `'GEN 1:1-3,5'`. + - Represents a reference with a segment, e.g. `'LUK 3:4b'`. - Validate references. - Supports versification types: Unknown, Original, Septuagint, Vulgate, English, RussianProtestant, RussianOrthodox. @@ -113,15 +114,35 @@ Useful static functions: ```typescript import { Canon } from '@sillsdev/scripture'; +console.log(Canon.bookIdToNumber('MAT')); // 40 + console.log(Canon.bookNumberToId(1)); // 'GEN' console.log(Canon.bookNumberToId(40)); // 'MAT' -console.log(Canon.bookNumberToId('MAT')); // 40 - console.log(Canon.bookNumberToEnglishName(1)); // 'Genesis' console.log(Canon.bookIdToEnglishName('GEN')); // 'Genesis' +console.log(Canon.isBookIdValid('MAT')); // true + +console.log(Canon.isBookNT('MAT')); // true +console.log(Canon.isBookNT(1)); // false + +console.log(Canon.isBookOT('MAT')); // false +console.log(Canon.isBookOT(1)); // true + +console.log(Canon.isBookOTNT('MAT')); // true +console.log(Canon.isBookOTNT(1)); // true + +console.log(Canon.isBookDC('TOB')); // true +console.log(Canon.isBookDC(1)); // false + +console.log(Canon.isCanonical('XXA')); // false +console.log(Canon.isCanonical(1)); // true + +console.log(Canon.isExtraMaterial('XXA')); // true +console.log(Canon.isExtraMaterial(1)); // false + console.log(Canon.isObsolete(87)); // true ``` diff --git a/package-lock.json b/package-lock.json index 048f70a..939117e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@sillsdev/scripture", - "version": "1.2.0", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@sillsdev/scripture", - "version": "1.2.0", + "version": "1.3.0", "license": "MIT", "devDependencies": { "@types/jest": "^29.5.2", @@ -106,9 +106,9 @@ "dev": true }, "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -158,9 +158,9 @@ } }, "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -3374,9 +3374,9 @@ } }, "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" @@ -4154,9 +4154,9 @@ } }, "node_modules/make-dir/node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, "bin": { "semver": "bin/semver.js" diff --git a/package.json b/package.json index 66ba063..65ff6ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sillsdev/scripture", - "version": "1.2.0", + "version": "1.3.0", "description": "TypeScript partial port of `libpalaso/SIL.Scripture`", "main": "dist/index.cjs.js", "module": "dist/index.es.js", diff --git a/src/canon.test.ts b/src/canon.test.ts index 7ece576..2309f5e 100644 --- a/src/canon.test.ts +++ b/src/canon.test.ts @@ -1,6 +1,62 @@ import { Canon } from './canon'; describe('Canon', () => { + describe('isBookNT()', () => { + it("should return whether it's NT or not", () => { + let isBookNT = Canon.isBookNT(1); + expect(isBookNT).toBe(false); + isBookNT = Canon.isBookNT('GEN'); + expect(isBookNT).toBe(false); + isBookNT = Canon.isBookNT(42); + expect(isBookNT).toBe(true); + isBookNT = Canon.isBookNT('MAT'); + expect(isBookNT).toBe(true); + }); + }); + + describe('isBookOT()', () => { + it("should return whether it's OT or not", () => { + let isBookOT = Canon.isBookOT(1); + expect(isBookOT).toBe(true); + isBookOT = Canon.isBookOT('GEN'); + expect(isBookOT).toBe(true); + isBookOT = Canon.isBookOT(42); + expect(isBookOT).toBe(false); + isBookOT = Canon.isBookOT('MAT'); + expect(isBookOT).toBe(false); + }); + }); + + describe('isBookDC()', () => { + it("should return whether it's Deutero Canon or not", () => { + let isBookDC = Canon.isBookDC(66); + expect(isBookDC).toBe(false); + isBookDC = Canon.isBookDC('REV'); + expect(isBookDC).toBe(false); + isBookDC = Canon.isBookDC(67); + expect(isBookDC).toBe(true); + isBookDC = Canon.isBookDC('TOB'); + expect(isBookDC).toBe(true); + }); + }); + + describe('allBookNumbers()', () => { + it('should yield each book number', () => { + const iterator = Canon.allBookNumbers(); + expect(iterator.next().value).toEqual(1); + expect(iterator.next().value).toEqual(2); + expect(iterator.next().value).toEqual(3); + }); + }); + + describe('extraBooks()', () => { + it('should return array of extra book IDs', () => { + const extraBooks = Canon.extraBooks(); + expect(extraBooks[0]).toEqual('XXA'); + expect(extraBooks.length).toEqual(7); + }); + }); + describe('bookNumberToId()', () => { it('should return bookId', () => { const bookId = Canon.bookNumberToId(1); @@ -32,6 +88,29 @@ describe('Canon', () => { }); }); + describe('isCanonical()', () => { + it("should return whether it's canonical or not", () => { + // `num` overload is tested in `isBookDC()`. + let isCanonical = Canon.isCanonical('GEN'); + expect(isCanonical).toBe(true); + isCanonical = Canon.isCanonical('XXA'); + expect(isCanonical).toBe(false); + }); + }); + + describe('isExtraMaterial()', () => { + it("should return whether it's extra material or not", () => { + let isExtraMaterial = Canon.isExtraMaterial(1); + expect(isExtraMaterial).toBe(false); + isExtraMaterial = Canon.isExtraMaterial('GEN'); + expect(isExtraMaterial).toBe(false); + isExtraMaterial = Canon.isExtraMaterial(93); + expect(isExtraMaterial).toBe(true); + isExtraMaterial = Canon.isExtraMaterial('XXA'); + expect(isExtraMaterial).toBe(true); + }); + }); + describe('isObsolete()', () => { it("should return whether it's obsolete or not", () => { let isObsolete = Canon.isObsolete(1); diff --git a/src/canon.ts b/src/canon.ts index 932b741..1fa24f0 100644 --- a/src/canon.ts +++ b/src/canon.ts @@ -166,9 +166,6 @@ export const nonCanonicalIds: string[] = [ 'NDX', ]; -export const firstBook = 1; -export const lastBook = allBookIds.length; - /** Array of the English names of all books. */ const allBookEnglishNames: string[] = [ 'Genesis', @@ -330,6 +327,100 @@ export function bookIdToNumber(id: string, ignoreCase = true): number { return bookNumbers[id]; } +/** + * Check if a book ID is valid. + * @param id - 3-letter book ID to check, e.g. `'MAT'`. + * @returns `true` if book ID is valid, `false` otherwise. + */ +export function isBookIdValid(id: string): boolean { + return bookIdToNumber(id) > 0; +} + +/** + * Check if book ID is in western NT. + * @param id - 3-letter book ID, e.g. `'MAT'` + * @returns `true` if the book is in the NT, `false` otherwise. + */ +export function isBookNT(id: string): boolean; +/** + * Check if book number is in western NT. + * @param num - Book number (this is 1-based, not an index). + * @returns `true` if the book is in the NT, `false` otherwise. + */ +// eslint-disable-next-line @typescript-eslint/unified-signatures +export function isBookNT(num: number): boolean; +export function isBookNT(value: string | number): boolean { + const num = typeof value === 'string' ? bookIdToNumber(value) : value; + return num >= 40 && num <= 66; +} + +/** + * Check if book ID is in Protestant OT. + * @param id - 3-letter book ID, e.g. `'MAT'` + * @returns `true` if the book is in the OT, `false` otherwise. + */ +export function isBookOT(id: string): boolean; +/** + * Check if book number is in Protestant OT. + * @param num - Book number (this is 1-based, not an index). + * @returns `true` if the book is in the OT, `false` otherwise. + */ +// eslint-disable-next-line @typescript-eslint/unified-signatures +export function isBookOT(num: number): boolean; +export function isBookOT(value: string | number): boolean { + const num = typeof value === 'string' ? bookIdToNumber(value) : value; + return num <= 39; +} + +/** + * Check if the book is in either the OT or the NT. + * @param num - Book number (this is 1-based, not an index). + * @returns `true` if the book is in either the OT or the NT, `false` otherwise. + */ +export function isBookOTNT(num: number): boolean { + return num <= 66; +} + +/** + * Check if book is in Deutero Canon. + * @param id - 3-letter book ID, e.g. `'MAT'` + * @returns `true` if the book is in the Deutero Canon, `false` otherwise. + */ +export function isBookDC(id: string): boolean; +/** + * Check if book is in Deutero Canon. + * @param num - Book number (this is 1-based, not an index). + * @returns `true` if the book is in the Deutero Canon, `false` otherwise. + */ +// eslint-disable-next-line @typescript-eslint/unified-signatures +export function isBookDC(num: number): boolean; +export function isBookDC(value: string | number): boolean { + const num = typeof value === 'string' ? bookIdToNumber(value) : value; + return isCanonical(num) && !isBookOTNT(num); +} + +/** + * Enumerates all book numbers. + * @yields The next book number. + */ +export function* allBookNumbers(): Generator { + for (let i = 1; i <= allBookIds.length; i++) yield i; +} + +/** Index of the first book. Abstracting this makes code less fragile. */ +export const firstBook = 1; + +/** Number of the last book (1-based). */ +export const lastBook = allBookIds.length; + +/** + * Array of extra book IDs. + * @returns The array of extra book IDs. + */ +export function extraBooks(): string[] { + return ['XXA', 'XXB', 'XXC', 'XXD', 'XXE', 'XXF', 'XXG']; +} + /** * Gets the ID of a book from its book number. * @param number - Book number (this is 1-based, not an index). @@ -369,6 +460,42 @@ export function bookIdToEnglishName(id: string): string { return bookNumberToEnglishName(bookIdToNumber(id)); } +/** + * Check if this is a canonical book ID, as opposed to front matter etc. + * @param id - 3-letter book ID, e.g. `'MAT'` + * @returns `true` if the book is canonical, `false` otherwise. + */ +export function isCanonical(id: string): boolean; +/** + * Check if this is a canonical book number, as opposed to front matter etc. + * @param bookNum - Book number (this is 1-based, not an index). + * @returns `true` if the book is canonical, `false` otherwise. + */ +// eslint-disable-next-line @typescript-eslint/unified-signatures +export function isCanonical(bookNum: number): boolean; +export function isCanonical(value: string | number): boolean { + const id = typeof value === 'number' ? bookNumberToId(value) : value; + return isBookIdValid(id) && !nonCanonicalIds.includes(id); +} + +/** + * Check if book ID is extra material. + * @param id - 3-letter book ID, e.g. `'MAT'` + * @returns `true` if the book extra material, `false` otherwise. + */ +export function isExtraMaterial(id: string): boolean; +/** + * Check if book number is extra material. + * @param bookNum - Book number (this is 1-based, not an index). + * @returns `true` if the book is extra material, `false` otherwise. + */ +// eslint-disable-next-line @typescript-eslint/unified-signatures +export function isExtraMaterial(bookNum: number): boolean; +export function isExtraMaterial(value: string | number): boolean { + const id = typeof value === 'number' ? bookNumberToId(value) : value; + return isBookIdValid(id) && nonCanonicalIds.includes(id); +} + /** * * @param bookNum - Book number (this is 1-based, not an index). @@ -393,12 +520,21 @@ function createBookNumbers(): BookNumbers { export const Canon = { allBookIds, nonCanonicalIds, + bookIdToNumber, + isBookIdValid, + isBookNT, + isBookOT, + isBookOTNT, + isBookDC, + allBookNumbers, firstBook, lastBook, - bookIdToNumber, + extraBooks, bookNumberToId, bookNumberToEnglishName, bookIdToEnglishName, + isCanonical, + isExtraMaterial, isObsolete, }; export default Canon;