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.
@@ -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.