bdkit 0.1.0__tar.gz

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.
bdkit-0.1.0/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ .DS_Store
bdkit-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sarif Mia
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.
bdkit-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,111 @@
1
+ Metadata-Version: 2.5
2
+ Name: bdkit
3
+ Version: 0.1.0
4
+ Summary: Everyday Bangladesh utilities: taka in words (English/Bangla, lakh/crore), mobile number validation with operator detection, Bangla digits.
5
+ Project-URL: Homepage, https://github.com/sarif-mia/bdkit
6
+ Project-URL: Source, https://github.com/sarif-mia/bdkit
7
+ Project-URL: Tracker, https://github.com/sarif-mia/bdkit/issues
8
+ Author: Sarif Mia
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: amount-in-words,bangla,bangladesh,bengali,crore,grameenphone,lakh,phone,taka
12
+ Classifier: Natural Language :: Bengali
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Libraries
16
+ Classifier: Topic :: Text Processing :: Linguistic
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.9
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest>=7; extra == 'test'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # bdkit
24
+
25
+ Everyday utilities for Bangladeshi apps: write taka amounts in words for invoices and cheques (English or Bangla, lakh/crore), validate mobile numbers and detect the operator, and convert between Bangla and English digits.
26
+
27
+ No dependencies. Python 3.9+.
28
+
29
+ ```sh
30
+ pip install bdkit
31
+ ```
32
+
33
+ ## Taka in words
34
+
35
+ ```python
36
+ from bdkit import taka_in_words, number_to_words
37
+
38
+ taka_in_words(125050.50)
39
+ # 'One Lakh Twenty-Five Thousand Fifty Taka and Fifty Paisa Only'
40
+
41
+ taka_in_words("12,500", lang="bn")
42
+ # 'বারো হাজার পাঁচ শত টাকা মাত্র'
43
+
44
+ taka_in_words(1000, only=False)
45
+ # 'One Thousand Taka'
46
+
47
+ number_to_words(52_71_036, lang="bn")
48
+ # 'বায়ান্ন লক্ষ একাত্তর হাজার ছত্রিশ'
49
+ ```
50
+
51
+ Amounts can be numbers, `Decimal`s or strings with commas or `৳`. Paisa are rounded half up to two decimal places.
52
+
53
+ ## Mobile numbers
54
+
55
+ ```python
56
+ from bdkit import parse_mobile, normalize_mobile, mobile_operator, is_valid_mobile
57
+
58
+ normalize_mobile("01712-345678") # '+8801712345678'
59
+ mobile_operator("+880 1912 345678") # 'Banglalink'
60
+ is_valid_mobile("০১৮১২৩৪৫৬৭৮") # True (Bangla digits work)
61
+
62
+ m = parse_mobile("008801512345678")
63
+ m.local, m.e164, m.operator, m.formatted()
64
+ # ('01512345678', '+8801512345678', 'Teletalk', '01512-345678')
65
+ ```
66
+
67
+ Accepted forms are `017…`, `+880 17…`, `+88 017…`, `88017…` and `0088017…`, with or without spaces, dashes or brackets.
68
+
69
+ | Prefix | Operator |
70
+ | --- | --- |
71
+ | 013, 017 | Grameenphone |
72
+ | 014, 019 | Banglalink |
73
+ | 016, 018 | Robi |
74
+ | 015 | Teletalk |
75
+
76
+ ## Digits and lakh formatting
77
+
78
+ ```python
79
+ from bdkit import to_bangla_digits, to_english_digits, format_lakh
80
+
81
+ to_bangla_digits("2026-11-15") # '২০২৬-১১-১৫'
82
+ to_english_digits("৳১,২৫,০০০") # '৳1,25,000'
83
+ format_lakh(12345678) # '1,23,45,678'
84
+ format_lakh(1234567.5, decimals=2) # '12,34,567.50'
85
+ format_lakh(150000, bangla=True) # '১,৫০,০০০'
86
+ ```
87
+
88
+ ## Command line
89
+
90
+ ```console
91
+ $ bdkit words 125050.50
92
+ One Lakh Twenty-Five Thousand Fifty Taka and Fifty Paisa Only
93
+ $ bdkit words 12500 --bn
94
+ বারো হাজার পাঁচ শত টাকা মাত্র
95
+ $ bdkit phone "+880 1712-345678" 01812345678
96
+ +8801712345678 Grameenphone
97
+ +8801812345678 Robi
98
+ $ bdkit lakh 12345678
99
+ 1,23,45,678
100
+ $ bdkit bn 2026
101
+ ২০২৬
102
+ ```
103
+
104
+ ## Related
105
+
106
+ - [bd-income-tax](https://pypi.org/project/bd-income-tax/): Bangladesh income tax calculator
107
+ - [bangladesh-mcp](https://pypi.org/project/bangladesh-mcp/): these tools for AI assistants (MCP)
108
+
109
+ ## License
110
+
111
+ MIT
bdkit-0.1.0/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # bdkit
2
+
3
+ Everyday utilities for Bangladeshi apps: write taka amounts in words for invoices and cheques (English or Bangla, lakh/crore), validate mobile numbers and detect the operator, and convert between Bangla and English digits.
4
+
5
+ No dependencies. Python 3.9+.
6
+
7
+ ```sh
8
+ pip install bdkit
9
+ ```
10
+
11
+ ## Taka in words
12
+
13
+ ```python
14
+ from bdkit import taka_in_words, number_to_words
15
+
16
+ taka_in_words(125050.50)
17
+ # 'One Lakh Twenty-Five Thousand Fifty Taka and Fifty Paisa Only'
18
+
19
+ taka_in_words("12,500", lang="bn")
20
+ # 'বারো হাজার পাঁচ শত টাকা মাত্র'
21
+
22
+ taka_in_words(1000, only=False)
23
+ # 'One Thousand Taka'
24
+
25
+ number_to_words(52_71_036, lang="bn")
26
+ # 'বায়ান্ন লক্ষ একাত্তর হাজার ছত্রিশ'
27
+ ```
28
+
29
+ Amounts can be numbers, `Decimal`s or strings with commas or `৳`. Paisa are rounded half up to two decimal places.
30
+
31
+ ## Mobile numbers
32
+
33
+ ```python
34
+ from bdkit import parse_mobile, normalize_mobile, mobile_operator, is_valid_mobile
35
+
36
+ normalize_mobile("01712-345678") # '+8801712345678'
37
+ mobile_operator("+880 1912 345678") # 'Banglalink'
38
+ is_valid_mobile("০১৮১২৩৪৫৬৭৮") # True (Bangla digits work)
39
+
40
+ m = parse_mobile("008801512345678")
41
+ m.local, m.e164, m.operator, m.formatted()
42
+ # ('01512345678', '+8801512345678', 'Teletalk', '01512-345678')
43
+ ```
44
+
45
+ Accepted forms are `017…`, `+880 17…`, `+88 017…`, `88017…` and `0088017…`, with or without spaces, dashes or brackets.
46
+
47
+ | Prefix | Operator |
48
+ | --- | --- |
49
+ | 013, 017 | Grameenphone |
50
+ | 014, 019 | Banglalink |
51
+ | 016, 018 | Robi |
52
+ | 015 | Teletalk |
53
+
54
+ ## Digits and lakh formatting
55
+
56
+ ```python
57
+ from bdkit import to_bangla_digits, to_english_digits, format_lakh
58
+
59
+ to_bangla_digits("2026-11-15") # '২০২৬-১১-১৫'
60
+ to_english_digits("৳১,২৫,০০০") # '৳1,25,000'
61
+ format_lakh(12345678) # '1,23,45,678'
62
+ format_lakh(1234567.5, decimals=2) # '12,34,567.50'
63
+ format_lakh(150000, bangla=True) # '১,৫০,০০০'
64
+ ```
65
+
66
+ ## Command line
67
+
68
+ ```console
69
+ $ bdkit words 125050.50
70
+ One Lakh Twenty-Five Thousand Fifty Taka and Fifty Paisa Only
71
+ $ bdkit words 12500 --bn
72
+ বারো হাজার পাঁচ শত টাকা মাত্র
73
+ $ bdkit phone "+880 1712-345678" 01812345678
74
+ +8801712345678 Grameenphone
75
+ +8801812345678 Robi
76
+ $ bdkit lakh 12345678
77
+ 1,23,45,678
78
+ $ bdkit bn 2026
79
+ ২০২৬
80
+ ```
81
+
82
+ ## Related
83
+
84
+ - [bd-income-tax](https://pypi.org/project/bd-income-tax/): Bangladesh income tax calculator
85
+ - [bangladesh-mcp](https://pypi.org/project/bangladesh-mcp/): these tools for AI assistants (MCP)
86
+
87
+ ## License
88
+
89
+ MIT
@@ -0,0 +1,27 @@
1
+ """Everyday Bangladesh utilities: taka in words, mobile numbers, Bangla digits.
2
+
3
+ >>> from bdkit import taka_in_words, normalize_mobile
4
+ >>> taka_in_words(1500)
5
+ 'One Thousand Five Hundred Taka Only'
6
+ >>> normalize_mobile("01712-345678")
7
+ '+8801712345678'
8
+ """
9
+ from .digits import format_lakh, to_bangla_digits, to_english_digits
10
+ from .phone import OPERATORS, MobileNumber, is_valid_mobile, mobile_operator, normalize_mobile, parse_mobile
11
+ from .words import number_to_words, taka_in_words
12
+
13
+ __version__ = "0.1.0"
14
+
15
+ __all__ = [
16
+ "OPERATORS",
17
+ "MobileNumber",
18
+ "format_lakh",
19
+ "is_valid_mobile",
20
+ "mobile_operator",
21
+ "normalize_mobile",
22
+ "number_to_words",
23
+ "parse_mobile",
24
+ "taka_in_words",
25
+ "to_bangla_digits",
26
+ "to_english_digits",
27
+ ]
@@ -0,0 +1,76 @@
1
+ """``bdkit`` command line.
2
+
3
+ bdkit words 125050.50 # One Lakh Twenty-Five Thousand Fifty Taka and Fifty Paisa Only
4
+ bdkit words 12500 --bn # বারো হাজার পাঁচ শত টাকা মাত্র
5
+ bdkit phone "+880 1712-345678" # +8801712345678 Grameenphone
6
+ bdkit lakh 12345678 # 1,23,45,678
7
+ bdkit bn 2026 # ২০২৬
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import sys
13
+ from typing import List, Optional
14
+
15
+ from . import __version__
16
+ from .digits import format_lakh, to_bangla_digits, to_english_digits
17
+ from .phone import parse_mobile
18
+ from .words import taka_in_words
19
+
20
+
21
+ def build_parser() -> argparse.ArgumentParser:
22
+ parser = argparse.ArgumentParser(prog="bdkit", description="Everyday Bangladesh utilities.",
23
+ formatter_class=argparse.RawDescriptionHelpFormatter,
24
+ epilog=__doc__.split("\n", 2)[2])
25
+ parser.add_argument("-V", "--version", action="version", version=f"%(prog)s {__version__}")
26
+ sub = parser.add_subparsers(dest="command", required=True)
27
+
28
+ words = sub.add_parser("words", help="taka amount in words")
29
+ words.add_argument("amount")
30
+ words.add_argument("--bn", action="store_true", help="in Bangla")
31
+ words.add_argument("--no-only", action="store_true", help='leave out "Only" / "মাত্র"')
32
+
33
+ phone = sub.add_parser("phone", help="validate a mobile number and show its operator")
34
+ phone.add_argument("number", nargs="+")
35
+
36
+ lakh = sub.add_parser("lakh", help="format a number with lakh/crore commas")
37
+ lakh.add_argument("amount")
38
+ lakh.add_argument("-d", "--decimals", type=int, default=0)
39
+ lakh.add_argument("--bn", action="store_true", help="use Bangla digits")
40
+
41
+ bn = sub.add_parser("bn", help="convert digits to Bangla")
42
+ bn.add_argument("text", nargs="+")
43
+ en = sub.add_parser("en", help="convert Bangla digits to English")
44
+ en.add_argument("text", nargs="+")
45
+ return parser
46
+
47
+
48
+ def main(argv: Optional[List[str]] = None) -> int:
49
+ args = build_parser().parse_args(argv)
50
+ try:
51
+ if args.command == "words":
52
+ print(taka_in_words(args.amount, lang="bn" if args.bn else "en", only=not args.no_only))
53
+ elif args.command == "phone":
54
+ status = 0
55
+ for raw in args.number:
56
+ parsed = parse_mobile(raw)
57
+ if parsed:
58
+ print(f"{parsed.e164} {parsed.operator}")
59
+ else:
60
+ print(f"{raw} invalid", file=sys.stderr)
61
+ status = 1
62
+ return status
63
+ elif args.command == "lakh":
64
+ print(format_lakh(args.amount, decimals=args.decimals, bangla=args.bn))
65
+ elif args.command == "bn":
66
+ print(to_bangla_digits(" ".join(args.text)))
67
+ elif args.command == "en":
68
+ print(to_english_digits(" ".join(args.text)))
69
+ except (ValueError, ArithmeticError) as e:
70
+ print(f"Error: {e}", file=sys.stderr)
71
+ return 1
72
+ return 0
73
+
74
+
75
+ if __name__ == "__main__":
76
+ sys.exit(main())
@@ -0,0 +1,44 @@
1
+ """Bangla ↔ English digits and lakh-style number formatting."""
2
+ from __future__ import annotations
3
+
4
+ from decimal import Decimal
5
+ from typing import Union
6
+
7
+ _EN = "0123456789"
8
+ _BN = "০১২৩৪৫৬৭৮৯"
9
+ _TO_BN = str.maketrans(_EN, _BN)
10
+ _TO_EN = str.maketrans(_BN, _EN)
11
+
12
+
13
+ def to_bangla_digits(value: object) -> str:
14
+ """``'2026-11-15' -> '২০২৬-১১-১৫'``"""
15
+ return str(value).translate(_TO_BN)
16
+
17
+
18
+ def to_english_digits(value: object) -> str:
19
+ """``'০১৭১২' -> '01712'``"""
20
+ return str(value).translate(_TO_EN)
21
+
22
+
23
+ def format_lakh(amount: Union[int, float, Decimal, str], decimals: int = 0, bangla: bool = False) -> str:
24
+ """Group digits the South Asian way: ``12345678 -> '1,23,45,678'``.
25
+
26
+ >>> format_lakh(12345678.5, decimals=2)
27
+ '1,23,45,678.50'
28
+ >>> format_lakh(150000, bangla=True)
29
+ '১,৫০,০০০'
30
+ """
31
+ value = Decimal(to_english_digits(str(amount)).replace(",", ""))
32
+ sign = "-" if value < 0 else ""
33
+ text = f"{abs(value):.{decimals}f}"
34
+ whole, _, frac = text.partition(".")
35
+ if len(whole) > 3:
36
+ head, tail = whole[:-3], whole[-3:]
37
+ groups = []
38
+ while len(head) > 2:
39
+ groups.insert(0, head[-2:])
40
+ head = head[:-2]
41
+ groups.insert(0, head)
42
+ whole = ",".join(groups + [tail])
43
+ result = sign + whole + (f".{frac}" if frac else "")
44
+ return to_bangla_digits(result) if bangla else result
@@ -0,0 +1,86 @@
1
+ """Bangladeshi mobile numbers: validate, normalise and detect the operator."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ from .digits import to_english_digits
9
+
10
+ OPERATORS = {
11
+ "013": "Grameenphone",
12
+ "017": "Grameenphone",
13
+ "014": "Banglalink",
14
+ "019": "Banglalink",
15
+ "016": "Robi", # formerly Airtel, merged into Robi
16
+ "018": "Robi",
17
+ "015": "Teletalk",
18
+ }
19
+
20
+ _SEPARATORS = re.compile(r"[\s\-().]")
21
+ # Optional country code (+88 / 0088 / 88), optional trunk 0, then 1 + operator digit + 8 digits.
22
+ _PATTERN = re.compile(r"^(?:(?:\+|00)?88)?0?(1[3-9]\d{8})$")
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class MobileNumber:
27
+ """A valid Bangladeshi mobile number."""
28
+
29
+ local: str # 01712345678
30
+
31
+ @property
32
+ def e164(self) -> str:
33
+ """International format: ``+8801712345678``."""
34
+ return "+88" + self.local
35
+
36
+ @property
37
+ def prefix(self) -> str:
38
+ return self.local[:3]
39
+
40
+ @property
41
+ def operator(self) -> str:
42
+ return OPERATORS[self.prefix]
43
+
44
+ def formatted(self) -> str:
45
+ """Readable local format: ``01712-345678``."""
46
+ return f"{self.local[:5]}-{self.local[5:]}"
47
+
48
+ def __str__(self) -> str:
49
+ return self.e164
50
+
51
+
52
+ def parse_mobile(number: str) -> Optional[MobileNumber]:
53
+ """Parse a mobile number written in almost any common way.
54
+
55
+ Accepts ``01712345678``, ``+880 1712-345678``, ``8801712345678``,
56
+ ``008801712345678`` and Bangla digits. Returns ``None`` if invalid.
57
+
58
+ >>> parse_mobile("+880 1712-345678").operator
59
+ 'Grameenphone'
60
+ >>> parse_mobile("০১৮১২৩৪৫৬৭৮").e164
61
+ '+8801812345678'
62
+ >>> parse_mobile("0123") is None
63
+ True
64
+ """
65
+ cleaned = _SEPARATORS.sub("", to_english_digits(str(number)))
66
+ match = _PATTERN.match(cleaned)
67
+ return MobileNumber("0" + match.group(1)) if match else None
68
+
69
+
70
+ def is_valid_mobile(number: str) -> bool:
71
+ """``True`` if ``number`` is a valid Bangladeshi mobile number."""
72
+ return parse_mobile(number) is not None
73
+
74
+
75
+ def normalize_mobile(number: str) -> str:
76
+ """Return the number as ``+8801XXXXXXXXX`` or raise ``ValueError``."""
77
+ parsed = parse_mobile(number)
78
+ if parsed is None:
79
+ raise ValueError(f"Not a valid Bangladeshi mobile number: {number!r}")
80
+ return parsed.e164
81
+
82
+
83
+ def mobile_operator(number: str) -> Optional[str]:
84
+ """Operator name for a number (``'Grameenphone'``, ``'Robi'``...), or ``None`` if invalid."""
85
+ parsed = parse_mobile(number)
86
+ return parsed.operator if parsed else None
File without changes
@@ -0,0 +1,123 @@
1
+ """Numbers and taka amounts in words, using the lakh/crore system, in English or Bangla."""
2
+ from __future__ import annotations
3
+
4
+ from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
5
+ from typing import List, Tuple, Union
6
+
7
+ Number = Union[int, float, str, Decimal]
8
+
9
+ _EN_ONES = (
10
+ "Zero One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve Thirteen Fourteen "
11
+ "Fifteen Sixteen Seventeen Eighteen Nineteen"
12
+ ).split()
13
+ _EN_TENS = "_ _ Twenty Thirty Forty Fifty Sixty Seventy Eighty Ninety".split()
14
+
15
+ # Bangla number words 0-99 are irregular, so they are listed in full.
16
+ _BN_0_99 = (
17
+ "শূন্য এক দুই তিন চার পাঁচ ছয় সাত আট নয় "
18
+ "দশ এগারো বারো তেরো চৌদ্দ পনেরো ষোলো সতেরো আঠারো উনিশ "
19
+ "বিশ একুশ বাইশ তেইশ চব্বিশ পঁচিশ ছাব্বিশ সাতাশ আটাশ ঊনত্রিশ "
20
+ "ত্রিশ একত্রিশ বত্রিশ তেত্রিশ চৌত্রিশ পঁয়ত্রিশ ছত্রিশ সাঁইত্রিশ আটত্রিশ ঊনচল্লিশ "
21
+ "চল্লিশ একচল্লিশ বিয়াল্লিশ তেতাল্লিশ চুয়াল্লিশ পঁয়তাল্লিশ ছেচল্লিশ সাতচল্লিশ আটচল্লিশ ঊনপঞ্চাশ "
22
+ "পঞ্চাশ একান্ন বায়ান্ন তিপ্পান্ন চুয়ান্ন পঞ্চান্ন ছাপ্পান্ন সাতান্ন আটান্ন ঊনষাট "
23
+ "ষাট একষট্টি বাষট্টি তেষট্টি চৌষট্টি পঁয়ষট্টি ছেষট্টি সাতষট্টি আটষট্টি ঊনসত্তর "
24
+ "সত্তর একাত্তর বাহাত্তর তিয়াত্তর চুয়াত্তর পঁচাত্তর ছিয়াত্তর সাতাত্তর আটাত্তর ঊনআশি "
25
+ "আশি একাশি বিরাশি তিরাশি চুরাশি পঁচাশি ছিয়াশি সাতাশি আটাশি ঊননব্বই "
26
+ "নব্বই একানব্বই বিরানব্বই তিরানব্বই চুরানব্বই পঁচানব্বই ছিয়ানব্বই সাতানব্বই আটানব্বই নিরানব্বই"
27
+ ).split()
28
+ assert len(_BN_0_99) == 100
29
+
30
+ # (value, english, bangla) from largest to smallest; crore repeats for bigger numbers.
31
+ _SCALES: Tuple[Tuple[int, str, str], ...] = (
32
+ (10_000_000, "Crore", "কোটি"),
33
+ (100_000, "Lakh", "লক্ষ"),
34
+ (1_000, "Thousand", "হাজার"),
35
+ (100, "Hundred", "শত"),
36
+ )
37
+
38
+
39
+ def _en_below_100(n: int) -> str:
40
+ if n < 20:
41
+ return _EN_ONES[n]
42
+ tens, ones = divmod(n, 10)
43
+ return _EN_TENS[tens] + (f"-{_EN_ONES[ones]}" if ones else "")
44
+
45
+
46
+ def _words(n: int, lang: str) -> List[str]:
47
+ """Words for a non-negative integer, lakh/crore style."""
48
+ small = _en_below_100 if lang == "en" else (lambda k: _BN_0_99[k])
49
+ if n < 100:
50
+ return [small(n)]
51
+ parts: List[str] = []
52
+ for value, en, bn in _SCALES:
53
+ if n >= value:
54
+ count, n = divmod(n, value)
55
+ # Above 99 crore, the crore count itself is spelled out ("One Hundred Crore").
56
+ parts += _words(count, lang) if count >= 100 else [small(count)]
57
+ parts.append(en if lang == "en" else bn)
58
+ if n:
59
+ parts.append(small(n))
60
+ return parts
61
+
62
+
63
+ def _check_lang(lang: str) -> str:
64
+ lang = lang.lower()
65
+ if lang not in ("en", "bn"):
66
+ raise ValueError("lang must be 'en' or 'bn'")
67
+ return lang
68
+
69
+
70
+ def number_to_words(number: int, lang: str = "en") -> str:
71
+ """Spell out a whole number using lakh and crore.
72
+
73
+ >>> number_to_words(1_25_000)
74
+ 'One Lakh Twenty-Five Thousand'
75
+ >>> number_to_words(1_25_000, lang="bn")
76
+ 'এক লক্ষ পঁচিশ হাজার'
77
+ """
78
+ lang = _check_lang(lang)
79
+ if isinstance(number, bool) or int(number) != number:
80
+ raise ValueError("number_to_words needs a whole number; use taka_in_words for money")
81
+ n = int(number)
82
+ prefix = ("Minus " if lang == "en" else "ঋণাত্মক ") if n < 0 else ""
83
+ return prefix + " ".join(_words(abs(n), lang))
84
+
85
+
86
+ def _to_decimal(amount: Number) -> Decimal:
87
+ try:
88
+ value = Decimal(str(amount).replace(",", "").replace("৳", "").strip())
89
+ except InvalidOperation:
90
+ raise ValueError(f"Not a valid amount: {amount!r}") from None
91
+ if not value.is_finite():
92
+ raise ValueError(f"Not a valid amount: {amount!r}")
93
+ return value.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
94
+
95
+
96
+ def taka_in_words(amount: Number, lang: str = "en", only: bool = True) -> str:
97
+ """Write a taka amount in words, as on invoices and cheques.
98
+
99
+ Paisa (two decimal places) are included when present. Pass ``only=False``
100
+ to drop the trailing "Only" / "মাত্র".
101
+
102
+ >>> taka_in_words(125050.50)
103
+ 'One Lakh Twenty-Five Thousand Fifty Taka and Fifty Paisa Only'
104
+ >>> taka_in_words("12,500", lang="bn")
105
+ 'বারো হাজার পাঁচ শত টাকা মাত্র'
106
+ """
107
+ lang = _check_lang(lang)
108
+ value = _to_decimal(amount)
109
+ negative = value < 0
110
+ taka, paisa = divmod(int(abs(value) * 100), 100)
111
+
112
+ if lang == "en":
113
+ text = f"{number_to_words(taka)} Taka"
114
+ if paisa:
115
+ text += f" and {number_to_words(paisa)} Paisa"
116
+ text += " Only" if only else ""
117
+ return ("Minus " + text) if negative else text
118
+
119
+ text = f"{number_to_words(taka, 'bn')} টাকা"
120
+ if paisa:
121
+ text += f" {number_to_words(paisa, 'bn')} পয়সা"
122
+ text += " মাত্র" if only else ""
123
+ return ("ঋণাত্মক " + text) if negative else text
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.26"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "bdkit"
7
+ version = "0.1.0"
8
+ description = "Everyday Bangladesh utilities: taka in words (English/Bangla, lakh/crore), mobile number validation with operator detection, Bangla digits."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = "MIT"
12
+ license-files = ["LICENSE"]
13
+ authors = [{ name = "Sarif Mia" }]
14
+ keywords = ["bangladesh", "bangla", "bengali", "taka", "amount-in-words", "lakh", "crore", "phone", "grameenphone"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3",
17
+ "Operating System :: OS Independent",
18
+ "Natural Language :: Bengali",
19
+ "Topic :: Software Development :: Libraries",
20
+ "Topic :: Text Processing :: Linguistic",
21
+ "Typing :: Typed",
22
+ ]
23
+ dependencies = []
24
+
25
+ [project.optional-dependencies]
26
+ test = ["pytest>=7"]
27
+
28
+ [project.scripts]
29
+ bdkit = "bdkit.cli:main"
30
+
31
+ [project.urls]
32
+ Homepage = "https://github.com/sarif-mia/bdkit"
33
+ Source = "https://github.com/sarif-mia/bdkit"
34
+ Tracker = "https://github.com/sarif-mia/bdkit/issues"
35
+
36
+ [tool.hatch.build.targets.sdist]
37
+ include = ["bdkit", "tests", "README.md", "LICENSE"]
38
+
39
+ [tool.pytest.ini_options]
40
+ addopts = "--doctest-modules"
41
+ testpaths = ["tests", "bdkit"]
@@ -0,0 +1,127 @@
1
+ import pytest
2
+
3
+ from bdkit import (
4
+ format_lakh,
5
+ is_valid_mobile,
6
+ mobile_operator,
7
+ normalize_mobile,
8
+ number_to_words,
9
+ parse_mobile,
10
+ taka_in_words,
11
+ to_bangla_digits,
12
+ to_english_digits,
13
+ )
14
+ from bdkit.cli import main
15
+
16
+
17
+ @pytest.mark.parametrize("n,en", [
18
+ (0, "Zero"),
19
+ (7, "Seven"),
20
+ (19, "Nineteen"),
21
+ (21, "Twenty-One"),
22
+ (100, "One Hundred"),
23
+ (101, "One Hundred One"),
24
+ (999, "Nine Hundred Ninety-Nine"),
25
+ (1_000, "One Thousand"),
26
+ (10_500, "Ten Thousand Five Hundred"),
27
+ (1_00_000, "One Lakh"),
28
+ (12_34_567, "Twelve Lakh Thirty-Four Thousand Five Hundred Sixty-Seven"),
29
+ (1_00_00_000, "One Crore"),
30
+ (99_99_99_999, "Ninety-Nine Crore Ninety-Nine Lakh Ninety-Nine Thousand Nine Hundred Ninety-Nine"),
31
+ (1_00_00_00_000, "One Hundred Crore"),
32
+ (1_50_00_00_00_000, "Fifteen Thousand Crore"),
33
+ (1_50_000_00_00_000, "One Lakh Fifty Thousand Crore"),
34
+ (-45, "Minus Forty-Five"),
35
+ ])
36
+ def test_english_words(n, en):
37
+ assert number_to_words(n) == en
38
+
39
+
40
+ @pytest.mark.parametrize("n,bn", [
41
+ (0, "শূন্য"),
42
+ (15, "পনেরো"),
43
+ (29, "ঊনত্রিশ"),
44
+ (99, "নিরানব্বই"),
45
+ (100, "এক শত"),
46
+ (1_25_000, "এক লক্ষ পঁচিশ হাজার"),
47
+ (2_00_00_000, "দুই কোটি"),
48
+ (52_71_036, "বায়ান্ন লক্ষ একাত্তর হাজার ছত্রিশ"),
49
+ ])
50
+ def test_bangla_words(n, bn):
51
+ assert number_to_words(n, lang="bn") == bn
52
+
53
+
54
+ def test_taka_in_words():
55
+ assert taka_in_words(125050.50) == "One Lakh Twenty-Five Thousand Fifty Taka and Fifty Paisa Only"
56
+ assert taka_in_words("12,500", lang="bn") == "বারো হাজার পাঁচ শত টাকা মাত্র"
57
+ assert taka_in_words(0.75, lang="bn") == "শূন্য টাকা পঁচাত্তর পয়সা মাত্র"
58
+ assert taka_in_words("৳1,000", only=False) == "One Thousand Taka"
59
+ assert taka_in_words(10.005) == "Ten Taka and One Paisa Only" # rounds half up
60
+ assert taka_in_words(-20) == "Minus Twenty Taka Only"
61
+
62
+
63
+ def test_words_errors():
64
+ with pytest.raises(ValueError):
65
+ taka_in_words("abc")
66
+ with pytest.raises(ValueError):
67
+ taka_in_words(float("inf"))
68
+ with pytest.raises(ValueError):
69
+ number_to_words(1.5)
70
+ with pytest.raises(ValueError):
71
+ number_to_words(5, lang="fr")
72
+
73
+
74
+ @pytest.mark.parametrize("raw,operator", [
75
+ ("01712345678", "Grameenphone"),
76
+ ("01312345678", "Grameenphone"),
77
+ ("+880 1912-345678", "Banglalink"),
78
+ ("8801412345678", "Banglalink"),
79
+ ("008801812345678", "Robi"),
80
+ ("(016) 1234-5678", "Robi"),
81
+ ("01512345678", "Teletalk"),
82
+ ("+88 01712345678", "Grameenphone"),
83
+ ("০১৭১২৩৪৫৬৭৮", "Grameenphone"),
84
+ ])
85
+ def test_valid_mobiles(raw, operator):
86
+ assert is_valid_mobile(raw)
87
+ assert mobile_operator(raw) == operator
88
+
89
+
90
+ @pytest.mark.parametrize("raw", ["", "0171234567", "017123456789", "01212345678", "02-9123456", "+9101712345678", "abc"])
91
+ def test_invalid_mobiles(raw):
92
+ assert not is_valid_mobile(raw)
93
+ assert mobile_operator(raw) is None
94
+ with pytest.raises(ValueError):
95
+ normalize_mobile(raw)
96
+
97
+
98
+ def test_mobile_formats():
99
+ m = parse_mobile("+8801712345678")
100
+ assert m.local == "01712345678" and m.e164 == "+8801712345678" and m.formatted() == "01712-345678"
101
+ assert str(m) == "+8801712345678"
102
+
103
+
104
+ def test_digits_and_lakh():
105
+ assert to_bangla_digits("2026-11-15") == "২০২৬-১১-১৫"
106
+ assert to_english_digits("৳১,২৫,০০০") == "৳1,25,000"
107
+ assert format_lakh(12345678) == "1,23,45,678"
108
+ assert format_lakh(999) == "999"
109
+ assert format_lakh(-1234567.891, decimals=2) == "-12,34,567.89"
110
+ assert format_lakh("১৫০০০০", bangla=True) == "১,৫০,০০০"
111
+
112
+
113
+ def test_cli(capsys):
114
+ assert main(["words", "125050.50"]) == 0
115
+ assert main(["words", "12500", "--bn"]) == 0
116
+ assert main(["phone", "+880 1712-345678"]) == 0
117
+ assert main(["lakh", "12345678"]) == 0
118
+ assert main(["bn", "2026"]) == 0
119
+ assert capsys.readouterr().out.splitlines() == [
120
+ "One Lakh Twenty-Five Thousand Fifty Taka and Fifty Paisa Only",
121
+ "বারো হাজার পাঁচ শত টাকা মাত্র",
122
+ "+8801712345678 Grameenphone",
123
+ "1,23,45,678",
124
+ "২০২৬",
125
+ ]
126
+ assert main(["phone", "123"]) == 1
127
+ assert main(["words", "xyz"]) == 1