@cbop-dev/aland-gospel-synopsis
Version:
ES Javascript module for looking up parallel texts from the NT gospels, based on Aland's 'Quattuor Synopsis Evangeliorum'
419 lines (375 loc) • 15.7 kB
JavaScript
import { mylog } from '../env/env.js';
import * as mathUtils from './math-utils.js';
/**
* @description - returns true if one NT reference is contained within another, e.g., refIncludes("Matt 1:1,3-10", "Matt 1:4") and
* refIncludes("Rev", "Rev 2") each return true, because the last parameter is contained in the first;
* but refIncludes("Mark 1", "Mark 2:2"), refIncludes("Luke 1:4", "Luke 1:7"), and refIncludes("Luke 16", "Mark 16:2")
* each return false, because the later are not contained in the former.
* @param {string} containingRef -- a reference to a book, chapter, verse, chapter-range or verse-range.
* @param {string} includedRef -- a reference to a book, chapter, or verse. NOT a range!(?) todo: allow for includedrange!
* @returns {boolean} true if the includedRef is contained in the containerRef
*/
export function refIncludes(containingRef, includedRef) {
//mylog("refIncludes("+[containingRef,includedRef].join(',')+")...");
let passed = true;
const logMsgFunc = "refIncludes('" + containingRef + ", '" + includedRef + "')";
const containingObj = getBookChapVerseFromRef(containingRef.trim().replaceAll(/\s+/g, ' '));
const includedObj = getBookChapVerseFromRef(includedRef.trim().replaceAll(/\s+/g, ' '));
if (containingObj.book != includedObj.book) {
passed = false;
}
if (containingObj.chap?.includes('-')) { //chap range!
if(containingObj.v?.includes('-')){
//oops! bad input: to many ranges
//mylog(logMsgFunc + " bad input: both verse and chapter ranges");
}
else {
const allowedChaps = createNumArrayFromStringListRange(containingObj.chap);
//keep going...
const includedChap = parseInt(includedObj.chap)
if (includedChap && !allowedChaps.includes(includedChap)) { //failed: not in range
passed = false;
// mylog(logMsgFunc + "-->false: included is not in container chapter range");
}
else { //we passed! do nothing!
}
}
}
else if (containingObj.v?.includes('-')){ //verse range!
if (containingObj.chap != includedObj.chap) {
passed = false;
}
else {//chapters match, but do verses?
const allowedVerses=createNumArrayFromStringListRange(containingObj.v);
const includedV = parseInt(includedObj.v);
if (includedV && !allowedVerses.includes(includedV)) { //oops: not in range
passed = false;
}
else { //we passed! do nothing!
}
}
}
else { //not a range: let's see what we've got!
if (containingObj.chap &&
containingObj.chap != includedObj.chap){ //not within the container chapter
passed = false;
}
else if(containingObj.v && containingObj.v != includedObj.v){ //book and chap match. Do the verses?
passed = false;
}
}
//mylog("refIncludes -> " + passed)
return passed;
}
/**
*
* @param {string} str
* @returns
*/
export function cleanString(str){
//mylog("CleanString("+str+")");
return str ? String(str).replaceAll(/\s+/g, ' ').trim() : '';
}
export function cleanNumString(numString){
return cleanString(String(numString));
}
/**
*
* @param {string} numString - a list of integers or integer ranges, separated by commas. Eg., "1,2,4-7", "3", "3-6,4", etc.
* @returns {number[]} - an array of all the numbers in the given list, e.g., "1,2,4-7" --> [1,2,4,5,6,7], etc.
*/
export function createNumArrayFromStringListRange(numString){
numString=cleanNumString(numString);
const nums=[];
const sepGroups = cleanNumString(numString).split(',');
for (const group of sepGroups){
const ranges = group.split("-");
const min = parseInt(ranges[0]);
const max = ranges.length > 1 ? parseInt(ranges[1]) : null;
if (ranges.length > 2) //bad input!
return [];
else if (ranges.length == 2 && max){
if (min < max){
for (let i = min; i <= max; i++) {
if (!nums.includes(i))
nums.push(i);
}
}
}
else if (ranges.length == 1){ //no range, just a plain number!
if (!nums.includes(min))
nums.push(min);
}
else{
//bad input?; don't add anything.
}
}
return nums.sort();
}
/**
*
* @param {string} string
* @returns {{book:string|null, chap:string|null}}
*/
export function splitBookChap(string){
const matches = cleanString(string).match(/^(([1-3]+ +)?[a-zA-Z]+)( +([0-9a-z-]+))?$/); //reading 'chapters' which might actually be verses, i.e., Jude 3a
let theBook = null,theChap = theBook;
if (matches && matches.length >=5){ //got chapter
theBook = matches[1];
theChap = matches[4] ? matches[4] : null;
}
else if(matches && matches[1]){//just a book
theBook=matches[1];
}
else{
//error
mylog("splitBookChap could not parse '"+string+"'");
}
// mylog("splitBookChap(string)->{b:" + theBook + ", c:"+theChap+"}");
return {book: theBook, chap: theChap}
}
/**
*
* @param {string} refString -- NT ref string of a book, chapter, verse, or a chapter range or verse range.
* E.g.:"1 Cor 2:3", "1 Cor 2", "1 Cor 2:3-4", "1 Cor 2-3" but NOT "1 Cor-2 Cor" nor "1 Cor 2-3:4-5" (which doesn't make sense)
* @returns {{book:string|null, chap:string|null, v:string|null}}
*/
export function getBookChapVerseFromRef(refString){
refString=cleanString(refString);
let book = null, chap = book, v = book;
//NB books with only 1 chap: [Phlm, Jude,2 John, 3 John]
let badInput = false;
if (refString.split(":").length == 2){//got explicit verses
let bookChap ='';
[bookChap,v] = refString.split(":");
if (v){ //got verses as expected
const bookChapObj = splitBookChap(bookChap);
book = bookChapObj.book;
chap = bookChapObj.chap;
}
else{ //what?? bad input: colon with not verses! (e.g., "Eph 2:")
badInput=true;
mylog("bad input with colon: '"+refString+"'");
}
}
else { //no verses, just book and chap
const bookChapObj = splitBookChap(refString);
book = bookChapObj.book;
chap = bookChapObj.chap;
if(!chap){
// mylog("getBookChapVerseFromRef("+refString+") got no chap!"+chap)
}
}
return {book: book,chap: chap, v: v}
}
/**
*
* @param {string} string - chapter and verse(s). E.g., "1:3" or "2" or even "2-3" (w/o verses)
* @returns {{chapter:string, verse:string}}
*/
export function getChapVerseFromRef(string){
string = cleanString(string);
const vSplit = string.split(":");
const chap = vSplit.length > 0 ? vSplit[0] : null;
const v = vSplit.length > 0 ? (vSplit[1] ? vSplit[1] : null) : null;
return {chap: vSplit[0], v: v}
}
/**
*
* @param {string} refString -- biblical references, with diffrerent chapters or books separated by semi-colons.
* NB: ranges must not cross chapter boundaries: e.g., Mark 2:1-3:4 is invalid and will not work (yet).
* @returns {string[]} an array of NT references, where each can stand on its own.
* E.g,: expandRefs("2 Tim 1:12; 2:3)"-->["2 Tim 1:12", "2 Tim 2:3"];
* and : expandRefs("Matt 3:2-10")-->["Matt 3:2", "Matt 3:3", ... "Matt 3:10"]
*/
export function expandRefs(refString){
const refArray = [];
refString = cleanString(refString);
let latestBook = '';
for (const ref of refString.split(";")){
let bookCv = getBookChapVerseFromRef(ref);
mylog("expandRefs: bookCv of '"+ref +"'= "+[bookCv.book,bookCv.chap,bookCv.v].join(','));
if (bookCv.book){
latestBook = bookCv.book;
}
else if (latestBook){
bookCv = getBookChapVerseFromRef(latestBook + " " + ref);
}
if(bookCv.book) {
if (bookCv.v?.includes(',')){
for (const vv of bookCv.v.split(',')){
if (vv.includes('-')){
for (const v of mathUtils.createNumArrayFromStringListRange(vv)){
refArray.push(bookCv.book + " " + bookCv.chap + (v? ":" + v : '')) ;
}
}
else{
refArray.push(bookCv.book + " " + bookCv.chap + (vv? ":" + vv : ''));
}
}
}
else if (bookCv.v?.includes('-')){
for (const v of mathUtils.createNumArrayFromStringListRange(bookCv.v)){
refArray.push(bookCv.book + " " + bookCv.chap + (v? ":" + v: ''));
}
}
else{
refArray.push(bookCv.book + " " + bookCv.chap + (bookCv.v ? ":" + bookCv.v : ''));
}
}
else{
//can't do anything: we don't know what book it is!
}
}
return refArray;
}
//TODO: review/test these functions, take from lxxweb app:
/**
*
* @param {string[]} refArray -- an array of strings where each string represents a single bible reference, whether a book, chapter or verse.
* NB: book names (or abbreviations) must not contain any spaces. (e.g, '2Sam' works, but not '2 Sam' or 'II Sam')
* @returns -- a string with the bible references grouped and combined by book and chapter, such that larger sections swallow smaller ones.
* e.g., ["Gen", "Gen 1"] => "Gen", but ["Gen 1", "Gen 2:4", "Exod 3:2"] => "Gen 1; 2:4; Ex 3:2".
*/
export function combineRefs(refArray) {
//console.debug("combineRefs() with:")
//console.debug(refArray)
/**
* @type {Object.<string,string[]|string>}
*/
let refList={}
for (const ref of refArray){
const parts = ref.trim().replace(":", " ").split(" ");
////console.debug(parts)
//refList[parts[0]] = parts.slice(1)
if (parts[0] && ! refList[parts[0]]) {
refList[parts[0]]={};
}
if (parts.length == 1) { //only got a chapter
refList[parts[0]] ='all';
}
if (parts.length == 2 && refList[parts[0]] != 'all'){ //got a book and a whole chapter, but don't have whole book
refList[parts[0]][parts[1]] = 'all'; //wipe out whatever was there before!
}
else if (parts.length == 3) {
if (!refList[parts[0]][parts[1]]){
refList[parts[0]][parts[1]] = [parts[2]];
}
else if (refList[parts[0]][parts[1]] != 'all' && refList[parts[0]][parts[1]].push){
refList[parts[0]][parts[1]].push(parts[2]);
}
}
}
// //console.debug("reflist")
// //console.debug(refList)
const combined = Object.entries(refList).map(([b,chapList])=>
(chapList == 'all' || Object.keys(chapList).length == 0) ?
b :
(b + " " + Object.entries(chapList).sort((a,b)=>a[0]-b[0]).map(
([c,vv])=>
vv.includes("all") ?
c
: c + ":" + joinInRanges(vv.sort((a,b)=>Number(a)-Number(b)))
).join("; "))
).join("; ")
////console.debug("combineRefs->'" + combined + ";");
return combined
}
/**
* @param {Array.<number|string>} numArray array of numbers(or numeric strings
* @returns {string} a string of the numbers, sorted with ranges, each range/number separated by 'separator'
* E.g. joinInRanges([1,3,2,5,5,6], ",") => "1-3,5-6".
* NB: this also tries to remove duplicates
*/
export function joinInRanges(numArray, separator=",", spreader="-"){
let uniqueArray = [...new Set(numArray.map((x)=>Number(x)))].sort((x,y)=>x-y);
// //console.debug("uniq array: [" + uniqueArray.join("/") + "]");
// //console.debug("uniqArray.entries():" + [...uniqueArray]);
let rangeString = '';
let curRangeStart = uniqueArray[0];
let curRangeEnd= uniqueArray[0];
if (uniqueArray.length == 1) {//only one guy, all by himself....
rangeString += curRangeEnd.toString();
}
else {
for (let [i,num] of uniqueArray.entries()){
// //console.debug("got i = " + i+"; num=" + num + " curSt=" + curRangeStart + "; curEnd=" + curRangeEnd);
//num=Number(num);
if (i == 0) { //first item; do nothing unless its also the last (see last condition)
}
else if (num == (curRangeEnd + 1))//extending the current range
curRangeEnd=num;
else {//add previous range, start new one:
if (rangeString.length > 0)//not at beginning
rangeString+=separator;
if (curRangeStart == curRangeEnd) { // had only singeton:
rangeString += curRangeEnd.toString();
}
else { // found end of range:
rangeString += curRangeStart.toString() + spreader + curRangeEnd.toString();
}
//reset new range:
curRangeStart = num;
curRangeEnd = num;
}
if (i == uniqueArray.length-1 && i > 0) { //last item, but not solo
if (rangeString.length > 0) // not first thing to add...
rangeString += separator;
if (num == curRangeStart){//last man standing...
rangeString += num.toString();
}
else { //ended with range:
rangeString += curRangeStart.toString()+spreader+curRangeEnd.toString();
}
}
}
}
return rangeString;
}
/**
* @description Sorting function for array of chapter:verse references as in ["12:3", "15:2"].
* refs can include verse ranges ("12:3-4") just be a chapter number ("12"), in which case the first verse in the range will be used for sorting,
* such that entire chapters will appear before verses: ["12", "12:1", "12:1-3"], etc.
* @param {string} cv1
* * @param {string} cv2
*/
export function sortChapVerseRefs(cv1, cv2){
const a = cv1.split(":");
const b = cv2.split(":");
const logMsg = "sortChapVerseRefs("+[cv1,cv2].join(',')+")";
let retVal = 0;
if (a[0]==b[0])
if (a[1] && b[1]) {
retVal = parseInt(a[1]) - parseInt(b[1]);
}
else if (!a[1])
retVal = -1;
else if (!b[1])
retVal = 1;
else
retVal = 0;
else if(a[0] && b[0])
retVal = parseInt(a[0])-parseInt(b[0]);
else if(!a[0]) //don't have a; keep it first.
retVal = -1
else if (!b[0]) // have a but not b, put b first.
retVal = 1;
else // no real difference between em!
retVal = 0;
// mylog("sortChaVerseRef("+cv1+","+cv2+")=" +retVal+")");
//mylog(logMsg+" --> " + retVal);
return retVal;
}
export default {
refIncludes,
cleanString,
cleanNumString,
createNumArrayFromStringListRange,
splitBookChap,
getBookChapVerseFromRef,
getChapVerseFromRef,
expandRefs,
combineRefs,
joinInRanges,
sortChapVerseRefs
};