quran-processing-toolkit 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,672 @@
1
+ """Typed objects representing addresses, tokens, words, verses, chapters, and juzs.
2
+
3
+ The hierarchy follows the corpus rather than Python list positions: chapters, verses,
4
+ words, and token parts are all addressed from one. A ``Word`` may contain several
5
+ ``Token`` objects because QAC stores prefixes, stems, and suffixes as separate
6
+ morphological segments. Convenience properties on a word normally aggregate only its
7
+ stem tokens so that prefixes do not unexpectedly supply a root or part of speech.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import dataclass
13
+ from typing import Any
14
+
15
+ from .chapter_names import chapter_name
16
+ from .collections import OneBasedCollection, QuerySet
17
+ from .text import TextMixin, split_surface_words
18
+
19
+
20
+ @dataclass(frozen=True, slots=True, order=True)
21
+ class VerseAddress:
22
+ """Immutable ``chapter:verse`` address using Quranic 1-based numbers."""
23
+
24
+ chapter: int
25
+ verse: int
26
+
27
+ def __iter__(self):
28
+ yield self.chapter
29
+ yield self.verse
30
+
31
+ def __str__(self) -> str:
32
+ return f"{self.chapter}:{self.verse}"
33
+
34
+
35
+ @dataclass(frozen=True, slots=True, order=True)
36
+ class WordAddress:
37
+ """Immutable ``chapter:verse:word`` address using 1-based numbers."""
38
+
39
+ chapter: int
40
+ verse: int
41
+ word: int
42
+
43
+ def __iter__(self):
44
+ yield self.chapter
45
+ yield self.verse
46
+ yield self.word
47
+
48
+ def as_tuple(self) -> tuple[int, int, int]:
49
+ """Return the address in the form used by keyed collection lookups."""
50
+ return self.chapter, self.verse, self.word
51
+
52
+ def __str__(self) -> str:
53
+ return f"{self.chapter}:{self.verse}:{self.word}"
54
+
55
+
56
+ @dataclass(frozen=True, slots=True, order=True)
57
+ class TokenAddress:
58
+ """Immutable ``chapter:verse:word:part`` address using 1-based numbers."""
59
+
60
+ chapter: int
61
+ verse: int
62
+ word: int
63
+ part: int
64
+
65
+ def __iter__(self):
66
+ yield self.chapter
67
+ yield self.verse
68
+ yield self.word
69
+ yield self.part
70
+
71
+ def as_tuple(self) -> tuple[int, int, int, int]:
72
+ """Return the address in the form used by keyed collection lookups."""
73
+ return self.chapter, self.verse, self.word, self.part
74
+
75
+ def __str__(self) -> str:
76
+ return f"{self.chapter}:{self.verse}:{self.word}:{self.part}"
77
+
78
+
79
+ class Token(TextMixin):
80
+ """One QAC morphological segment with source and convenience annotations.
81
+
82
+ Unknown attributes are looked up against the retained uppercase record keys for
83
+ backwards compatibility. New code should prefer the documented lowercase
84
+ properties such as :attr:`root`, :attr:`aspect`, and :attr:`case`.
85
+ """
86
+
87
+ __slots__ = ("_data", "_nun_of_emphasis")
88
+
89
+ def __init__(self, data: dict[str, Any]) -> None:
90
+ self._data = data
91
+ # Filled when Word assembles the segments; never added to source records.
92
+ self._nun_of_emphasis = False
93
+
94
+ def has_nun_of_emphasis(self) -> bool:
95
+ """Return whether this verb stem has an explicit ``+n:EMPH`` suffix.
96
+
97
+ The containing Word establishes suffix ownership during construction.
98
+ Other tokens (including the suffix itself) return False, as do standalone
99
+ tokens not yet assembled into a Word. This is not a spelling heuristic.
100
+ """
101
+ return self._nun_of_emphasis
102
+
103
+ @property
104
+ def address(self) -> TokenAddress:
105
+ """Return this segment's complete semantic address."""
106
+ return TokenAddress(
107
+ self._data["CHAPTER"],
108
+ self._data["VERSE"],
109
+ self._data["WORD"],
110
+ self._data["WORD_PART"],
111
+ )
112
+
113
+ @property
114
+ def address_tuple(self) -> tuple[int, int, int, int]:
115
+ """Return ``(chapter, verse, word, part)`` without allocating an address."""
116
+ return (
117
+ self._data["CHAPTER"],
118
+ self._data["VERSE"],
119
+ self._data["WORD"],
120
+ self._data["WORD_PART"],
121
+ )
122
+
123
+ def __getattr__(self, name: str) -> Any:
124
+ try:
125
+ return self._data[name]
126
+ except KeyError as error:
127
+ raise AttributeError(name) from error
128
+
129
+ def __repr__(self) -> str:
130
+ return f"Token({self.address}, {self.form!r}, tag={self.tag!r})"
131
+
132
+ def get(self, field: str, default: Any = None) -> Any:
133
+ """Read an uppercase derived-record field without raising when absent."""
134
+ return self._data.get(field, default)
135
+
136
+ @property
137
+ def data(self) -> dict[str, Any]:
138
+ """Return a shallow copy of all retained and derived record fields."""
139
+ return self._data.copy()
140
+
141
+ @property
142
+ def chapter(self) -> int:
143
+ """Return the 1-based chapter number."""
144
+ return self._data["CHAPTER"]
145
+
146
+ @property
147
+ def verse(self) -> int:
148
+ """Return the 1-based verse number within the chapter."""
149
+ return self._data["VERSE"]
150
+
151
+ @property
152
+ def word(self) -> int:
153
+ """Return the 1-based word number within the verse."""
154
+ return self._data["WORD"]
155
+
156
+ @property
157
+ def part(self) -> int:
158
+ """Return the 1-based morphological part number within the word."""
159
+ return self._data["WORD_PART"]
160
+
161
+ @property
162
+ def form(self) -> str:
163
+ """Return the source Buckwalter surface form for this segment."""
164
+ return self._data["FORM"]
165
+
166
+ def get_form(self) -> str:
167
+ """Return this segment's Buckwalter surface form."""
168
+ return self.form
169
+
170
+ @property
171
+ def tag(self) -> str:
172
+ """Return the source QAC tag, such as ``V``, ``N``, or ``P``."""
173
+ return self._data["TAG"]
174
+
175
+ @property
176
+ def role(self) -> str:
177
+ """Return ``PREFIX``, ``STEM``, or ``SUFFIX``."""
178
+ return self._data["TOKEN_ROLE"]
179
+
180
+ @property
181
+ def source_features(self) -> tuple[str, ...]:
182
+ """Return the untouched non-keyed QAC features for this segment."""
183
+ return tuple(
184
+ feature for feature in self.source_feature_text.split("|") if ":" not in feature
185
+ )
186
+
187
+ @property
188
+ def source_feature_text(self) -> str:
189
+ """Return the complete original QAC feature column."""
190
+ return self._data["FEATURES"]
191
+
192
+ @property
193
+ def part_of_speech(self) -> str | None:
194
+ """Return the QAC ``POS`` code when the segment supplies one."""
195
+ return self._data.get("POS")
196
+
197
+ @property
198
+ def root(self) -> str | None:
199
+ """Return the Buckwalter root annotation when present."""
200
+ return self._data.get("ROOT")
201
+
202
+ @property
203
+ def lemma(self) -> str | None:
204
+ """Return the Buckwalter lemma annotation when present."""
205
+ return self._data.get("LEM")
206
+
207
+ @property
208
+ def transliteration(self) -> str:
209
+ """Return the segment's Buckwalter surface form."""
210
+ return self.form
211
+
212
+ @property
213
+ def verb_form(self) -> str | None:
214
+ """Return the derived verb-form/baab code, such as ``I`` or ``IV``."""
215
+ return self._data.get("VERB_FORM")
216
+
217
+ @property
218
+ def baab(self) -> str | None:
219
+ """Alias for :attr:`verb_form`."""
220
+ return self.verb_form
221
+
222
+ @property
223
+ def aspect(self) -> str | None:
224
+ """Return ``PERFECT``, ``IMPERFECT``, or ``IMPERATIVE`` when present."""
225
+ return self._data.get("ASPECT")
226
+
227
+ @property
228
+ def tense(self) -> str | None:
229
+ """Return the toolkit's derived tense label when present."""
230
+ return self._data.get("TENSE")
231
+
232
+ @property
233
+ def voice(self) -> str | None:
234
+ """Return ``ACTIVE`` or ``PASSIVE`` for finite verbs."""
235
+ return self._data.get("VOICE")
236
+
237
+ @property
238
+ def case(self) -> str | None:
239
+ """Return the derived case/state annotation when present."""
240
+ return self._data.get("CASE")
241
+
242
+ @property
243
+ def mood(self) -> str | None:
244
+ """Return the untouched keyed QAC mood annotation when present."""
245
+ return self._data.get("MOOD")
246
+
247
+ @property
248
+ def gender(self) -> str | None:
249
+ """Return the derived ``MASC`` or ``FEM`` annotation when present."""
250
+ return self._data.get("GENDER")
251
+
252
+ @property
253
+ def definiteness(self) -> str | None:
254
+ """Return the derived definiteness annotation when present."""
255
+ return self._data.get("DEFINITENESS")
256
+
257
+ @property
258
+ def derived_noun(self) -> str | None:
259
+ """Return participle or verbal-noun classification when present."""
260
+ return self._data.get("DERIVED_NOUN")
261
+
262
+ @property
263
+ def conjugation(self) -> int | None:
264
+ """Return the legacy 1-through-14 conjugation identifier when present."""
265
+ return self._data.get("CONJUGATE")
266
+
267
+ @property
268
+ def person(self) -> int | None:
269
+ """Return grammatical person derived from the conjugation annotation."""
270
+ if self.conjugation is None:
271
+ return None
272
+ return int(_CONJUGATION_LABELS[self.conjugation][0])
273
+
274
+ @property
275
+ def grammatical_number(self) -> str | None:
276
+ """Return ``SINGULAR``, ``DUAL``, or ``PLURAL`` when annotated."""
277
+ if self.conjugation is not None:
278
+ suffix = _CONJUGATION_LABELS[self.conjugation][-1]
279
+ return {"S": "SINGULAR", "D": "DUAL", "P": "PLURAL"}[suffix]
280
+ return self._data.get("COUNT")
281
+
282
+ def has_source_feature(self, feature: str) -> bool:
283
+ """Return whether the exact non-keyed feature occurs in the source column."""
284
+ return feature in self.source_features
285
+
286
+
287
+ _CONJUGATION_LABELS = {
288
+ 1: "3MS",
289
+ 2: "3MD",
290
+ 3: "3MP",
291
+ 4: "3FS",
292
+ 5: "3FD",
293
+ 6: "3FP",
294
+ 7: "2MS",
295
+ 8: "2MD",
296
+ 9: "2MP",
297
+ 10: "2FS",
298
+ 11: "2FD",
299
+ 12: "2FP",
300
+ 13: "1S",
301
+ 14: "1P",
302
+ }
303
+
304
+ VERB_FORM_NAMES = {
305
+ "I": "Thulathy Mujarrad",
306
+ "II": "Taf'eel",
307
+ "III": "Mufaa'alah",
308
+ "IV": "If'aal",
309
+ "V": "Tafa'ul",
310
+ "VI": "Tafaa'ul",
311
+ "VII": "Infi'aal",
312
+ "VIII": "Ifti'aal",
313
+ "IX": "If'ilaal",
314
+ "X": "Istif'aal",
315
+ "XI": "If'eelaal",
316
+ "XII": "Other",
317
+ "rI": "Ruba'iy Mujarrad",
318
+ "rIV": "If'illaal",
319
+ }
320
+
321
+
322
+ class Word(TextMixin):
323
+ """One orthographic Quran word assembled from one or more QAC tokens."""
324
+
325
+ __slots__ = ("address", "tokens", "_stem_tokens", "arabic_text")
326
+
327
+ def __init__(self, address: WordAddress, tokens: list[Token], arabic_text: str) -> None:
328
+ self.address = address
329
+ self.tokens = OneBasedCollection(tokens)
330
+ self._stem_tokens = tuple(token for token in tokens if token.role == "STEM")
331
+ self.arabic_text = arabic_text
332
+ # A suffix belongs to the preceding stem, stopping at each new stem.
333
+ # EMPH also labels lam prefixes, so its tag alone is not sufficient.
334
+ stem: Token | None = None
335
+ for token in tokens:
336
+ if token.role == "STEM":
337
+ stem = token
338
+ stem._nun_of_emphasis = False
339
+ elif (
340
+ token.role == "SUFFIX"
341
+ and token.get("+n") == "EMPH"
342
+ and stem is not None
343
+ and stem.tag == "V"
344
+ ):
345
+ stem._nun_of_emphasis = True
346
+
347
+ def has_nun_of_emphasis(self) -> bool:
348
+ """Return whether any verb stem has an annotated nun-of-emphasis suffix."""
349
+ return any(token.has_nun_of_emphasis() for token in self._stem_tokens)
350
+
351
+ @property
352
+ def stem_tokens(self) -> QuerySet[Token]:
353
+ """Return only morphological segments classified as stems."""
354
+ return QuerySet(self._stem_tokens)
355
+
356
+ def __repr__(self) -> str:
357
+ return f"Word({self.address}, {self.arabic_text!r})"
358
+
359
+ @property
360
+ def chapter(self) -> int:
361
+ """Return the 1-based chapter number."""
362
+ return self.address.chapter
363
+
364
+ @property
365
+ def verse(self) -> int:
366
+ """Return the 1-based verse number within the chapter."""
367
+ return self.address.verse
368
+
369
+ @property
370
+ def word(self) -> int:
371
+ """Return the 1-based word number within the verse."""
372
+ return self.address.word
373
+
374
+ @property
375
+ def transliteration(self) -> str:
376
+ """Join all token forms into the word's complete Buckwalter spelling."""
377
+ return "".join(token.form for token in self.tokens)
378
+
379
+ def values(self, field: str, *, stems_only: bool = True) -> tuple[Any, ...]:
380
+ """Return distinct values of an uppercase token field in source order.
381
+
382
+ Stem-only aggregation is the default because affixes can have their own part
383
+ of speech and other annotations. Pass ``stems_only=False`` when explicitly
384
+ inspecting a complete segmented word.
385
+ """
386
+ tokens = self._stem_tokens if stems_only else self.tokens
387
+ result: list[Any] = []
388
+ for token in tokens:
389
+ value = token.get(field)
390
+ if value is not None and value not in result:
391
+ result.append(value)
392
+ return tuple(result)
393
+
394
+ def value(self, field: str, *, stems_only: bool = True) -> Any:
395
+ """Return ``None``, one field value, or a tuple for a multi-valued word."""
396
+ values = self.values(field, stems_only=stems_only)
397
+ if not values:
398
+ return None
399
+ return values[0] if len(values) == 1 else values
400
+
401
+ @property
402
+ def roots(self) -> tuple[str, ...]:
403
+ """Return every distinct stem root in source order."""
404
+ return self.values("ROOT")
405
+
406
+ @property
407
+ def root(self) -> str | tuple[str, ...] | None:
408
+ """Return the sole root, multiple roots, or ``None``."""
409
+ return self.value("ROOT")
410
+
411
+ @property
412
+ def lemmas(self) -> tuple[str, ...]:
413
+ """Return every distinct stem lemma in source order."""
414
+ return self.values("LEM")
415
+
416
+ @property
417
+ def lemma(self) -> str | tuple[str, ...] | None:
418
+ """Return the sole lemma, multiple lemmas, or ``None``."""
419
+ return self.value("LEM")
420
+
421
+ @property
422
+ def parts_of_speech(self) -> tuple[str, ...]:
423
+ """Return every distinct stem part-of-speech code."""
424
+ return self.values("POS")
425
+
426
+ @property
427
+ def part_of_speech(self) -> str | tuple[str, ...] | None:
428
+ """Return one part of speech, several, or ``None``."""
429
+ return self.value("POS")
430
+
431
+ @property
432
+ def is_multi_stem(self) -> bool:
433
+ """Return whether QAC represents the word with more than one stem."""
434
+ return len(self._stem_tokens) > 1
435
+
436
+ def has_part_of_speech(self, value: str) -> bool:
437
+ """Return whether any stem has the exact QAC part-of-speech code."""
438
+ return value in self.parts_of_speech
439
+
440
+ def is_verb(self) -> bool:
441
+ """Return whether any stem is tagged as a verb."""
442
+ return self.has_part_of_speech("V")
443
+
444
+ def is_noun(self) -> bool:
445
+ """Return whether any stem is tagged as a noun."""
446
+ return self.has_part_of_speech("N")
447
+
448
+ @property
449
+ def verb_form(self) -> str | tuple[str, ...] | None:
450
+ """Return one or more Roman-numeral verb-form codes."""
451
+ return self.value("VERB_FORM")
452
+
453
+ @property
454
+ def baab(self) -> str | tuple[str, ...] | None:
455
+ """Convenience alias for :attr:`verb_form`."""
456
+ return self.verb_form
457
+
458
+ @property
459
+ def baab_name(self) -> str | tuple[str, ...] | None:
460
+ """Return familiar transliterated names for the word's verb forms."""
461
+ forms = self.values("VERB_FORM")
462
+ names = tuple(VERB_FORM_NAMES.get(form, form) for form in forms)
463
+ return names[0] if len(names) == 1 else names or None
464
+
465
+ def get_baab(self) -> str | tuple[str, ...] | None:
466
+ """Compatibility alias for the prototype's named-baab method."""
467
+ return self.baab_name
468
+
469
+ def get_form(self) -> str | tuple[str, ...] | None:
470
+ """Return the verb form/baab code, such as ``I`` or ``IV``."""
471
+ return self.verb_form
472
+
473
+ def get_root(self) -> str | tuple[str, ...] | None:
474
+ """Compatibility method returning :attr:`root`."""
475
+ return self.root
476
+
477
+ def get_lemma(self) -> str | tuple[str, ...] | None:
478
+ """Compatibility method returning :attr:`lemma`."""
479
+ return self.lemma
480
+
481
+ @property
482
+ def tense(self) -> str | tuple[str, ...] | None:
483
+ """Return derived convenience tense values from all stems."""
484
+ return self.value("TENSE")
485
+
486
+ def get_tense(self) -> str | tuple[str, ...] | None:
487
+ """Compatibility method returning :attr:`tense`."""
488
+ return self.tense
489
+
490
+ @property
491
+ def aspect(self) -> str | tuple[str, ...] | None:
492
+ """Return grammatical aspect values from all stems."""
493
+ return self.value("ASPECT")
494
+
495
+ def get_aspect(self) -> str | tuple[str, ...] | None:
496
+ """Compatibility method returning :attr:`aspect`."""
497
+ return self.aspect
498
+
499
+ @property
500
+ def voice(self) -> str | tuple[str, ...] | None:
501
+ """Return finite verb voice values from all stems."""
502
+ return self.value("VOICE")
503
+
504
+ def get_voice(self) -> str | tuple[str, ...] | None:
505
+ """Compatibility method returning :attr:`voice`."""
506
+ return self.voice
507
+
508
+ @property
509
+ def conjugation(self) -> int | tuple[int, ...] | None:
510
+ """Return legacy numeric conjugation identifiers from all stems."""
511
+ return self.value("CONJUGATE")
512
+
513
+ @property
514
+ def conjugation_labels(self) -> tuple[str, ...]:
515
+ """Return compact person/gender/number labels such as ``3MS``."""
516
+ return tuple(
517
+ _CONJUGATION_LABELS[number]
518
+ for number in self.values("CONJUGATE")
519
+ if number in _CONJUGATION_LABELS
520
+ )
521
+
522
+ @property
523
+ def person(self) -> int | tuple[int, ...] | None:
524
+ """Return one or more grammatical persons derived from conjugation."""
525
+ values = tuple(dict.fromkeys(int(label[0]) for label in self.conjugation_labels))
526
+ return values[0] if len(values) == 1 else values or None
527
+
528
+ def get_person(self) -> int | tuple[int, ...] | None:
529
+ """Compatibility method returning :attr:`person`."""
530
+ return self.person
531
+
532
+ @property
533
+ def grammatical_number(self) -> str | tuple[str, ...] | None:
534
+ """Return singular, dual, or plural from conjugation or nominal count."""
535
+ mapping = {"S": "SINGULAR", "D": "DUAL", "P": "PLURAL"}
536
+ values = tuple(dict.fromkeys(mapping[label[-1]] for label in self.conjugation_labels))
537
+ if values:
538
+ return values[0] if len(values) == 1 else values
539
+ # Nominals carry COUNT directly rather than through a finite-verb
540
+ # conjugation label. Falling back here makes with_number() useful for both.
541
+ return self.value("COUNT")
542
+
543
+ def get_number(self) -> str | tuple[str, ...] | None:
544
+ """Compatibility method returning :attr:`grammatical_number`."""
545
+ return self.grammatical_number
546
+
547
+ @property
548
+ def gender(self) -> str | tuple[str, ...] | None:
549
+ """Return masculine/feminine values from all stems."""
550
+ return self.value("GENDER")
551
+
552
+ def get_gender(self) -> str | tuple[str, ...] | None:
553
+ """Compatibility method returning :attr:`gender`."""
554
+ return self.gender
555
+
556
+ @property
557
+ def case(self) -> str | tuple[str, ...] | None:
558
+ """Return the word's derived case/state annotation."""
559
+ return self.value("CASE")
560
+
561
+ @property
562
+ def mood(self) -> str | tuple[str, ...] | None:
563
+ """Return the word's untouched keyed QAC mood annotation."""
564
+ return self.value("MOOD")
565
+
566
+ @property
567
+ def definiteness(self) -> str | tuple[str, ...] | None:
568
+ """Return the word's derived definiteness annotation."""
569
+ return self.value("DEFINITENESS")
570
+
571
+ @property
572
+ def derived_noun(self) -> str | tuple[str, ...] | None:
573
+ """Return verbal-noun or participle classification when present."""
574
+ return self.value("DERIVED_NOUN")
575
+
576
+
577
+ class Verse(TextMixin):
578
+ """A Quran verse with words, Uthmani text, and an optional translation."""
579
+
580
+ __slots__ = ("address", "words", "arabic_text", "translation_text")
581
+
582
+ def __init__(
583
+ self,
584
+ address: VerseAddress,
585
+ words: list[Word],
586
+ arabic_text: str,
587
+ translation_text: str | None,
588
+ ) -> None:
589
+ self.address = address
590
+ self.words = OneBasedCollection(words)
591
+ self.arabic_text = arabic_text
592
+ self.translation_text = translation_text
593
+
594
+ def __repr__(self) -> str:
595
+ return f"Verse({self.address}, {len(self.words)} words)"
596
+
597
+ def surface_words(self, *, ignore_pause_marks: bool = False) -> tuple[str, ...]:
598
+ """Return space-delimited Arabic segments, independently of QAC words."""
599
+ return split_surface_words(self.arabic_text, ignore_pause_marks=ignore_pause_marks)
600
+
601
+ def get_chapter_name(self, representation: str = "transliteration") -> str:
602
+ """Return this verse's chapter name in transliteration, Arabic, or English."""
603
+ return chapter_name(self.chapter, representation)
604
+
605
+ @property
606
+ def chapter(self) -> int:
607
+ """Return the 1-based chapter number."""
608
+ return self.address.chapter
609
+
610
+ @property
611
+ def verse(self) -> int:
612
+ """Return the 1-based verse number within the chapter."""
613
+ return self.address.verse
614
+
615
+ @property
616
+ def transliteration(self) -> str:
617
+ """Join word transliterations with spaces in Quranic order."""
618
+ return " ".join(word.transliteration for word in self.words)
619
+
620
+ @property
621
+ def has_translation(self) -> bool:
622
+ """Return whether a user-supplied translation covers this verse."""
623
+ return self.translation_text is not None
624
+
625
+
626
+ class Chapter:
627
+ """A 1-based chapter containing a 1-based verse collection."""
628
+
629
+ __slots__ = ("number", "verses")
630
+
631
+ def __init__(self, number: int, verses: list[Verse]) -> None:
632
+ self.number = number
633
+ self.verses = OneBasedCollection(verses)
634
+
635
+ @property
636
+ def chapter(self) -> int:
637
+ """Alias for the chapter's numeric address."""
638
+ return self.number
639
+
640
+ @property
641
+ def name(self) -> str:
642
+ """Return the conventional ASCII transliterated chapter name."""
643
+ return self.get_name()
644
+
645
+ @property
646
+ def name_arabic(self) -> str:
647
+ """Return the Arabic chapter name (not verse text)."""
648
+ return self.get_name("arabic")
649
+
650
+ @property
651
+ def name_english(self) -> str:
652
+ """Return an English meaning of the chapter name, not a verse translation."""
653
+ return self.get_name("english")
654
+
655
+ def get_name(self, representation: str = "transliteration") -> str:
656
+ """Return the chapter name in transliteration, Arabic, or English."""
657
+ return chapter_name(self.number, representation)
658
+
659
+ def __repr__(self) -> str:
660
+ return f"Chapter({self.number}, {len(self.verses)} verses)"
661
+
662
+
663
+ @dataclass(frozen=True, slots=True)
664
+ class Juz:
665
+ """One of thirty sequential juz divisions and its ordered verses."""
666
+
667
+ number: int
668
+ start: VerseAddress
669
+ verses: QuerySet[Verse]
670
+
671
+ def __len__(self) -> int:
672
+ return len(self.verses)
@@ -0,0 +1 @@
1
+