• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

jkrrv / ScripturNum / 18861633312

28 Oct 2025 01:59AM UTC coverage: 99.715% (-0.3%) from 100.0%
18861633312

push

github

jkrrv
Better exception handling

3 of 5 new or added lines in 1 file covered. (60.0%)

700 of 702 relevant lines covered (99.72%)

338.4 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.56
/src/ScripturNum.php
1
<?php
2

3

4
namespace ScripturNum;
5

6
use TypeError;
7

8
/**
9
 * The ScripturNum class, which represents a single continuous passage.
10
 *
11
 * @property-read ?int $int
12
 * @property-read ?int $book
13
 * @property-read ?int $startCh
14
 * @property-read ?int $startV
15
 * @property-read ?int $endCh
16
 * @property-read ?int $endV
17
 */
18
class ScripturNum
19
{
20
        protected $int;
21
        protected $book;
22
        protected $startCh;
23
        protected $startV;
24
        protected $endCh;
25
        protected $endV;
26

27
        const BOOK_MASK = 4278190080;
28
        const START_MASK = 16773120;
29
        const END_MASK = 4095;
30

31
        protected static $bibleClass = Bible::class;
32

33
        /**
34
         * ScripturNum constructor.
35
         *
36
         * @param int|string $intOrString ScripturNum int or a human-readable string.
37
         *
38
         * @throws ScripturNumException  Thrown if the provided int or string can't be understood.
39
         */
40
        public function __construct($intOrString)
41
        {
42
                if (is_numeric($intOrString)) {
1,881✔
43
                        $int = (int)$intOrString;
561✔
44
                } else {
45
                        try {
46
                                $int = self::stringToInt($intOrString);
1,507✔
47
                        } catch (TypeError $e) {
11✔
48
                                throw new ScripturNumException("Invalid value provided to ScripturNum constructor.");
11✔
49
                        }
50
                }
51
                self::intToRefNums($int, $this->book, $this->startCh, $this->startV, $this->endCh, $this->endV);
1,870✔
52
                $this->int = $int;
1,837✔
53
        }
501✔
54

55
        /**
56
         * @param $what
57
         *
58
         * @since 2.0.0
59
         *
60
         * @return int
61
         */
62
        public function __get($what)
63
        {
64
                switch ($what) {
17✔
65
                        case "int":
187✔
66
                                return $this->getInt();
11✔
67

68
                        case "book":
187✔
69
                                return $this->book;
187✔
70

71
                        case "startCh":
99✔
72
                                return $this->startCh;
99✔
73

74
                        case "startV":
99✔
75
                                return $this->startV;
11✔
76

77
                        case "endCh":
99✔
78
                                return $this->endCh;
99✔
79

80
                        case "endV":
11✔
81
                                return $this->endV;
11✔
82
                }
83
                throw new \Error("The requested property does not exist.");
11✔
84
        }
85

86

87
        protected static $stringSettings = [
88
                'abbrev' => [
89
                        'space' => '',
90
                        'cvsep' => '.',
91
                        'range' => '-',
92
                        'names' => 1,
93
                        'plurl' => false
94
                ],
95
                'long'   => [
96
                        'space' => ' ',
97
                        'cvsep' => ':',
98
                        'range' => '-',
99
                        'names' => 0,
100
                        'plurl' => true
101
                ],
102
        ];
103

104

105
        /**
106
         * Update string settings.
107
         *
108
         * @param       $key
109
         * @param array $settings
110
         *
111
         * @return void
112
         */
113
        public static function setStringSettings($key, array $settings)
114
        {
115
                if ( ! isset(static::$stringSettings[$key])) {
88✔
116
                        static::$stringSettings[$key] = [];
77✔
117
                }
118

119
                foreach (reset(static::$stringSettings) as $k => $v) {
88✔
120
                        if (isset($settings[$k])) {
88✔
121
                                static::$stringSettings[$key][$k] = $settings[$k];
79✔
122
                        }
123
                }
124
        }
24✔
125

126

127
        /**
128
         * Get the ScripturNum integer.
129
         *
130
         * @return int The ScripturNum integer
131
         */
132
        public function getInt(): int
133
        {
134
                return $this->int;
1,111✔
135
        }
136

137

138
        /**
139
         * Generic toString.  Uses the long form.
140
         *
141
         * @return string
142
         * @throws ScripturNumException
143
         */
144
        public function __toString(): string
145
        {
146
                return $this->getLongString();
220✔
147
        }
148

149

150
        /**
151
         * Get a human-readable abbreviation for the passage.  By default, these are meant for usage in short links.
152
         *
153
         * @return string An abbreviation
154
         * @throws ScripturNumException
155
         */
156
        public function getAbbrev(): string
157
        {
158
                return $this->toString('abbrev');
121✔
159
        }
160

161

162
        /**
163
         * Get a human-readable name of the passage.  By default, these are meant for humans to read.
164
         *
165
         * @return string The name of the passage, as one might pronounce it.
166
         * @throws ScripturNumException
167
         */
168
        public function getLongString(): string
169
        {
170
                return $this->toString('long');
528✔
171
        }
172

173

174
        /**
175
         * Get a string.  Publicly-accessible.
176
         *
177
         * @since 2.0.0
178
         *
179
         * @throws ScripturNumException
180
         */
181
        public function toString($options): string
182
        {
183
                $s = $this->getStringWithSettings($options);
836✔
184
                if (isset($options['callback']) && is_callable($options['callback'])) {
737✔
185
                        $s = call_user_func($options['callback'], $s, $this);
11✔
186
                }
187
                return $s;
737✔
188
        }
189

190

191
        /**
192
         * Returns a human-readable string with the settings defined in a given set of settings.
193
         *
194
         * @param string|array $options The setting set to use, or an array of options.
195
         *
196
         * @return string The human-intelligible string.
197
         * @throws ScripturNumException  If a setting is invalid.
198
         */
199
        protected function getStringWithSettings($options): string
200
        {
201
                $settingKey = 'long';
836✔
202
                if (is_string($options)) {
836✔
203
                        $settingKey = $options;
748✔
204
                        $options = [];
748✔
205
                } else if (is_array($options) && isset($options['settings'])) {
99✔
206
                        $settingKey = $options['settings'];
22✔
207
                }
208

209
                if ( ! isset(static::$stringSettings[$settingKey])) {
836✔
210
                        throw new ScripturNumException('Invalid key for creating a string.');
33✔
211
                }
212

213
                if ( ! isset(static::$stringSettings[$settingKey]['space'])) {
803✔
214
                        throw new ScripturNumException('Invalid space character.');
11✔
215
                }
216

217
                if ( ! isset(static::$stringSettings[$settingKey]['cvsep'])) {
792✔
218
                        throw new ScripturNumException('Invalid chapter-verse separation character.');
11✔
219
                }
220

221
                if ( ! isset(static::$stringSettings[$settingKey]['range'])) {
781✔
222
                        throw new ScripturNumException('Invalid range character.');
11✔
223
                }
224

225
                if ( ! isset(static::$stringSettings[$settingKey]['names']) ||
770✔
226
                     ! is_numeric(static::$stringSettings[$settingKey]['names'])) {
770✔
227
                        throw new ScripturNumException('Invalid name offset.');
11✔
228
                }
229

230
                if ( ! isset(static::$stringSettings[$settingKey]['plurl'])) {
759✔
231
                        throw new ScripturNumException('Plurality is not defined.');
22✔
232
                }
233

234

235
                $s = static::$stringSettings[$settingKey]['space'];
737✔
236
                $c = static::$stringSettings[$settingKey]['cvsep'];
737✔
237
                $r = static::$stringSettings[$settingKey]['range'];
737✔
238
                $n = (int)static::$stringSettings[$settingKey]['names'];
737✔
239
                $p = !! static::$stringSettings[$settingKey]['plurl'];
737✔
240

241
                if (!isset($options['excludeBook']) || !$options['excludeBook']) {
737✔
242
                        $b = static::getBookNames();
737✔
243

244
                        if ($n > count($b[$this->book - 1])) {
737✔
245
                                $n = count($b[$this->book - 1]) - 1;
11✔
246
                        }
247

248
                        $b = $b[$this->book - 1][$n];
737✔
249

250
                        if ($this->startCh !== $this->endCh && $p) {
737✔
251
                                $b = self::pluralizeBookName($b);
569✔
252
                        }
253
                } else {
254
                        $b = "";
33✔
255
                        $s = "";
33✔
256
                }
257

258
                if ($this->isWholeBook()) {
737✔
259
                        return $b;
66✔
260
                } elseif ($this->isWholeChapters()) {
671✔
261
                        if ($this->startCh === $this->endCh) {
385✔
262
                                return $b . $s . $this->startCh;
198✔
263
                        }
264

265
                        return $b . $s . $this->startCh . $r . $this->endCh;
220✔
266
                } else {
267
                        $startC = $this->startCh;
341✔
268
                        $endC = $this->endCh;
341✔
269
                        if (isset($options['excludeCh']) && !!$options['excludeCh']) {
341✔
270
                                $startC = "";
11✔
271
                                $endC = "";
11✔
272
                                $c = "";
11✔
273
                        }
274

275
                        if ($this->bookHasSingleChapter()) {
341✔
276
                                if ($this->startV === $this->endV) {
55✔
277
                                        return $b . $s . $this->startV;
33✔
278
                                }
279

280
                                return $b . $s . $this->startV . $r . $this->endV;
22✔
281
                        } elseif ($this->startCh === $this->endCh) {
286✔
282
                                if ($this->startV === $this->endV) {
198✔
283
                                        return $b . $s . $startC . $c . $this->startV;
143✔
284
                                }
285

286
                                return $b . $s . $startC . $c . $this->startV . $r . $this->endV;
88✔
287
                        }
288

289
                        return $b . $s . $startC . $c . $this->startV . $r . $endC . $c . $this->endV;
88✔
290
                }
291
        }
292

293

294
        /**
295
         * Returns true if the passage is an entire chapter.
296
         *
297
         * @return bool
298
         */
299
        public function isWholeChapters(): bool
300
        {
301
                $v = Bible::getVerseCounts();
682✔
302

303
                return ($this->startV === 1 && $this->endV === $v[$this->book - 1][$this->endCh - 1]);
682✔
304
        }
305

306
        /**
307
         * Returns a ScripturNum for the current range, expanded to the whole chapter.
308
         *
309
         * @since 2.0.0
310
         *
311
         * @return static
312
         * @throws ScripturNumException
313
         */
314
        public function getWholeChapters(): ScripturNum
315
        {
316
                if ($this->isWholeChapters()) {
33✔
317
                        return $this;
11✔
318
                }
319

320
                $i = static::refNumsToInt($this->book, $this->startCh, null, $this->endCh, null);
22✔
321
                return new static($i);
22✔
322
        }
323

324
        /**
325
         * Returns a ScripturNum for the chapter after the current highest chapter.
326
         *
327
         * @since 2.0.0
328
         *
329
         * @return static
330
         * @throws ScripturNumException
331
         */
332
        public function getNextChapter(): ScripturNum
333
        {
334
                $v = Bible::getVerseCounts();
55✔
335
                if (! isset($v[$this->book]) && $this->endCh === count($v[$this->book - 1])) {
55✔
336
                        throw new ScripturNumException("There are no more chapters in the Bible.");
11✔
337
                }
338
                if (! isset($v[$this->book - 1][$this->endCh])) {
44✔
339
                        $i = static::refNumsToInt($this->book + 1, 1, null, 1, null);
22✔
340
                } else {
341
                        $ch = $this->endCh + 1;
22✔
342
                        $i = static::refNumsToInt($this->book, $ch, null, $ch, null);
22✔
343
                }
344
                return new static($i);
44✔
345
        }
346

347
        /**
348
         * Returns a ScripturNum for the chapter prior to the current lowest chapter.
349
         *
350
         * @since 2.0.0
351
         *
352
         * @return static
353
         * @throws ScripturNumException
354
         */
355
        public function getPrevChapter(): ScripturNum
356
        {
357
                if ($this->startCh === 1 && $this->book === 1) {
55✔
358
                        throw new ScripturNumException("There are no more chapters in the Bible.");
11✔
359
                }
360
                $v = Bible::getVerseCounts();
44✔
361
                if ($this->startCh === 1) {
44✔
362
                        $ch = count($v[$this->book - 2]);
22✔
363
                        $i = static::refNumsToInt($this->book - 1, $ch, null, $ch, null);
22✔
364
                } else {
365
                        $ch = $this->startCh - 1;
22✔
366
                        $i = static::refNumsToInt($this->book, $ch, null, $ch, null);
22✔
367
                }
368
                return new static($i);
44✔
369
        }
370

371
        /**
372
         * Returns true if the passage is a whole book.
373
         *
374
         * @return bool
375
         */
376
        public function isWholeBook(): bool
377
        {
378
                $v = Bible::getVerseCounts();
737✔
379

380
                return ($this->startCh === 1
737✔
381
                        && $this->startV === 1
737✔
382
                        && $this->endCh === count($v[$this->book - 1])
737✔
383
                        && $this->endV === $v[$this->book - 1][$this->endCh - 1]);
737✔
384
        }
385

386

387
        /**
388
         * Returns true if the passage is just a single verse.
389
         *
390
         * @since 2.0.0
391
         *
392
         * @return bool
393
         */
394
        public function isSingleVerse(): bool
395
        {
396
                return ($this->startCh === $this->endCh
99✔
397
                        && $this->startV === $this->endV);
99✔
398
        }
399

400

401
        /**
402
         * Returns true if the book only has one chapter (e.g. Jude)
403
         *
404
         * @return bool
405
         */
406
        public function bookHasSingleChapter(): bool
407
        {
408
                return Bible::bookHasSingleChapter($this->book - 1);
341✔
409
        }
410

411

412
        /**
413
         * @param $bookName
414
         *
415
         * @return int
416
         * @throws ScripturNumException
417
         */
418
        protected static function bookNameToBookNum($bookName): int
419
        {
420
                $books = static::getBookNames();
1,727✔
421
                foreach ($books as $book => $bookNames) {
1,727✔
422
                        if (Bible::in_arrayi($bookName, $bookNames)) {
1,727✔
423
                                return $book + 1;
1,700✔
424
                        }
425
                }
426
                throw new ScripturNumException('Book name is invalid.');
33✔
427
        }
428

429
        /**
430
         * @return string[][]
431
         *
432
         * @since 2.0.0
433
         *
434
         * @see Bible::getBookNames()
435
         */
436
        protected static function getBookNames(): array
437
        {
438
                return call_user_func([static::$bibleClass, 'getBookNames']);
1,848✔
439
        }
440

441
        /**
442
         * @return string[]
443
         *
444
         * @since 2.0.0
445
         *
446
         * @see Bible::getCommonTerms()
447
         */
448
        protected static function getCommonTerms(): array
449
        {
450
                return call_user_func([static::$bibleClass, 'getCommonTerms']);
165✔
451
        }
452

453
        /**
454
         * @return string[]
455
         *
456
         * @since 2.0.0
457
         *
458
         * @see Bible::getConjunctions()
459
         */
460
        protected static function getConjunctions(): array
461
        {
462
                return call_user_func([static::$bibleClass, 'getConjunctions']);
242✔
463
        }
464

465
        /**
466
         * @param string $bookNameSingular
467
         *
468
         * @return string
469
         *
470
         * @since 2.0.0
471
         *
472
         * @see Bible::pluralizeBookName()
473
         */
474
        protected static function pluralizeBookName(string $bookNameSingular): string
475
        {
476
                return call_user_func([static::$bibleClass, 'pluralizeBookName'], $bookNameSingular);
275✔
477
        }
478

479
        /**
480
         * @param string $bookStr
481
         * @param ?int   $startCh
482
         * @param ?int   $startV
483
         * @param ?int   $endCh
484
         * @param ?int   $endV
485
         *
486
         * @return ScripturNum
487
         * @throws ScripturNumException
488
         */
489
        public static function newFromParsed(string $bookStr,
490
                $startCh = null, $startV = 1,
491
                $endCh = null, $endV = null): ScripturNum
492
        {
493
                $book = self::bookNameToBookNum($bookStr);
11✔
494
                self::validateRefNums($book, $startCh, $startV, $endCh, $endV);
11✔
495
                $int  = self::refNumsToInt($book, $startCh, $startV, $endCh, $endV);
11✔
496
                $c    = static::class;
11✔
497

498
                return new $c($int);
11✔
499
        }
500

501

502
        /**
503
         * @param int      $book The book of the Bible the range is within. 1-rel.
504
         * @param int      $startCh The chapter of the start of the range. 1-rel.
505
         * @param int|null $startV The verse of the start of the range.  1-rel. Defaults to 1.
506
         * @param int|null $endCh The end chapter of the range.  If null or not provided, assumed to be the same as the
507
         *     start chapter.
508
         * @param int|null $endV The end verse of the range.  If null or not provided, assumed to be the end of the
509
         *     chapter.
510
         *
511
         * @return ScripturNum The ScripturNum object that represents this range of scripture.
512
         *
513
         * @throws ScripturNumException A chapter was requested that does not exist within the requested book.
514
         * @throws ScripturNumException A verse was requested that does not exist within the requested range.
515
         */
516
        public static function newFromInts(int $book,
517
                int $startCh, $startV = null,
518
        $endCh = null, $endV = null): ScripturNum
519
        {
520
                self::validateRefNums($book, $startCh, $startV, $endCh, $endV);
22✔
521
                $int = self::refNumsToInt($book, $startCh, $startV, $endCh, $endV);
22✔
522
                $c   = static::class;
22✔
523

524
                return new $c($int);
22✔
525
        }
526

527

528
        /**
529
         * Takes a string that represents a single passage and returns it as an int.
530
         *
531
         * @param string $string A human-readable scripture reference that should be converted to an int.
532
         *
533
         * @return int The int.
534
         *
535
         * @throws ScripturNumException
536
         */
537
        public static function stringToInt(string $string): int
538
        {
539
                // Standardize dashes
540
                $string = str_replace(['&ndash;', '–'], '-', $string);
1,496✔
541

542
                // Remove duplicate spaces (Can't remove all spaces because spaces may occur in book names like Song of Songs
543
                $string = preg_replace("/\s\s+/", ' ', $string);
1,496✔
544

545
                // Remove spaces among the numerical parts.
546
                $string = preg_replace("/(\d+)\s*([-.:])\s*(\d+)/i", '$1$2$3', $string);
1,496✔
547

548
                // Look for right-most space or alpha char.  This should separate book name from numerical ref.
549
                preg_match('/.*([a-zA-Z\s])/', $string, $matches, PREG_OFFSET_CAPTURE);
1,496✔
550
                $spaceIndex = (int)$matches[1][1] + 1;
1,496✔
551
                $book       = trim(substr($string, 0, $spaceIndex));
1,496✔
552
                $ref        = substr($string, $spaceIndex);
1,496✔
553

554
                // Parse numbers
555
                self::refNumStringToRefNums($ref, $startCh, $startV, $endCh, $endV);
1,496✔
556

557
                // Change book name to number
558
                $book = self::bookNameToBookNum($book);
1,496✔
559

560
                // Assemble and return the int
561
                self::validateRefNums($book, $startCh, $startV, $endCh, $endV);
1,496✔
562
                return self::refNumsToInt($book, $startCh, $startV, $endCh, $endV);
1,496✔
563
        }
564

565
        /**
566
         * Takes a reference string and returns a plurality of ints for corresponding passages.  This should only be passed
567
         * values that are known to be references, not just any text. Use self::extractFromString for that.
568
         *
569
         * @param string     $string A human-readable scripture reference that should be converted to one or more ints.
570
         * Allows for commas and semicolons.
571
         * @param bool|array $exceptions Pass an array to this parameter, and it will be populated with any exceptions that
572
         * occur.  By passing an array, this will not throw the exception, and by not throwing the exception, execution
573
         * continues and you may be apprised of multiple errors that may exist.
574
         *
575
         * @return int[] The ints.
576
         * @throws ScripturNumException
577
         * @see self::extractFromString();
578
         *
579
         * @since 2.0.0
580
         *
581
         */
582
        public static function stringToInts(string $string, &$exceptions = false): array
583
        {
584
                $cj = static::getConjunctions();
220✔
585

586
                // standardize punctuation bits
587
                $string = str_replace([$cj['and'], '&', $cj['chapter'], $cj['through'], '&endash;', '–'],
220✔
588
                                      [',', ',', '', '-', '-', '-'],
220✔
589
                                      $string);
220✔
590

591
                // Remove duplicate spaces (Can't remove all spaces because spaces may occur in book names like Song of Songs
592
                $string = preg_replace("/\s\s+/", ' ', $string);
220✔
593

594
                // Look for right-most alpha char.  This should separate book name from numerical ref.
595
                preg_match('/.*([a-zA-Z])/', $string, $matches, PREG_OFFSET_CAPTURE);
220✔
596
                $spaceIndex = (int)$matches[1][1] + 1; // Space index may not actually be a space.
220✔
597
                $book       = trim(substr($string, 0, $spaceIndex));
220✔
598
                $ref        = substr($string, $spaceIndex);
220✔
599

600
                // Change book name to number
601
                try {
602
                        $book = self::bookNameToBookNum($book);
220✔
603
                } catch (ScripturNumException $e) {
22✔
604
                        if (is_array($exceptions)) {
22✔
605
                                $exceptions[] = $e;
11✔
606
                        } else {
607
                                throw $e;
11✔
608
                        }
609
                        return [];
11✔
610
                }
611

612
                $ints = [];
198✔
613

614
                // Remove all spaces from reference.
615
                $ref = preg_replace("/\s*/", '', $ref);
198✔
616

617
                $levelsOfCommas = (2 * (strpos($ref, ';') > -1) + (strpos($ref, ',') > -1));
198✔
618
                foreach (explode(";", $ref) as $sA) {
198✔
619
                        if ($levelsOfCommas > 1) {
198✔
620
                                unset($startCh, $startV, $endCh, $endV);
22✔
621
                        }
622
                        foreach (explode(",", $sA) as $s) {
198✔
623
                                $s = trim($s);
198✔
624
                                if ($s === "" && isset($endCh))
198✔
625
                                        continue;
11✔
626
                                try {
627
                                        // Parse numbers
628
                                        self::refNumStringToRefNums($s, $startCh, $startV, $endCh, $endV, true);
198✔
629

630
                                        // Assemble and return the int
631
                                        self::validateRefNums($book, $startCh, $startV, $endCh, $endV);
198✔
632
                                        $ints[] = self::refNumsToInt($book, $startCh, $startV, $endCh, $endV);
176✔
633

634
                                } catch (ScripturNumException $e) {
44✔
635
                                        if (is_array($exceptions)) {
44✔
636
                                                $exceptions[] = $e;
22✔
637
                                        } else {
638
                                                throw $e;
22✔
639
                                        }
640
                                        continue;
52✔
641
                                }
642
                        }
643
                }
644

645
                return $ints;
187✔
646
        }
647

648
        /**
649
         * Validate that reference numbers can be matched to verses that exist.
650
         *
651
         * @since 2.0.0  Previously, this functionality was handled within the refNumsToInt function.
652
         *
653
         * @param int  $book
654
         * @param ?int $startCh
655
         * @param ?int $startV
656
         * @param ?int $endCh
657
         * @param ?int $endV
658
         *
659
         * @return void
660
         * @throws ScripturNumException
661
         */
662
        protected static function validateRefNums(int $book, &$startCh, &$startV, &$endCh, &$endV)
663
        {
664
                $book--;
1,749✔
665
                if ($startCh > count(Bible::getVerseCounts()[$book]) ||
1,749✔
666
            $endCh > count(Bible::getVerseCounts()[$book]
1,749✔
667
                        )) { // invalid request OR request for a single-chapter book.
1,272✔
668
                        if (Bible::bookHasSingleChapter($book) && $startV === null && $endV === null) { // single-chapter book.
132✔
669
                                $startV  = $startCh;
99✔
670
                                $endV    = $endCh;
99✔
671
                                $startCh = 1;
99✔
672
                                $endCh   = 1;
99✔
673
                        } else {
674
                                throw new ScripturNumException("A chapter was requested that does not exist within the requested book.");
33✔
675
                        }
676
                }
677
                if ($startCh === null && $endCh === null) { // whole book
1,716✔
678
                        $startCh = 1;
33✔
679
                        $endCh   = count(Bible::getVerseCounts()[$book]);
33✔
680
                }
681
                if (($startV - 1) > Bible::getVerseCounts()[$book][$startCh - 1] || ($endV - 1) > Bible::getVerseCounts()[$book][$endCh - 1]) {
1,716✔
682
                        throw new ScripturNumException("A verse was requested that does not exist within the requested chapter.");
44✔
683
                }
684
        }
462✔
685

686
        /**
687
         * Take reference indexes and convert them to the ScripturNum int.  Assumes numbers are already validated by either
688
         * safely existing or being validated against self::validateRefNums()
689
         *
690
         * @param int $book
691
         * @param ?int $startCh
692
         * @param ?int $startV
693
         * @param ?int $endCh
694
         * @param ?int $endV
695
         *
696
         * @return int
697
         */
698
        protected static function refNumsToInt(int $book, $startCh, $startV, $endCh, $endV): int
699
        {
700
                $v = Bible::getVerseCounts();
1,727✔
701
                $book--;
1,727✔
702
                $int = ($book) << 24;
1,727✔
703
                if ($startCh === null && $endCh === null) { // whole book
1,727✔
704
                        $startCh = 1;
11✔
705
                        $endCh   = count($v[$book]);
11✔
706
                }
707
                if ($endCh === null) { // single chapter
1,727✔
708
                        $endCh = $startCh;
22✔
709
                }
710
                if ($startCh !== null) $startCh--;
1,727✔
711
                if ($startV !== null) $startV--;
1,727✔
712
                if ($endCh !== null) $endCh--;
1,727✔
713
                if ($endV !== null) $endV--;
1,727✔
714
                if ($endV === null) {
1,727✔
715
                        $endV = $v[$book][$endCh] - 1;
1,353✔
716
                }
717

718
                $ch = 0;
1,727✔
719
                while ($ch < ($startCh)) {
1,727✔
720
                        $startV += $v[$book][$ch];
1,309✔
721
                        $ch++;
1,309✔
722
                }
723
                $ch = 0;
1,727✔
724
                while ($ch < ($endCh)) {
1,727✔
725
                        $endV += $v[$book][$ch];
1,485✔
726
                        $ch++;
1,485✔
727
                }
728

729
                $int += ($startV << 12);
1,727✔
730
                $int += ($endV);
1,727✔
731

732
                return $int;
1,727✔
733
        }
734

735
        /**
736
         * This function reads through a single ref string (e.g. 3:5-6:9) one character at a time to parse it into a known
737
         * reference.
738
         *
739
         * @param string $string The string to parse.
740
         * @param        $chapterStart
741
         * @param        $verseStart
742
         * @param        $chapterEnd
743
         * @param        $verseEnd
744
         * @param bool   $useHints If true, will consider the values provided to the chapter and verse parameters in
745
         *     parsing the string.  Default false.
746
         *
747
         * @throws ScripturNumException
748
         */
749
        protected static function refNumStringToRefNums(string $string, &$chapterStart = null, &$verseStart = null, &$chapterEnd = null, &$verseEnd = null, bool $useHints = false)
750
        {
751
                if (preg_match('/[a-zA-Z]/', $string)) {
1,771✔
752
                        throw new ScripturNumException("Parse Ref only handles the numerical part of the reference.  Alphabetical characters are not permitted.");
11✔
753
                }
754

755
                $startNums     = [];
1,760✔
756
                $endNums       = [];
1,760✔
757
                $currentNumber = '';
1,760✔
758
                $beforeHyphen  = true;
1,760✔
759
                $useHints      = $useHints && !!$chapterEnd;
1,760✔
760

761
                // adding the extra character allows the last digit to actually get parsed.
762
                foreach (str_split($string . ' ') as $char) {
1,760✔
763
                        if (is_numeric($char)) {
1,760✔
764
                                // still finding the full number
765
                                $currentNumber .= $char;
1,716✔
766
                        } else {
767
                                // End of number.  Int-ify and assign to appropriate half.
768
                                $currentNumber = (int)$currentNumber;
1,760✔
769
                                if ($currentNumber === 0) {
1,760✔
770
                                        continue;
44✔
771
                                }
772
                                if ($beforeHyphen) {
1,716✔
773
                                        $startNums[] = $currentNumber;
1,716✔
774
                                } else {
775
                                        $endNums[] = $currentNumber;
1,001✔
776
                                }
777
                                $currentNumber = ''; // reset for next number.
1,716✔
778

779
                                if ($char == '-') {
1,716✔
780
                                        $beforeHyphen = false;
1,131✔
781
                                }
782
                        }
783
                }
784

785
                $numInx = count($startNums) * 10 + count($endNums) + ($useHints ? 100 : 0);
1,760✔
786
                switch ($numInx) {
160✔
787
                        case 0: // whole book
1,760✔
788
                                $chapterStart = null;
44✔
789
                                $chapterEnd = null;
44✔
790
                                $verseStart = null;
44✔
791
                                $verseEnd = null;
44✔
792
                                break;
44✔
793
                        case 10: // one full chapter
1,716✔
794
                                $chapterStart = $startNums[0];
968✔
795
                                $chapterEnd   = $chapterStart;
968✔
796
                                break;
968✔
797
                        case 110: // One verse, same chapter as previous (Probably?)
1,364✔
798
                                if (!$verseStart && !$verseEnd) { // Previous indicator was chapters only.  This should be, too.
44✔
799
                                        $chapterStart = $startNums[0];
33✔
800
                                        $chapterEnd = $chapterStart;
33✔
801
                                } else { // Previous indicator had verses; this should be verses, too.
802
                                        $chapterStart = $chapterEnd;
11✔
803
                                        $verseStart   = $startNums[0];
11✔
804
                                        $verseEnd     = $verseStart;
11✔
805
                                }
806
                                break;
44✔
807
                        case 11: // multiple full chapters
1,342✔
808
                                $chapterStart = $startNums[0];
627✔
809
                                $chapterEnd   = $endNums[0];
627✔
810
                                break;
627✔
811
                        case 111: // multiple verses from previous chapter (probably?)
737✔
812
                                if (!$verseStart && !$verseEnd) { // Previous indicator was chapters only.  This should be, too.
33✔
813
                                        $chapterStart = $startNums[0];
11✔
814
                                        $chapterEnd = $endNums[0];
11✔
815
                                        $verseStart = null;
11✔
816
                                        $verseEnd = null;
11✔
817
                                } else { // Previous indicator had verses; this should be verses, too.
818
                                        $chapterStart = $chapterEnd;
22✔
819
                                        $verseStart   = $startNums[0];
22✔
820
                                        $verseEnd     = $endNums[0];
22✔
821
                                }
822
                                break;
33✔
823
                        case 12: // full chapter to part of chapter
726✔
824
                        case 112:
704✔
825
                                $chapterStart = $startNums[0];
22✔
826
                                $verseStart   = null;
22✔
827
                                $chapterEnd   = $endNums[0];
22✔
828
                                $verseEnd     = $endNums[1];
22✔
829
                                break;
22✔
830
                        case 20: // one verse.  This is the weird case.
704✔
831
                        case 120:
352✔
832
                                $chapterStart = $startNums[0];
374✔
833
                                $verseStart   = $startNums[1];
374✔
834
                                $chapterEnd   = $chapterStart;
374✔
835
                                $verseEnd     = $verseStart;
374✔
836
                                break;
374✔
837
                        case 21: // multiple verses from one chapter.
352✔
838
                        case 121:
110✔
839
                                $chapterStart = $startNums[0];
242✔
840
                                $chapterEnd   = $chapterStart;
242✔
841
                                $verseStart   = $startNums[1];
242✔
842
                                $verseEnd     = $endNums[0];
242✔
843
                                break;
242✔
844
                        case 22: // multiple verses from across chapters
110✔
845
                        case 122:
22✔
846
                                $chapterStart = $startNums[0];
99✔
847
                                $verseStart   = $startNums[1];
99✔
848
                                $chapterEnd   = $endNums[0];
99✔
849
                                $verseEnd     = $endNums[1];
99✔
850
                                break;
99✔
851
                        default:
852
                                throw new ScripturNumException("Badly formed numerical reference.");
11✔
853
                }
854
        }
477✔
855

856
        /**
857
         * Converts a ScripturNum int into reference numbers.
858
         *
859
         * @param int $int The ScripturNum integer
860
         * @param int $book The book number
861
         * @param int $chapterStart The first Chapter
862
         * @param int $verseStart The first Verse
863
         * @param int $chapterEnd The last Chapter
864
         * @param int $verseEnd The last Verse
865
         *
866
         * @throws ScripturNumException If the reference is unintelligible.
867
         */
868
        protected static function intToRefNums(int $int, &$book, &$chapterStart, &$verseStart, &$chapterEnd, &$verseEnd)
869
        {
870
                $book = $int >> 24;
1,892✔
871
                $int  -= ($book << 24);
1,892✔
872

873
                $refAIndex = $int >> 12;
1,892✔
874
                $int       -= ($refAIndex << 12);
1,892✔
875

876
                $refBIndex = &$int;
1,892✔
877

878
                if ($refBIndex < $refAIndex) {
1,892✔
879
                        throw new ScripturNumException('Unintelligible Reference');
11✔
880
                }
881

882
                self::bkIndexToSingleRef($book, $refAIndex, $chapterStart, $verseStart);
1,881✔
883
                self::bkIndexToSingleRef($book, $refBIndex, $chapterEnd, $verseEnd);
1,870✔
884

885
                $book++;
1,859✔
886
        }
507✔
887

888

889
        /**
890
         * Convert a ScrupturNum int into a concatenated number.  (Concatenated numbers are often used for text libraries.)
891
         *
892
         * @param int        $int The int representing the full passage
893
         * @param string|int $concatStart The concatenated "number" possibly larger than an int representing the start of
894
         *     the passage.
895
         * @param string|int $concatEnd The concatenated "number" possibly larger than an int representing the end of the
896
         *     passage.
897
         *
898
         * @throws ScripturNumException If the reference is unintelligible.
899
         */
900
        public static function intToConcats(int $int, &$concatStart, &$concatEnd)
901
        {
902
                $p = [0, 0, 0, 0, 0];
11✔
903
                self::intToRefNums($int, $p[0], $p[1], $p[2], $p[3], $p[4]);
11✔
904
                $concatStart = $p[2] + ($p[1] * 1000) + ($p[0] * 1000000);
11✔
905
                $concatEnd   = $p[4] + ($p[3] * 1000) + ($p[0] * 1000000);
11✔
906
        }
3✔
907

908
        /**
909
         * Parse a book index number into a chapter and verse.
910
         *
911
         * @param int $book Book number
912
         * @param int $index Verse Index Number
913
         * @param int $chapter Chapter
914
         * @param int $verse Verse
915
         *
916
         * @throws ScripturNumException
917
         */
918
        protected static function bkIndexToSingleRef($book, $index, &$chapter, &$verse)
919
        {
920
                $index++;
1,892✔
921
                $v       = Bible::getVerseCounts();
1,892✔
922
                $chapter = 0;
1,892✔
923
                if ( ! isset($v[$book])) {
1,892✔
924
                        throw new ScripturNumException("There are not that many books in the Bible.");
11✔
925
                }
926
                while ($index > $v[$book][$chapter]) {
1,881✔
927
                        $index -= $v[$book][$chapter];
1,661✔
928
                        if ( ! isset($v[$book][++$chapter])) {
1,661✔
929
                                throw new ScripturNumException("There are not that many verses in this book.");
11✔
930
                        }
931
                }
932
                $chapter++;
1,881✔
933
                $verse = $index;
1,881✔
934
        }
513✔
935

936
        /**
937
         * Given a string with any kind of text content, this method will search for any human-readable scripture references
938
         * and try to parse them into discrete passages.  Returns a ScripturNumArray.
939
         *
940
         * @since 2.0.0
941
         *
942
         * @param string $string
943
         * @param bool   $excludeAllBookOnlyRefs
944
         * @param null   $exceptions
945
         *
946
         * @return ScripturNumArray
947
         */
948
        public static function extractFromString(string $string, bool $excludeAllBookOnlyRefs = false, &$exceptions = null): ScripturNumArray
949
        {
950
                $results = new ScripturNumArray();
176✔
951

952
                $allBookNames = self::getBookNames();
176✔
953
                $allBookNames = array_merge(...$allBookNames);
176✔
954

955
                $cj = static::getConjunctions();
176✔
956

957
                $string = str_replace([$cj['and'], $cj['through'], $cj['chapter']], [',', '-', ''], $string);
176✔
958

959
                if ($excludeAllBookOnlyRefs) {
176✔
960
                        $regExSets = [
8✔
961
                                [
8✔
962
                                        'bs' => $allBookNames,
11✔
963
                                        'ps' => '+'
11✔
964
                                ]
8✔
965
                        ];
8✔
966
                } else {
967
                        $b2 = self::getCommonTerms();
165✔
968
                        $b1 = array_diff($allBookNames, $b2);
165✔
969
                        $regExSets = [
120✔
970
                                [
120✔
971
                                        'bs' => $b1,
165✔
972
                                        'ps' => '*'
165✔
973
                                ],
120✔
974
                                [
120✔
975
                                        'bs' => $b2,
165✔
976
                                        'ps' => '+'
165✔
977
                                ]
120✔
978
                        ];
120✔
979
                        unset($b1, $b2);
165✔
980
                }
981
                unset($allBookNames);
176✔
982

983
        $combinedMatches = [];
176✔
984
                foreach ($regExSets as $re) {
176✔
985
            $b = implode("|", $re['bs']);
176✔
986
            $plusOrStar = $re['ps'];
176✔
987
            /** @noinspection RegExpUnnecessaryNonCapturingGroup -- They really are necessary. */
988
            $pattern = "/\b(?:$b)\.?(?:[-\s,;&]*1?\d{1,2}:?(?:1?\d{1,2})?)$plusOrStar\b/i";
176✔
989

990
            preg_match_all($pattern, $string, $matches, PREG_OFFSET_CAPTURE);
176✔
991

992
            $combinedMatches = array_merge($combinedMatches, $matches[0]);
176✔
993
        }
994

995
        // Sort matches by position in string
996
        usort($combinedMatches, function($a, $b) {
128✔
997
            return $a[1] - $b[1];
66✔
998
        });
176✔
999

1000
        $lastBook = -1;
176✔
1001
        $lastEnd = -1;
176✔
1002
        foreach ($combinedMatches as $m) {
176✔
1003
            try {
1004
                $ints = static::stringToInts($m[0], $exceptions);
154✔
1005
            } catch (ScripturNumException $e) {
11✔
1006
                continue;
11✔
1007
            }
1008
            foreach($ints as $i) {
154✔
1009
                try {
1010
                    $sn = new static($i);
154✔
NEW
1011
                } catch (ScripturNumException $e) {
×
NEW
1012
                    continue;
×
1013
                }
1014
                if ($lastBook != $sn->book && $lastEnd > $m[1]) {
154✔
1015
                    // See issue #14
1016
                    continue;
11✔
1017
                }
1018
                $results[] = $sn;
154✔
1019
                $lastBook = $sn->book;
154✔
1020
            }
1021
            $lastEnd = $m[1] + strlen($m[0]);
154✔
1022
        }
1023

1024
                return $results;
176✔
1025
        }
1026

1027
        /**
1028
         * Test whether a given passage is within a given larger passage.  Will also return true if they are the same.
1029
         *
1030
         * @since 2.0.0
1031
         *
1032
         * @param int $largerPassage
1033
         *
1034
         * @return bool
1035
         */
1036
        public function isWithinInt(int $largerPassage): bool
1037
        {
1038
                if (($this->int & self::BOOK_MASK) != ($largerPassage & self::BOOK_MASK))
154✔
1039
                        return false;
22✔
1040

1041
                if (($this->int & self::START_MASK) < ($largerPassage & self::START_MASK))
132✔
1042
                        return false;
22✔
1043

1044
                if (($this->int & self::END_MASK) > ($largerPassage & self::END_MASK))
110✔
1045
                        return false;
22✔
1046

1047
                return true;
88✔
1048
        }
1049

1050
        /**
1051
         * Test whether a given passage is within a given larger passage.  Will also return true if they are the same.
1052
         *
1053
         * @since 2.0.0
1054
         *
1055
         * @param ScripturNum $largerPassage
1056
         *
1057
         * @return bool
1058
         */
1059
        public function isWithin(ScripturNum $largerPassage): bool
1060
        {
1061
                return $this->isWithinInt($largerPassage->getInt());
77✔
1062
        }
1063

1064
        /**
1065
         * Test whether a given passage has any commonality with another passage.
1066
         *
1067
         * @since 2.0.0
1068
         *
1069
         * @param int $otherPassage
1070
         *
1071
         * @return bool
1072
         */
1073
        public function overlapsWithInt(int $otherPassage): bool
1074
        {
1075
                if (($this->int & self::BOOK_MASK) != ($otherPassage & self::BOOK_MASK))
165✔
1076
                        return false;
33✔
1077

1078
                if (($this->int & self::START_MASK) > (($otherPassage & self::END_MASK) << 12))
132✔
1079
                        return false;
22✔
1080

1081
                if (($this->int & self::END_MASK) < (($otherPassage & self::START_MASK) >> 12))
110✔
1082
                        return false;
22✔
1083

1084
                return true;
88✔
1085
        }
1086

1087
        /**
1088
         * Test whether a given passage has any commonality with another passage.
1089
         *
1090
         * @since 2.0.0
1091
         *
1092
         * @param ScripturNum $otherPassage
1093
         *
1094
         * @return bool
1095
         */
1096
        public function overlapsWith(ScripturNum $otherPassage): bool
1097
        {
1098
                return $this->overlapsWithInt($otherPassage->getInt());
88✔
1099
        }
1100

1101
        /**
1102
         * Test whether a given passage has any commonality with another passage, or is adjacent to it.
1103
         *
1104
         * @since 2.0.0
1105
         *
1106
         * @param int $otherPassage
1107
         *
1108
         * @return bool
1109
         */
1110
        public function overlapsOrAdjacentInt(int $otherPassage): bool
1111
        {
1112
                if (($this->int & self::BOOK_MASK) != ($otherPassage & self::BOOK_MASK))
473✔
1113
                        return false;
132✔
1114

1115
                if (($this->int & self::START_MASK) - (1 << 12) > (($otherPassage & self::END_MASK) << 12))
374✔
1116
                        return false;
44✔
1117

1118
                if ((($this->int & self::END_MASK) + 1) < (($otherPassage & self::START_MASK) >> 12))
330✔
1119
                        return false;
154✔
1120

1121
                return true;
198✔
1122
        }
1123

1124
        /**
1125
         * Test whether a given passage has any commonality with another passage, or is adjacent to it.
1126
         *
1127
         * @since 2.0.0
1128
         *
1129
         * @param ScripturNum $otherPassage
1130
         *
1131
         * @return bool
1132
         */
1133
        public function overlapsOrAdjacent(ScripturNum $otherPassage): bool
1134
        {
1135
                return $this->overlapsOrAdjacentInt($otherPassage->getInt());
297✔
1136
        }
1137

1138
        /**
1139
         * Combines two adjacent or overlapping passages into one int.
1140
         *
1141
         * @since 2.0.0
1142
         *
1143
         * @param int $otherPassage
1144
         *
1145
         * @return int
1146
         * @throws ScripturNumException
1147
         *@since 2.0.0
1148
         */
1149
        public function combineWithInt(int $otherPassage): int
1150
        {
1151
                if (!$this->overlapsOrAdjacentInt($otherPassage)) {
110✔
1152
                        throw new ScripturNumException("Cannot combine passages that aren't overlapping or adjacent.");
22✔
1153
                }
1154

1155
                $newInt = $this->int & self::BOOK_MASK;
88✔
1156
                $newInt += min($this->int & self::START_MASK, $otherPassage & self::START_MASK);
88✔
1157
                $newInt += max($this->int & self::END_MASK, $otherPassage & self::END_MASK);
88✔
1158

1159
                return $newInt;
88✔
1160
        }
1161

1162
        /**
1163
         * Combines two adjacent or overlapping passages into one ScripturNum.
1164
         *
1165
         * @since 2.0.0
1166
         *
1167
         * @param ScripturNum $otherPassage
1168
         *
1169
         * @return ScripturNum
1170
         * @throws ScripturNumException
1171
         */
1172
        public function combineWith(ScripturNum $otherPassage): ScripturNum
1173
        {
1174
                $int = $this->combineWithInt($otherPassage->getInt());
77✔
1175
                return new ScripturNum($int);
66✔
1176
        }
1177

1178
        /**
1179
         * Test whether a given passage contains a given smaller passage.  Will also return true if they are the same.
1180
         *
1181
         * @param int $smallerPassage
1182
         *
1183
         * @return bool
1184
         */
1185
        public function containsInt(int $smallerPassage): bool
1186
        {
1187
                if (($this->int & self::BOOK_MASK) != ($smallerPassage & self::BOOK_MASK))
132✔
1188
                        return false;
22✔
1189

1190
                if (($this->int & self::START_MASK) > ($smallerPassage & self::START_MASK))
110✔
1191
                        return false;
66✔
1192

1193
                if (($this->int & self::END_MASK) < ($smallerPassage & self::END_MASK))
44✔
1194
                        return false;
22✔
1195

1196
                return true;
22✔
1197
        }
1198

1199
        /**
1200
         * Test whether a given passage contains a given smaller passage.  Will also return true if they are the same.
1201
         *
1202
         * @param ScripturNum $smallerPassage
1203
         *
1204
         * @return bool
1205
         */
1206
        public function contains(ScripturNum $smallerPassage): bool
1207
        {
1208
                return $this->containsInt($smallerPassage->getInt());
66✔
1209
        }
1210

1211
        /**
1212
         * Generate a query statement that can be used to search an int column in generic SQL for a passage that is
1213
         * entirely contained within the given ScripturNum.
1214
         *
1215
         * @param string $columnRef  The name of the column or value to use in the query.
1216
         *
1217
         * @return string
1218
         */
1219
        public function toSqlExclusive(string $columnRef): string
1220
        {
1221
                if ($this->isSingleVerse()) {
99✔
1222
                        $i = $this->getInt();
22✔
1223
                        return "$columnRef = $i";
22✔
1224
                }
1225

1226
                $r = [];
77✔
1227

1228
                $mask = self::BOOK_MASK;
77✔
1229
                $value = $this->int & self::BOOK_MASK;
77✔
1230
                $r[] = "($columnRef & $mask) = $value";
77✔
1231

1232
                $mask = self::START_MASK;
77✔
1233
                $value = ($this->int & self::START_MASK);
77✔
1234
                $r[] = "($columnRef & $mask) >= $value";
77✔
1235

1236
                $mask = self::END_MASK;
77✔
1237
                $value = ($this->int & self::END_MASK);
77✔
1238
                $r[] = "($columnRef & $mask) <= $value";
77✔
1239

1240
                return "( " . implode(" AND ", $r) . " )";
77✔
1241
        }
1242

1243
        /**
1244
         * Generate a query statement that can be used to search an int column in generic SQL for a passage that overlaps
1245
         * with the given ScripturNum.
1246
         *
1247
         * @param string $columnRef  The name of the column or value to use in the query.
1248
         *
1249
         * @return string
1250
         */
1251
        public function toSqlInclusive(string $columnRef): string
1252
        {
1253
                $r = [];
99✔
1254

1255
                $mask = self::BOOK_MASK;
99✔
1256
                $value = $this->int & self::BOOK_MASK;
99✔
1257
                $r[] = "($columnRef & $mask) = $value";
99✔
1258

1259
                $mask = self::START_MASK;
99✔
1260
                $value = ($this->int & self::END_MASK) << 12;
99✔
1261
                $r[] = "($columnRef & $mask) <= $value";
99✔
1262

1263
                $mask = self::END_MASK;
99✔
1264
                $value = ($this->int & self::START_MASK) >> 12;
99✔
1265
                $r[] = "($columnRef & $mask) >= $value";
99✔
1266

1267
                return "( " . implode(" AND ", $r) . " )";
99✔
1268
        }
1269
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc