scripturelookup 0.0.1__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.
- geezify-python-main/LICENSE +21 -0
- geezify-python-main/README.md +8 -0
- geezify-python-main/arabify.py +43 -0
- geezify-python-main/arabify_test.py +13 -0
- geezify-python-main/geezify.py +49 -0
- geezify-python-main/geezify_test.py +13 -0
- geezify-python-main/test_data.py +92 -0
- scripturelookup/__init__.py +0 -0
- scripturelookup/command_line.py +37 -0
- scripturelookup/data/metadata-languages.min.json +940 -0
- scripturelookup/data/metadata-scriptures.min.json +59792 -0
- scripturelookup/data.py +58 -0
- scripturelookup/lookup.py +307 -0
- scripturelookup/numbers.py +267 -0
- scripturelookup-0.0.1.dist-info/LICENSE +21 -0
- scripturelookup-0.0.1.dist-info/METADATA +40 -0
- scripturelookup-0.0.1.dist-info/RECORD +20 -0
- scripturelookup-0.0.1.dist-info/WHEEL +5 -0
- scripturelookup-0.0.1.dist-info/entry_points.txt +2 -0
- scripturelookup-0.0.1.dist-info/top_level.txt +2 -0
scripturelookup/data.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Python standard libraries
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
# Third-party libraries
|
|
7
|
+
import requests
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
data_directory = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'data')
|
|
11
|
+
os.makedirs(data_directory, exist_ok = True)
|
|
12
|
+
|
|
13
|
+
# Download JSON data
|
|
14
|
+
def download_data(filename, filepath):
|
|
15
|
+
request_url = f'https://raw.githubusercontent.com/samuelbradshaw/python-scripture-scraper/refs/heads/main/sample/{filename}'
|
|
16
|
+
r = requests.get(request_url)
|
|
17
|
+
r.encoding = 'utf-8'
|
|
18
|
+
if r and r.status_code == 200:
|
|
19
|
+
data = r.content
|
|
20
|
+
with open(filepath, 'wb') as f:
|
|
21
|
+
f.write(data)
|
|
22
|
+
else:
|
|
23
|
+
sys.exit('\nError: Couldn’t download JSON data:\n{request_url}\n')
|
|
24
|
+
|
|
25
|
+
# Load JSON data
|
|
26
|
+
def load_data(filename):
|
|
27
|
+
filepath = os.path.join(data_directory, filename)
|
|
28
|
+
if os.path.isfile(filepath):
|
|
29
|
+
with open(filepath, 'r', encoding='utf-8') as f:
|
|
30
|
+
return json.load(f)
|
|
31
|
+
else:
|
|
32
|
+
download_data(filename, filepath)
|
|
33
|
+
load_data(filename)
|
|
34
|
+
|
|
35
|
+
# Update JSON data
|
|
36
|
+
def update_data():
|
|
37
|
+
for filename in ('metadata-languages.min.json', 'metadata-scriptures.min.json',):
|
|
38
|
+
download_data(filename, os.path.join(data_directory, filename))
|
|
39
|
+
|
|
40
|
+
languages = load_data('metadata-languages.min.json')
|
|
41
|
+
scriptures = load_data('metadata-scriptures.min.json')
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# Get the BCP 47 language tag for a given language code
|
|
46
|
+
def get_bcp47(lang):
|
|
47
|
+
if lang and 'Hant' in lang:
|
|
48
|
+
lang = 'cmn-Hant'
|
|
49
|
+
elif lang and 'Hans' in lang:
|
|
50
|
+
lang = 'cmn-Hans'
|
|
51
|
+
|
|
52
|
+
bcp47 = languages.get('mapToBcp47', {}).get(lang)
|
|
53
|
+
if not bcp47:
|
|
54
|
+
bcp47 = 'en'
|
|
55
|
+
if lang:
|
|
56
|
+
sys.stdout.write(f'Warning: Couldn’t find BCP 47 language tag for “{lang}” – falling back to “en” (English).\n')
|
|
57
|
+
|
|
58
|
+
return bcp47
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Python standard libraries
|
|
2
|
+
import sys
|
|
3
|
+
import re
|
|
4
|
+
import unicodedata
|
|
5
|
+
|
|
6
|
+
# Internal imports
|
|
7
|
+
from . import data, numbers
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Reference:
|
|
11
|
+
def __init__(self, lang = 'en', publication_slug = None, book_slug = None, chapter = None, verse_groups = [], context_verse_groups = []):
|
|
12
|
+
self.lang = lang
|
|
13
|
+
self.publication_slug = publication_slug
|
|
14
|
+
self.book_slug = book_slug
|
|
15
|
+
self.chapter = chapter
|
|
16
|
+
self.verse_groups = verse_groups
|
|
17
|
+
self.context_verse_groups = context_verse_groups
|
|
18
|
+
|
|
19
|
+
# Get a localized label (e.g. Old Testament, Genesis 1, Helaman 5:12, etc.)
|
|
20
|
+
def label(self, skip_book_name = False, abbreviated = False):
|
|
21
|
+
translated_names = data.scriptures['languages'][self.lang]['translatedNames']
|
|
22
|
+
english_translated_names = data.scriptures['languages']['en']['translatedNames']
|
|
23
|
+
|
|
24
|
+
if self.publication_slug and not self.book_slug:
|
|
25
|
+
publication_name = translated_names.get(self.publication_slug, {}).get('name') or english_translated_names.get(self.publication_slug, {}).get('name')
|
|
26
|
+
if abbreviated:
|
|
27
|
+
publication_name = translated_names.get(self.publication_slug, {}).get('abbrev') or publication_name
|
|
28
|
+
return publication_name
|
|
29
|
+
|
|
30
|
+
label = ''
|
|
31
|
+
book_slug = self.book_slug
|
|
32
|
+
if not skip_book_name:
|
|
33
|
+
if book_slug == 'psalms':
|
|
34
|
+
book_slug = 'psalm'
|
|
35
|
+
elif book_slug == 'sections':
|
|
36
|
+
book_slug = 'doctrine-and-covenants'
|
|
37
|
+
elif book_slug == 'official-declarations':
|
|
38
|
+
book_slug = 'official-declaration'
|
|
39
|
+
elif book_slug == 'facsimiles':
|
|
40
|
+
book_slug = 'facsimile'
|
|
41
|
+
elif book_slug == 'jst-psalms':
|
|
42
|
+
book_slug = 'jst-psalm'
|
|
43
|
+
|
|
44
|
+
book_name = translated_names.get(book_slug, {}).get('name') or english_translated_names.get(book_slug, {}).get('name') or ''
|
|
45
|
+
if abbreviated:
|
|
46
|
+
book_name = translated_names.get(book_slug, {}).get('abbrev') or book_name
|
|
47
|
+
label += book_name
|
|
48
|
+
|
|
49
|
+
if self.chapter:
|
|
50
|
+
punctuation = data.scriptures['languages'][self.lang]['punctuation']
|
|
51
|
+
numerals = data.scriptures['languages'][self.lang]['numerals']
|
|
52
|
+
if not skip_book_name:
|
|
53
|
+
label += punctuation['bookChapterSeparator']
|
|
54
|
+
|
|
55
|
+
# Get localized chapter name
|
|
56
|
+
chapter_name = (
|
|
57
|
+
translated_names.get(self.chapter, {}).get('name') or
|
|
58
|
+
english_translated_names.get(self.chapter, {}).get('name') or
|
|
59
|
+
numbers.get_formatted_number(self.chapter, target_lang = self.lang, target_custom_numerals = numerals) or
|
|
60
|
+
punctuation['verseRangeSeparator'].join([numbers.get_formatted_number(ch, target_lang = self.lang, target_custom_numerals = numerals) for ch in re.split(verse_range_separators_pattern, self.chapter)]) or
|
|
61
|
+
self.chapter
|
|
62
|
+
)
|
|
63
|
+
if abbreviated:
|
|
64
|
+
chapter_name = translated_names.get(self.chapter, {}).get('abbrev') or chapter_name
|
|
65
|
+
label += chapter_name
|
|
66
|
+
|
|
67
|
+
# Get localized verses
|
|
68
|
+
if self.verse_groups:
|
|
69
|
+
label += punctuation['chapterVerseSeparator']
|
|
70
|
+
label += convert_verse_groups_to_string(self.verse_groups, punctuation['verseRangeSeparator'], punctuation['verseGroupSeparator'], numerals, lang = self.lang)
|
|
71
|
+
if self.context_verse_groups:
|
|
72
|
+
label += punctuation['openingParenthesis'] + convert_verse_groups_to_string(self.context_verse_groups, punctuation['verseRangeSeparator'], punctuation['verseGroupSeparator'], numerals, lang = self.lang) + punctuation['closingParenthesis']
|
|
73
|
+
|
|
74
|
+
return label
|
|
75
|
+
|
|
76
|
+
# Get the Church URI (e.g. /scriptures/ot, /scriptures/bofm/1-ne/3.7)
|
|
77
|
+
def church_uri(self, use_query_parameters = False):
|
|
78
|
+
if self.publication_slug and not self.book_slug:
|
|
79
|
+
uri = data.scriptures['structure'].get(self.publication_slug, {}).get('churchUri')
|
|
80
|
+
return uri
|
|
81
|
+
|
|
82
|
+
uri = ''
|
|
83
|
+
for publication_data in data.scriptures['structure'].values():
|
|
84
|
+
for slug, book_data in publication_data['books'].items():
|
|
85
|
+
if slug == self.book_slug:
|
|
86
|
+
uri += book_data['churchUri']
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
if uri and self.chapter:
|
|
90
|
+
chapter = str(self.chapter)
|
|
91
|
+
if re.match(rf'\d+{verse_range_separators_pattern}\d+', chapter):
|
|
92
|
+
# Chapter range – only use the first chapter
|
|
93
|
+
chapter = re.split(verse_range_separators_pattern, chapter)[0]
|
|
94
|
+
uri += '/' + str(chapter)
|
|
95
|
+
if self.verse_groups:
|
|
96
|
+
if use_query_parameters:
|
|
97
|
+
uri += '?id=' + convert_verse_groups_to_string(self.verse_groups, '-', ',', verse_number_prefix = 'p')
|
|
98
|
+
if self.context_verse_groups:
|
|
99
|
+
uri += '&context=' + convert_verse_groups_to_string(self.context_verse_groups, '-', ',', verse_number_prefix = 'p')
|
|
100
|
+
else:
|
|
101
|
+
uri += '.' + convert_verse_groups_to_string(self.verse_groups, '-', ',')
|
|
102
|
+
if self.context_verse_groups:
|
|
103
|
+
uri += '(' + convert_verse_groups_to_string(self.context_verse_groups, '-', ',') + ')'
|
|
104
|
+
|
|
105
|
+
return uri
|
|
106
|
+
|
|
107
|
+
# Get the Church website URL (e.g. https://www.churchofjesuschrist.org/study/scriptures/bofm/1-ne/3?id=p7&lang=eng#p7)
|
|
108
|
+
# TODO: Add validation based on online availability in the given language
|
|
109
|
+
def church_url(self, skip_lang = False, skip_fragment = False):
|
|
110
|
+
url = 'https://www.churchofjesuschrist.org/study'
|
|
111
|
+
url += self.church_uri(use_query_parameters = True)
|
|
112
|
+
if not skip_lang:
|
|
113
|
+
church_lang = data.languages['languages'][self.lang]['churchLang']
|
|
114
|
+
url += f'&lang={church_lang}' if '?' in url else f'?lang={church_lang}'
|
|
115
|
+
if self.verse_groups and not skip_fragment:
|
|
116
|
+
url += '#p' + str(self.verse_groups[0][0])
|
|
117
|
+
return url
|
|
118
|
+
|
|
119
|
+
# Get an HTML link to the Church website
|
|
120
|
+
def church_link(self, link_class = None, link_target = None, skip_book_name = False, abbreviated = False, skip_lang = False, skip_fragment = False):
|
|
121
|
+
additional_attributes = ''
|
|
122
|
+
if link_class:
|
|
123
|
+
additional_attributes += f' class="{link_class}"'
|
|
124
|
+
if link_target:
|
|
125
|
+
additional_attributes += f' target="{link_target}"'
|
|
126
|
+
label = self.label(skip_book_name = skip_book_name, abbreviated = abbreviated)
|
|
127
|
+
url = self.church_url(skip_lang = skip_lang, skip_fragment = skip_fragment)
|
|
128
|
+
return f'<a href="{url}"{additional_attributes}>{label}</a>'
|
|
129
|
+
|
|
130
|
+
def __str__(self):
|
|
131
|
+
return label(self)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
reference_separators_pattern = r'|'.join([re.escape(s.strip()) for s in data.scriptures['summary']['punctuation']['referenceSeparator']] + [re.escape(';'), re.escape('\n')])
|
|
135
|
+
chapter_verse_separators_pattern = r'|'.join([re.escape(s.strip()) for s in data.scriptures['summary']['punctuation']['chapterVerseSeparator']] + [re.escape(':')])
|
|
136
|
+
verse_group_separators_pattern = r'|'.join([re.escape(s.strip()) for s in data.scriptures['summary']['punctuation']['verseGroupSeparator']] + [re.escape(',')])
|
|
137
|
+
verse_range_separators_pattern = r'|'.join([re.escape(s.strip()) for s in data.scriptures['summary']['punctuation']['verseRangeSeparator']] + [re.escape('-'), re.escape('–'), re.escape('〜')])
|
|
138
|
+
opening_parenthesis_pattern = r'|'.join([re.escape(s.strip()) for s in data.scriptures['summary']['punctuation']['openingParenthesis']] + [re.escape('(')])
|
|
139
|
+
closing_parenthesis_pattern = r'|'.join([re.escape(s.strip()) for s in data.scriptures['summary']['punctuation']['closingParenthesis']] + [re.escape(')')])
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Normalize text by removing anything that's not a letter or number, and converting to lowercase. This allows for a fuzzy comparison between input text and a known list of values.
|
|
143
|
+
def normalizeForCompare(text):
|
|
144
|
+
decomposed_text = unicodedata.normalize('NFKD', text)
|
|
145
|
+
normalized_text = ''.join([c for c in decomposed_text if unicodedata.category(c)[0] in ['L', 'N']]).lower()
|
|
146
|
+
return normalized_text
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# Parse verses into verse groups
|
|
150
|
+
# Example: '1-2,5-7' –> [[1, 2], [5, 6, 7]]
|
|
151
|
+
def parse_verses_string(verses_string):
|
|
152
|
+
verses_string = (verses_string or '').replace('p', '').strip()
|
|
153
|
+
if not verses_string:
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
unique_verses = set()
|
|
157
|
+
for verse_group_string in re.split(verse_group_separators_pattern, verses_string):
|
|
158
|
+
verse_strings = re.split(verse_range_separators_pattern, verse_group_string)
|
|
159
|
+
lower_int = numbers.convert_number_to_int(verse_strings[0])
|
|
160
|
+
upper_int = numbers.convert_number_to_int(verse_strings[-1])
|
|
161
|
+
unique_verses.update(range(lower_int, upper_int + 1))
|
|
162
|
+
|
|
163
|
+
verse_groups = []
|
|
164
|
+
verses = sorted(unique_verses)
|
|
165
|
+
previous_verse = -1
|
|
166
|
+
for verse in verses:
|
|
167
|
+
if verse == previous_verse + 1:
|
|
168
|
+
verse_groups[-1].append(verse)
|
|
169
|
+
else:
|
|
170
|
+
verse_groups.append([verse])
|
|
171
|
+
previous_verse = verse
|
|
172
|
+
|
|
173
|
+
return verse_groups
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# Format verse groups to a localized string
|
|
177
|
+
# Example: [[1, 2], [5, 6, 7]] –> '1-2,5-7'
|
|
178
|
+
def convert_verse_groups_to_string(verse_groups, verse_range_separator, verse_group_separator, numerals = [], lang = 'en', verse_number_prefix = ''):
|
|
179
|
+
verse_ranges = []
|
|
180
|
+
for verse_group in verse_groups:
|
|
181
|
+
if verse_group[0] == verse_group[-1]:
|
|
182
|
+
verse_range = verse_number_prefix + numbers.get_formatted_number(verse_group[0], target_lang = lang, target_custom_numerals = numerals)
|
|
183
|
+
else:
|
|
184
|
+
verse_range = verse_number_prefix + numbers.get_formatted_number(verse_group[0], target_lang = lang, target_custom_numerals = numerals) + verse_range_separator + verse_number_prefix + numbers.get_formatted_number(verse_group[-1], target_lang = lang, target_custom_numerals = numerals)
|
|
185
|
+
verse_ranges.append(verse_range)
|
|
186
|
+
return verse_group_separator.join(verse_ranges)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# Parse one or more scripture references, URIs, URLs, or slugs
|
|
190
|
+
def parse_references_string(input_string, lang = 'en'):
|
|
191
|
+
lang = data.get_bcp47(lang)
|
|
192
|
+
|
|
193
|
+
input_list = re.split(reference_separators_pattern, input_string)
|
|
194
|
+
references = []
|
|
195
|
+
|
|
196
|
+
previous_book_slug = None
|
|
197
|
+
for input_string in input_list:
|
|
198
|
+
input_string = input_string.strip()
|
|
199
|
+
|
|
200
|
+
verses_string = None
|
|
201
|
+
context_verses_string = None
|
|
202
|
+
chapter_string = None
|
|
203
|
+
book_string = None
|
|
204
|
+
|
|
205
|
+
if '/scriptures/' in input_string:
|
|
206
|
+
# Church URI or URL
|
|
207
|
+
# Example: /scriptures/ot
|
|
208
|
+
# Example: /scriptures/ot/gen
|
|
209
|
+
# Example: /scriptures/ot/gen/1
|
|
210
|
+
# Example: /scriptures/ot/gen/1.1-3
|
|
211
|
+
# Example: gospellibrary://content/scriptures/ot/gen/3.1-3
|
|
212
|
+
# Example: http://lds.org/scriptures/ot/gen/3.1-3?lang=eng
|
|
213
|
+
# Example: https://www.churchofjesuschrist.org/study/scriptures/ot/gen/3?id=p1-p3&lang=eng#p1
|
|
214
|
+
|
|
215
|
+
unparsed = '/scriptures/' + input_string.split('/scriptures/')[1]
|
|
216
|
+
query_string = None
|
|
217
|
+
if '?' in unparsed:
|
|
218
|
+
unparsed, query_string = unparsed.split('?')
|
|
219
|
+
|
|
220
|
+
# Get verses string
|
|
221
|
+
if '.' in unparsed:
|
|
222
|
+
unparsed, verses_string = unparsed.split('.')
|
|
223
|
+
if '(' in verses_string:
|
|
224
|
+
verses_string, context_verses_string = verses_string.replace(')', '').split('(')
|
|
225
|
+
elif query_string:
|
|
226
|
+
if 'id=' in query_string:
|
|
227
|
+
verses_string = query_string.split('id=')[1].split('&')[0]
|
|
228
|
+
if 'context=' in query_string:
|
|
229
|
+
context_verses_string = query_string.split('context=')[1].split('&')[0]
|
|
230
|
+
elif '#' in query_string:
|
|
231
|
+
verses_string = query_string.split('#')[1]
|
|
232
|
+
|
|
233
|
+
# Get chapter string and book string
|
|
234
|
+
book_string = unparsed
|
|
235
|
+
if book_string.count('/') > 3:
|
|
236
|
+
book_string, chapter_string = book_string.rsplit('/', 1)
|
|
237
|
+
|
|
238
|
+
else:
|
|
239
|
+
# Scripture reference or slug
|
|
240
|
+
# Examples: Old Testament; 1 Nephi; Matthew 1; Helaman 5:12; words-of-mormon
|
|
241
|
+
|
|
242
|
+
# Get verses string
|
|
243
|
+
unparsed, verses_string = (re.split(chapter_verse_separators_pattern, input_string) + [''])[:2]
|
|
244
|
+
verses_string, context_verses_string = (re.split(opening_parenthesis_pattern, re.sub(closing_parenthesis_pattern, '', verses_string)) + [''])[:2]
|
|
245
|
+
|
|
246
|
+
# Get chapter string and book string
|
|
247
|
+
book_string = unparsed
|
|
248
|
+
chapter_match = re.match(rf'^(?:.*[^\d{verse_range_separators_pattern}])?([\d{verse_range_separators_pattern}]*)$', book_string)
|
|
249
|
+
if chapter_match:
|
|
250
|
+
chapter_string = chapter_match.group(1)
|
|
251
|
+
book_string = book_string.removesuffix(chapter_string).strip()
|
|
252
|
+
|
|
253
|
+
verse_groups = parse_verses_string(verses_string)
|
|
254
|
+
context_verse_groups = parse_verses_string(context_verses_string)
|
|
255
|
+
chapter = numbers.convert_number_to_int(chapter_string)
|
|
256
|
+
book_slug = None
|
|
257
|
+
skip_book_name = False
|
|
258
|
+
if book_string:
|
|
259
|
+
book_slug = data.scriptures['mapToSlug'].get(book_string, None)
|
|
260
|
+
if not book_slug:
|
|
261
|
+
for key, value in data.scriptures['mapToSlug'].items():
|
|
262
|
+
if normalizeForCompare(book_string) == normalizeForCompare(key):
|
|
263
|
+
book_slug = value
|
|
264
|
+
break
|
|
265
|
+
if book_slug == previous_book_slug:
|
|
266
|
+
skip_book_name = True
|
|
267
|
+
previous_book_slug = book_slug
|
|
268
|
+
else:
|
|
269
|
+
book_slug = previous_book_slug
|
|
270
|
+
skip_book_name = True
|
|
271
|
+
|
|
272
|
+
publication_slug = None
|
|
273
|
+
if book_slug in data.scriptures['structure'].keys():
|
|
274
|
+
publication_slug = book_slug
|
|
275
|
+
book_slug = None
|
|
276
|
+
|
|
277
|
+
reference = Reference(lang = lang, publication_slug = publication_slug, book_slug = book_slug, chapter = chapter, verse_groups = verse_groups, context_verse_groups = context_verse_groups)
|
|
278
|
+
references.append(reference)
|
|
279
|
+
|
|
280
|
+
return references
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def get_label(input_string, lang = 'en', separator = '\n', skip_book_name = False, abbreviated = False, **kwargs):
|
|
285
|
+
references = parse_references_string(input_string, lang = lang)
|
|
286
|
+
return separator.join([ref.label(skip_book_name = skip_book_name, abbreviated = abbreviated) for ref in references])
|
|
287
|
+
|
|
288
|
+
def get_church_uri(input_string, separator = '\n', use_query_parameters = False, **kwargs):
|
|
289
|
+
references = parse_references_string(input_string)
|
|
290
|
+
return separator.join([ref.church_uri(use_query_parameters = use_query_parameters) for ref in references])
|
|
291
|
+
|
|
292
|
+
def get_church_url(input_string, lang = 'en', separator = '\n', skip_lang = False, skip_fragment = False, **kwargs):
|
|
293
|
+
references = parse_references_string(input_string, lang = lang)
|
|
294
|
+
return separator.join([ref.church_url(skip_lang = skip_lang, skip_fragment = skip_fragment) for ref in references])
|
|
295
|
+
|
|
296
|
+
def get_church_link(input_string, lang = 'en', separator = '\n', link_class = None, link_target = None, skip_book_name = False, abbreviated = False, skip_lang = False, skip_fragment = False, **kwargs):
|
|
297
|
+
references = parse_references_string(input_string, lang = lang)
|
|
298
|
+
return separator.join([ref.church_link(link_class = link_class, link_target = link_target, skip_book_name = skip_book_name, abbreviated = abbreviated, skip_lang = skip_lang, skip_fragment = skip_fragment) for ref in references])
|
|
299
|
+
|
|
300
|
+
def get_langs(**kwargs):
|
|
301
|
+
return data.scriptures['languages'].keys()
|
|
302
|
+
|
|
303
|
+
def get_punctuation(lang = 'en', **kwargs):
|
|
304
|
+
return data.scriptures['languages'][lang]['punctuation']
|
|
305
|
+
|
|
306
|
+
def get_numerals(lang = 'en', **kwargs):
|
|
307
|
+
return data.scriptures['languages'][lang]['numerals']
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Python standard libraries
|
|
2
|
+
import sys
|
|
3
|
+
import importlib
|
|
4
|
+
|
|
5
|
+
# Third-party libraries
|
|
6
|
+
GeezGeezify = importlib.import_module('geezify-python-main.geezify', package='...').Geezify
|
|
7
|
+
GeezArabify = importlib.import_module('geezify-python-main.arabify', package='...').Arabify
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Format a positive whole number to a specified language or numeral system
|
|
11
|
+
# Either a target_lang, target_format, or target_custom_numerals must be provided
|
|
12
|
+
# Supported target formats:
|
|
13
|
+
# --- STANDARD DECIMAL ---
|
|
14
|
+
# decimal-int (ex: 14)
|
|
15
|
+
# decimal-string (ex: '14') – default for most languages
|
|
16
|
+
# --- SIMPLE CONVERSION ---
|
|
17
|
+
# arabic-eastern (ex: ١٤) – default if target_lang is 'ar'
|
|
18
|
+
# arabic-extended (ex: ۱۴) – default if target_lang is 'fa' OR 'ur'
|
|
19
|
+
# devangari (ex: १४) – default if target_lang is 'ne'
|
|
20
|
+
# khmer (ex: ១៤) – default if target_lang is 'km'
|
|
21
|
+
# myanmar (ex: ၁၄) – default if target_lang is 'my'
|
|
22
|
+
# thai (ex: ๑๔) – default if target_lang is 'th'
|
|
23
|
+
# custom – requires numerals 0–9 to be passed in as a list
|
|
24
|
+
# --- COMPLEX CONVERSION ---
|
|
25
|
+
# chinese-simplified (ex: 十四)
|
|
26
|
+
# chinese-traditional (ex: 十四)
|
|
27
|
+
# decimal-fullwidth (ex: '14') – uses full-width numeral only if the number is a single digit
|
|
28
|
+
# geez (ex: ፲፬) – default if target_lang is 'am'
|
|
29
|
+
# roman-upper (ex: 'XIV')
|
|
30
|
+
# roman-lower (ex: 'xiv')
|
|
31
|
+
# alphabet-upper (ex: 'N')
|
|
32
|
+
# alphabet-lower (ex: 'n')
|
|
33
|
+
def get_formatted_number(number, target_lang = None, target_format = None, target_custom_numerals = []):
|
|
34
|
+
formatted_number = ''
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
int_number = abs(convert_number_to_int(number))
|
|
38
|
+
except:
|
|
39
|
+
# Number can't be formatted (ex: 56-57 OR fac-1)
|
|
40
|
+
return formatted_number
|
|
41
|
+
|
|
42
|
+
if target_custom_numerals:
|
|
43
|
+
target_format = 'custom'
|
|
44
|
+
if not target_format:
|
|
45
|
+
if target_lang in ('ar',):
|
|
46
|
+
target_format = 'arabic-eastern'
|
|
47
|
+
elif target_lang in ('fa', 'ur',):
|
|
48
|
+
target_format = 'arabic-extended'
|
|
49
|
+
elif target_lang in ('ne',):
|
|
50
|
+
target_format = 'devangari'
|
|
51
|
+
elif target_lang in ('km',):
|
|
52
|
+
target_format = 'khmer'
|
|
53
|
+
elif target_lang in ('my',):
|
|
54
|
+
target_format = 'myanmar'
|
|
55
|
+
elif target_lang in ('th',):
|
|
56
|
+
target_format = 'thai'
|
|
57
|
+
elif target_lang in ('am',):
|
|
58
|
+
target_format = 'geez'
|
|
59
|
+
else:
|
|
60
|
+
target_format = 'decimal-string'
|
|
61
|
+
|
|
62
|
+
# Numerals 0–9: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
|
|
63
|
+
simple_numeral_mapping = {
|
|
64
|
+
'arabic-eastern': ['٠', '١', '٢', '٣', '٤', '٥', '٦', '٧', '٨', '٩'],
|
|
65
|
+
'arabic-extended': ['۰', '۱', '۲', '۳', '۴', '۵', '۶', '۷', '۸', '۹'],
|
|
66
|
+
'devangari': ['०', '१', '२', '३', '४', '५', '६', '७', '८', '९'],
|
|
67
|
+
'khmer': ['០', '១', '២', '៣', '៤', '៥', '៦', '៧', '៨', '៩'],
|
|
68
|
+
'myanmar': ['၀', '၁', '၂', '၃', '၄', '၅', '၆', '၇', '၈', '၉'],
|
|
69
|
+
'thai': ['๐', '๑', '๒', '๓', '๔', '๕', '๖', '๗', '๘', '๙'],
|
|
70
|
+
'custom': target_custom_numerals,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if int_number is not None:
|
|
74
|
+
# Standard decimal
|
|
75
|
+
if target_format == 'decimal-int':
|
|
76
|
+
formatted_number = int_number
|
|
77
|
+
elif target_format == 'decimal-string':
|
|
78
|
+
formatted_number = str(int_number)
|
|
79
|
+
|
|
80
|
+
# Simple conversion
|
|
81
|
+
elif target_format in ('arabic-eastern', 'arabic-extended', 'devangari', 'khmer', 'myanmar', 'thai', 'custom'):
|
|
82
|
+
formatted_number = ''
|
|
83
|
+
for digit in str(int_number):
|
|
84
|
+
formatted_number += simple_numeral_mapping[target_format][int(digit)]
|
|
85
|
+
|
|
86
|
+
# Complex conversion
|
|
87
|
+
elif target_format == 'chinese-simplified':
|
|
88
|
+
formatted_number = format_number_chinese(int_number, script = 'Hans')
|
|
89
|
+
elif target_format == 'chinese-traditional':
|
|
90
|
+
formatted_number = format_number_chinese(int_number, script = 'Hant')
|
|
91
|
+
elif target_format == 'decimal-fullwidth':
|
|
92
|
+
formatted_number = format_number_fullwidth(int_number, convert_one_digit = True, convert_two_digits = False, convert_more_than_two_digits = False)
|
|
93
|
+
elif target_format == 'geez':
|
|
94
|
+
formatted_number = format_number_geez(int_number)
|
|
95
|
+
elif target_format == 'roman-upper':
|
|
96
|
+
formatted_number = format_number_roman(int_number, uppercase = True)
|
|
97
|
+
elif target_format == 'roman-lower':
|
|
98
|
+
formatted_number = format_number_roman(int_number, uppercase = False)
|
|
99
|
+
elif target_format == 'alphabet-upper':
|
|
100
|
+
formatted_number = format_number_alphabet(int_number, uppercase = True)
|
|
101
|
+
elif target_format == 'alphabet-lower':
|
|
102
|
+
formatted_number = format_number_alphabet(int_number, uppercase = False)
|
|
103
|
+
|
|
104
|
+
return formatted_number
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# Convert a formatted number to an integer
|
|
108
|
+
def convert_number_to_int(number):
|
|
109
|
+
int_number = None
|
|
110
|
+
|
|
111
|
+
if isinstance(number, int) or isinstance(number, float):
|
|
112
|
+
int_number = int(number)
|
|
113
|
+
|
|
114
|
+
elif isinstance(number, str) and len(number) > 0:
|
|
115
|
+
number = number.strip()
|
|
116
|
+
try:
|
|
117
|
+
int_number = int(number)
|
|
118
|
+
except:
|
|
119
|
+
if all(char in ['〇', '零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '百', '千', '万', '萬', '亿', '億'] for char in number):
|
|
120
|
+
int_number = chinese_numerals_to_int(number)
|
|
121
|
+
elif all(char in ['፲', '፳', '፴', '፵', '፶', '፷', '፸', '፹', '፺', '፻'] for char in number):
|
|
122
|
+
int_number = geez_numerals_to_int(number)
|
|
123
|
+
elif all(char in ['I', 'V', 'X', 'L', 'C', 'D', 'M', 'i', 'v', 'x', 'l', 'c', 'd', 'm'] for char in number):
|
|
124
|
+
int_number = roman_numerals_to_int(number)
|
|
125
|
+
elif all(char in ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'] for char in number):
|
|
126
|
+
int_number = alphabet_numerals_to_int(number)
|
|
127
|
+
else:
|
|
128
|
+
int_number = number
|
|
129
|
+
|
|
130
|
+
return int_number or None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# Convert a formatted number (Chinese numerals) to an integer
|
|
134
|
+
def chinese_numerals_to_int(number):
|
|
135
|
+
# TODO: Not yet implemented
|
|
136
|
+
sys.stdout.write('Warning: Conversion from Chinese numerals to an integer is not yet implemented.\n')
|
|
137
|
+
pass
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
# Convert a formatted number (Geʽez numerals) to an integer
|
|
141
|
+
def geez_numerals_to_int(number):
|
|
142
|
+
return GeezArabify.arabify(number)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# Convert a formatted number (Roman numerals) to an integer
|
|
146
|
+
# Algorithm adapted from:
|
|
147
|
+
# https://stackoverflow.com/a/48557664
|
|
148
|
+
# License: CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/)
|
|
149
|
+
def roman_numerals_to_int(number):
|
|
150
|
+
number = number.upper()
|
|
151
|
+
int_number = 0
|
|
152
|
+
|
|
153
|
+
roman_numeral_values = { 'I': 1, 'V': 5, 'X': 10, 'L': 50, 'C': 100, 'D': 500, 'M': 1000 }
|
|
154
|
+
for i, c in enumerate(number):
|
|
155
|
+
if (i + 1) == len(number) or roman_numeral_values[c] >= roman_numeral_values[number[i + 1]]:
|
|
156
|
+
int_number += roman_numeral_values[c]
|
|
157
|
+
else:
|
|
158
|
+
int_number -= roman_numeral_values[c]
|
|
159
|
+
|
|
160
|
+
return int_number
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# Convert a formatted number (alphabet numerals) to an integer
|
|
164
|
+
def alphabet_numerals_to_int(number):
|
|
165
|
+
number = number.upper()
|
|
166
|
+
|
|
167
|
+
# Algorithm adapted from:
|
|
168
|
+
# https://stackoverflow.com/a/63013258
|
|
169
|
+
# License: CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/)
|
|
170
|
+
def recursive_letters_to_number(s):
|
|
171
|
+
n = ord(s[-1]) - 64
|
|
172
|
+
if s[:-1]:
|
|
173
|
+
return 26 * (recursive_letters_to_number(s[:-1])) + n
|
|
174
|
+
else:
|
|
175
|
+
return n
|
|
176
|
+
|
|
177
|
+
return recursive_letters_to_number(number)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# Convert an integer to Chinese numerals
|
|
181
|
+
def format_number_chinese(int_number, script = 'Hans'):
|
|
182
|
+
int_number = int(int_number)
|
|
183
|
+
# TODO: Not yet implemented
|
|
184
|
+
sys.stdout.write('Warning: Conversion from an integer to Chinese numerals is not yet implemented.\n')
|
|
185
|
+
return str(int_number)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Convert an integer to fullwidth numerals
|
|
189
|
+
def format_number_fullwidth(int_number, convert_one_digit = True, convert_two_digits = False, convert_more_than_two_digits = False):
|
|
190
|
+
num_digits = len(str(int_number))
|
|
191
|
+
fullwidth_numerals = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
|
|
192
|
+
|
|
193
|
+
if (num_digits == 1 and convert_one_digit) or (num_digits == 2 and convert_two_digits) or (num_digits > 2 and convert_more_than_two_digits):
|
|
194
|
+
formatted_number = ''
|
|
195
|
+
for digit in str(int_number):
|
|
196
|
+
formatted_number += fullwidth_numerals[int(digit)]
|
|
197
|
+
else:
|
|
198
|
+
formatted_number = str(int_number)
|
|
199
|
+
|
|
200
|
+
return formatted_number
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# Convert an integer to Geʽez numerals
|
|
204
|
+
def format_number_geez(int_number):
|
|
205
|
+
int_number = int(int_number)
|
|
206
|
+
return GeezGeezify.geezify(int_number)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
# Convert an integer to Roman numerals
|
|
210
|
+
# Corresponds to /r or /R number style in PDF specification (/PageLabels)
|
|
211
|
+
# Algorithm adapted from:
|
|
212
|
+
# https://stackoverflow.com/a/35728954
|
|
213
|
+
# License: CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/)
|
|
214
|
+
def format_number_roman(int_number, uppercase = True):
|
|
215
|
+
int_number = int(int_number)
|
|
216
|
+
formatted_number = ''
|
|
217
|
+
|
|
218
|
+
if int_number < 1:
|
|
219
|
+
return formatted_number
|
|
220
|
+
|
|
221
|
+
while int_number >= 1000:
|
|
222
|
+
formatted_number += 'm'
|
|
223
|
+
int_number -= 1000
|
|
224
|
+
|
|
225
|
+
diffs = [900, 500, 400, 300, 200, 100, 90, 50, 40, 30, 20, 10, 9, 5, 4, 3, 2, 1]
|
|
226
|
+
digits = ['cm', 'd', 'cd', 'ccc', 'cc', 'c', 'xc', 'l', 'xl', 'xxx', 'xx', 'x', 'ix', 'v', 'iv', 'iii', 'ii', 'i']
|
|
227
|
+
|
|
228
|
+
for i in range(len(diffs)):
|
|
229
|
+
if int_number >= diffs[i]:
|
|
230
|
+
formatted_number += digits[i]
|
|
231
|
+
int_number -= diffs[i]
|
|
232
|
+
|
|
233
|
+
if uppercase:
|
|
234
|
+
formatted_number = formatted_number.upper()
|
|
235
|
+
|
|
236
|
+
return formatted_number
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# Convert an integer to alphabet numerals (A, B, … Z, AA, AB, … AZ, etc.)
|
|
240
|
+
# Corresponds to /a or /A number style in PDF specification (/PageLabels)
|
|
241
|
+
# Also corresponds to how columns are numbered in a spreadsheet
|
|
242
|
+
def format_number_alphabet(int_number, uppercase = True):
|
|
243
|
+
int_number = int(int_number)
|
|
244
|
+
formatted_number = ''
|
|
245
|
+
|
|
246
|
+
if int_number < 1:
|
|
247
|
+
return formatted_number
|
|
248
|
+
|
|
249
|
+
# Algorithm adapted from:
|
|
250
|
+
# https://stackoverflow.com/a/63013258
|
|
251
|
+
# License: CC BY-SA 4.0 (https://creativecommons.org/licenses/by-sa/4.0/)
|
|
252
|
+
def recursive_number_to_letters(n):
|
|
253
|
+
letters = list('abcdefghijklmnopqrstuvwxyz')
|
|
254
|
+
n, remainder = divmod(n - 1, 26)
|
|
255
|
+
char = letters[remainder]
|
|
256
|
+
if n:
|
|
257
|
+
return recursive_number_to_letters(n) + char
|
|
258
|
+
else:
|
|
259
|
+
return char
|
|
260
|
+
|
|
261
|
+
formatted_number = recursive_number_to_letters(int_number)
|
|
262
|
+
|
|
263
|
+
if uppercase:
|
|
264
|
+
formatted_number = formatted_number.upper()
|
|
265
|
+
|
|
266
|
+
return formatted_number
|
|
267
|
+
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Samuel Bradshaw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|