djalaali 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.
@@ -0,0 +1,65 @@
1
+ Metadata-Version: 2.4
2
+ Name: djalaali
3
+ Version: 0.1.0
4
+ Summary: Jalali (Persian) calendar support for Django
5
+ License: MIT
6
+ Keywords: django,jalali,persian,calendar,jdate
7
+ Classifier: Development Status :: 3 - Alpha
8
+ Classifier: Environment :: Web Environment
9
+ Classifier: Framework :: Django
10
+ Classifier: Framework :: Django :: 4.2
11
+ Classifier: Framework :: Django :: 5.0
12
+ Classifier: Framework :: Django :: 5.1
13
+ Classifier: Framework :: Django :: 5.2
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: C
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: 3.9
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Programming Language :: Python :: 3.13
25
+ Classifier: Topic :: Software Development :: Libraries
26
+ Classifier: Topic :: Software Development :: Internationalization
27
+ Requires-Python: >=3.9
28
+ Description-Content-Type: text/markdown
29
+ Requires-Dist: Django>=4.2
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest; extra == "dev"
32
+ Requires-Dist: pytest-cov; extra == "dev"
33
+ Requires-Dist: pytest-xdist; extra == "dev"
34
+
35
+ # djalaali
36
+
37
+ Jalali (Persian) calendar support for Django: `jdate` and `jdatetime` types,
38
+ model and form fields, and template tags.
39
+
40
+ ## Install
41
+
42
+ ```sh
43
+ pip install djalaali
44
+ ```
45
+
46
+ Add `"djalaali"` to `INSTALLED_APPS`.
47
+
48
+ ## Usage
49
+
50
+ ```python
51
+ from djalaali.jdate import jdate
52
+
53
+ d = jdate(1403, 1, 1)
54
+ ```
55
+
56
+ ## Development
57
+
58
+ ```sh
59
+ make install
60
+ make test
61
+ ```
62
+
63
+ ## License
64
+
65
+ MIT
@@ -0,0 +1,31 @@
1
+ # djalaali
2
+
3
+ Jalali (Persian) calendar support for Django: `jdate` and `jdatetime` types,
4
+ model and form fields, and template tags.
5
+
6
+ ## Install
7
+
8
+ ```sh
9
+ pip install djalaali
10
+ ```
11
+
12
+ Add `"djalaali"` to `INSTALLED_APPS`.
13
+
14
+ ## Usage
15
+
16
+ ```python
17
+ from djalaali.jdate import jdate
18
+
19
+ d = jdate(1403, 1, 1)
20
+ ```
21
+
22
+ ## Development
23
+
24
+ ```sh
25
+ make install
26
+ make test
27
+ ```
28
+
29
+ ## License
30
+
31
+ MIT
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
@@ -0,0 +1,336 @@
1
+ /*
2
+ * C implementation of the Jalaali conversion algorithm in djalaali/algo.py.
3
+ *
4
+ * Ported from https://github.com/jalaali/go-jalaali
5
+ *
6
+ * Built against the stable ABI (abi3), so a single wheel per platform serves
7
+ * every supported Python version. C integer division and remainder truncate
8
+ * toward zero, exactly like Go's, so the formulas are copied verbatim.
9
+ *
10
+ * Arguments that do not fit comfortably in 64-bit arithmetic are handed to
11
+ * the pure-Python implementation registered through setup(), which keeps
12
+ * the behaviour (including exceptions) identical for every input.
13
+ */
14
+ #define PY_SSIZE_T_CLEAN
15
+ #include <Python.h>
16
+ #include <stdint.h>
17
+
18
+ #define N_BREAKS 20
19
+ #define MAX_ARG 1000000000000LL /* keeps every intermediate within int64 */
20
+
21
+ static const int64_t BREAKS[N_BREAKS] = {
22
+ -61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181,
23
+ 1210, 1635, 2060, 2097, 2192, 2262, 2324, 2394, 2456, 3178,
24
+ };
25
+
26
+ static PyObject *error_type = NULL;
27
+ static PyObject *py_to_jalaali = NULL;
28
+ static PyObject *py_to_gregorian = NULL;
29
+ static PyObject *py_is_leap_year = NULL;
30
+ static PyObject *py_month_length = NULL;
31
+
32
+ /* Returns 0 on success, -1 with an exception set, 1 if too large for C. */
33
+ static int
34
+ get_arg(PyObject *obj, int64_t *out)
35
+ {
36
+ int overflow = 0;
37
+ long long v = PyLong_AsLongLongAndOverflow(obj, &overflow);
38
+ if (v == -1 && PyErr_Occurred()) {
39
+ return -1;
40
+ }
41
+ if (overflow || v > MAX_ARG || v < -MAX_ARG) {
42
+ return 1;
43
+ }
44
+ *out = v;
45
+ return 0;
46
+ }
47
+
48
+ static int
49
+ get_args(PyObject *args, int n, int64_t *out)
50
+ {
51
+ if (PyTuple_Size(args) != n) {
52
+ PyErr_Format(PyExc_TypeError, "expected %d arguments, got %zd", n,
53
+ PyTuple_Size(args));
54
+ return -1;
55
+ }
56
+ int too_big = 0;
57
+ for (int i = 0; i < n; i++) {
58
+ int r = get_arg(PyTuple_GetItem(args, i), &out[i]);
59
+ if (r < 0) {
60
+ return -1;
61
+ }
62
+ too_big |= r;
63
+ }
64
+ return too_big;
65
+ }
66
+
67
+ static void
68
+ raise_invalid_year(int64_t year)
69
+ {
70
+ PyObject *exc = PyObject_CallFunction(error_type, "L", (long long)year);
71
+ if (exc != NULL) {
72
+ PyErr_SetObject(error_type, exc);
73
+ Py_DECREF(exc);
74
+ }
75
+ }
76
+
77
+ /* Returns 0 on success, -1 (without an exception set) for an invalid year. */
78
+ static int
79
+ jal_cal(int64_t jy, int64_t *leap, int64_t *gy, int64_t *march)
80
+ {
81
+ int64_t g = jy + 621;
82
+ int64_t leap_j = -14;
83
+ int64_t jp = BREAKS[0];
84
+ int64_t jump = 0;
85
+
86
+ if (jy < jp || jy >= BREAKS[N_BREAKS - 1]) {
87
+ return -1;
88
+ }
89
+
90
+ for (int i = 1; i < N_BREAKS; i++) {
91
+ int64_t jm = BREAKS[i];
92
+ jump = jm - jp;
93
+ if (jy < jm) {
94
+ break;
95
+ }
96
+ leap_j += (jump / 33) * 8 + (jump % 33) / 4;
97
+ jp = jm;
98
+ }
99
+
100
+ int64_t n = jy - jp;
101
+ leap_j += (n / 33) * 8 + (n % 33 + 3) / 4;
102
+ if (jump % 33 == 4 && jump - n == 4) {
103
+ leap_j += 1;
104
+ }
105
+
106
+ int64_t leap_g = g / 4 - ((g / 100 + 1) * 3) / 4 - 150;
107
+
108
+ if (jump - n < 6) {
109
+ n = n - jump + ((jump + 4) / 33) * 33;
110
+ }
111
+
112
+ int64_t l = ((n + 1) % 33 - 1) % 4;
113
+ if (l == -1) {
114
+ l = 4;
115
+ }
116
+
117
+ *leap = l;
118
+ *gy = g;
119
+ *march = 20 + leap_j - leap_g;
120
+ return 0;
121
+ }
122
+
123
+ static int64_t
124
+ g2d(int64_t gy, int64_t gm, int64_t gd)
125
+ {
126
+ int64_t d = ((gy + (gm - 8) / 6 + 100100) * 1461) / 4 +
127
+ (153 * ((gm + 9) % 12) + 2) / 5 + gd - 34840408;
128
+ d = d - (((gy + 100100 + (gm - 8) / 6) / 100) * 3) / 4 + 752;
129
+ return d;
130
+ }
131
+
132
+ static void
133
+ d2g(int64_t jdn, int64_t *gy, int64_t *gm, int64_t *gd)
134
+ {
135
+ int64_t j = 4 * jdn + 139361631;
136
+ j = j + (((4 * jdn + 183187720) / 146097) * 3) / 4 * 4 - 3908;
137
+ int64_t i = ((j % 1461) / 4) * 5 + 308;
138
+ *gd = (i % 153) / 5 + 1;
139
+ *gm = (i / 153) % 12 + 1;
140
+ *gy = j / 1461 - 100100 + (8 - *gm) / 6;
141
+ }
142
+
143
+ static int64_t
144
+ month_len(int64_t jy, int64_t jm, int *err)
145
+ {
146
+ *err = 0;
147
+ if (jm <= 6) {
148
+ return 31;
149
+ }
150
+ if (jm <= 11) {
151
+ return 30;
152
+ }
153
+ int64_t leap, gy, march;
154
+ if (jal_cal(jy, &leap, &gy, &march) < 0) {
155
+ *err = 1;
156
+ return 0;
157
+ }
158
+ return leap == 0 ? 30 : 29;
159
+ }
160
+
161
+ static PyObject *
162
+ fallback(PyObject *func, PyObject *args)
163
+ {
164
+ return PyObject_CallObject(func, args);
165
+ }
166
+
167
+ static PyObject *
168
+ calgo_to_jalaali(PyObject *self, PyObject *args)
169
+ {
170
+ int64_t a[3];
171
+ int r = get_args(args, 3, a);
172
+ if (r < 0) {
173
+ return NULL;
174
+ }
175
+ if (r > 0) {
176
+ return fallback(py_to_jalaali, args);
177
+ }
178
+
179
+ int64_t gy, gm, gd;
180
+ int64_t jdn = g2d(a[0], a[1], a[2]);
181
+ d2g(jdn, &gy, &gm, &gd);
182
+
183
+ int64_t jy = gy - 621;
184
+ int64_t leap, g, march;
185
+ if (jal_cal(jy, &leap, &g, &march) < 0) {
186
+ raise_invalid_year(jy);
187
+ return NULL;
188
+ }
189
+ int64_t k = jdn - g2d(gy, 3, march);
190
+ int64_t jm, jd;
191
+ if (k >= 0) {
192
+ if (k <= 185) {
193
+ return Py_BuildValue("(LLL)", (long long)jy,
194
+ (long long)(1 + k / 31),
195
+ (long long)(k % 31 + 1));
196
+ }
197
+ k -= 186;
198
+ }
199
+ else {
200
+ jy -= 1;
201
+ k += 179;
202
+ if (leap == 1) {
203
+ k += 1;
204
+ }
205
+ }
206
+ jm = 7 + k / 30;
207
+ jd = k % 30 + 1;
208
+ return Py_BuildValue("(LLL)", (long long)jy, (long long)jm,
209
+ (long long)jd);
210
+ }
211
+
212
+ static PyObject *
213
+ calgo_to_gregorian(PyObject *self, PyObject *args)
214
+ {
215
+ int64_t a[3];
216
+ int r = get_args(args, 3, a);
217
+ if (r < 0) {
218
+ return NULL;
219
+ }
220
+ if (r > 0) {
221
+ return fallback(py_to_gregorian, args);
222
+ }
223
+ int64_t jy = a[0], jm = a[1], jd = a[2];
224
+
225
+ int valid = !(jy < -61 || jy > 3177 || jm < 1 || jm > 12 || jd < 1);
226
+ if (valid) {
227
+ int err;
228
+ valid = jd <= month_len(jy, jm, &err);
229
+ }
230
+ if (!valid) {
231
+ return PyErr_Format(
232
+ PyExc_ValueError,
233
+ "invalid jalaali date: year=%lld, month=%lld, day=%lld",
234
+ (long long)jy, (long long)jm, (long long)jd);
235
+ }
236
+
237
+ int64_t leap, gy, march;
238
+ if (jal_cal(jy, &leap, &gy, &march) < 0) {
239
+ raise_invalid_year(jy);
240
+ return NULL;
241
+ }
242
+ int64_t jm7 = jm / 7;
243
+ int64_t jdn = g2d(gy, 3, march) + (jm - 1) * 31 - jm7 * (jm - 7) + jd - 1;
244
+ int64_t y, m, d;
245
+ d2g(jdn, &y, &m, &d);
246
+ return Py_BuildValue("(LLL)", (long long)y, (long long)m, (long long)d);
247
+ }
248
+
249
+ static PyObject *
250
+ calgo_is_leap_year(PyObject *self, PyObject *args)
251
+ {
252
+ int64_t a[1];
253
+ int r = get_args(args, 1, a);
254
+ if (r < 0) {
255
+ return NULL;
256
+ }
257
+ if (r > 0) {
258
+ return fallback(py_is_leap_year, args);
259
+ }
260
+ int64_t leap, gy, march;
261
+ if (jal_cal(a[0], &leap, &gy, &march) < 0) {
262
+ raise_invalid_year(a[0]);
263
+ return NULL;
264
+ }
265
+ return PyBool_FromLong(leap == 0);
266
+ }
267
+
268
+ static PyObject *
269
+ calgo_month_length(PyObject *self, PyObject *args)
270
+ {
271
+ int64_t a[2];
272
+ int r = get_args(args, 2, a);
273
+ if (r < 0) {
274
+ return NULL;
275
+ }
276
+ if (r > 0) {
277
+ return fallback(py_month_length, args);
278
+ }
279
+ int err;
280
+ int64_t n = month_len(a[0], a[1], &err);
281
+ if (err) {
282
+ raise_invalid_year(a[0]);
283
+ return NULL;
284
+ }
285
+ return PyLong_FromLongLong(n);
286
+ }
287
+
288
+ static PyObject *
289
+ calgo_setup(PyObject *self, PyObject *args)
290
+ {
291
+ PyObject *e, *a, *b, *c, *d;
292
+ if (!PyArg_ParseTuple(args, "OOOOO", &e, &a, &b, &c, &d)) {
293
+ return NULL;
294
+ }
295
+ Py_INCREF(e);
296
+ Py_XDECREF(error_type);
297
+ error_type = e;
298
+ Py_INCREF(a);
299
+ Py_XDECREF(py_to_jalaali);
300
+ py_to_jalaali = a;
301
+ Py_INCREF(b);
302
+ Py_XDECREF(py_to_gregorian);
303
+ py_to_gregorian = b;
304
+ Py_INCREF(c);
305
+ Py_XDECREF(py_is_leap_year);
306
+ py_is_leap_year = c;
307
+ Py_INCREF(d);
308
+ Py_XDECREF(py_month_length);
309
+ py_month_length = d;
310
+ Py_RETURN_NONE;
311
+ }
312
+
313
+ static PyMethodDef methods[] = {
314
+ {"to_jalaali", calgo_to_jalaali, METH_VARARGS,
315
+ "to_jalaali(gy, gm, gd) -> (jy, jm, jd)"},
316
+ {"to_gregorian", calgo_to_gregorian, METH_VARARGS,
317
+ "to_gregorian(jy, jm, jd) -> (gy, gm, gd)"},
318
+ {"is_leap_year", calgo_is_leap_year, METH_VARARGS,
319
+ "is_leap_year(jy) -> bool"},
320
+ {"month_length", calgo_month_length, METH_VARARGS,
321
+ "month_length(jy, jm) -> int"},
322
+ {"setup", calgo_setup, METH_VARARGS,
323
+ "setup(error_type, to_jalaali, to_gregorian, is_leap_year, "
324
+ "month_length): register the exception class and Python fallbacks"},
325
+ {NULL, NULL, 0, NULL},
326
+ };
327
+
328
+ static struct PyModuleDef module = {
329
+ PyModuleDef_HEAD_INIT, "djalaali._calgo", NULL, -1, methods,
330
+ };
331
+
332
+ PyMODINIT_FUNC
333
+ PyInit__calgo(void)
334
+ {
335
+ return PyModule_Create(&module);
336
+ }
@@ -0,0 +1,235 @@
1
+ """
2
+ jalaali - A Python implementation of the Jalaali calendar conversion.
3
+
4
+ Ported from https://github.com/jalaali/go-jalaali
5
+ """
6
+
7
+ import os
8
+ from enum import IntEnum
9
+
10
+ # Jalaali leap year break points
11
+ BREAKS = [
12
+ -61,
13
+ 9,
14
+ 38,
15
+ 199,
16
+ 426,
17
+ 686,
18
+ 756,
19
+ 818,
20
+ 1111,
21
+ 1181,
22
+ 1210,
23
+ 1635,
24
+ 2060,
25
+ 2097,
26
+ 2192,
27
+ 2262,
28
+ 2324,
29
+ 2394,
30
+ 2456,
31
+ 3178,
32
+ ]
33
+
34
+
35
+ class Month(IntEnum):
36
+ """Represents a Jalaali month (1-12)."""
37
+
38
+ Farvardin = 1
39
+ Ordibehesht = 2
40
+ Khordad = 3
41
+ Tir = 4
42
+ Mordad = 5
43
+ Shahrivar = 6
44
+ Mehr = 7
45
+ Aban = 8
46
+ Azar = 9
47
+ Dey = 10
48
+ Bahman = 11
49
+ Esfand = 12
50
+
51
+
52
+ class ErrorInvalidYear(Exception):
53
+ """Raised when year is out of valid range."""
54
+
55
+ def __init__(self, year):
56
+ self.year = year
57
+ super().__init__(f"jalaali: {year} is invalid year")
58
+
59
+
60
+ def _div(a, b):
61
+ """Integer division truncated toward zero (matches Go's `/` for ints)."""
62
+ q = abs(a) // abs(b)
63
+ if (a < 0) != (b < 0):
64
+ q = -q
65
+ return q
66
+
67
+
68
+ def _mod(a, b):
69
+ """Remainder with sign of dividend (matches Go's `%` for ints)."""
70
+ r = abs(a) % abs(b)
71
+ if a < 0:
72
+ r = -r
73
+ return r
74
+
75
+
76
+ def to_jalaali(gy, gm, gd):
77
+ """
78
+ Convert a Gregorian date to a Jalaali date.
79
+
80
+ Returns a tuple (jy, jm, jd).
81
+ Raises ErrorInvalidYear if the resulting Jalaali year is out of range.
82
+ """
83
+ return _d2j(_g2d(gy, gm, gd))
84
+
85
+
86
+ def to_gregorian(jy, jm, jd):
87
+ """
88
+ Convert a Jalaali date to a Gregorian date.
89
+
90
+ Returns a tuple (gy, gm, gd).
91
+ Raises ValueError for an invalid Jalaali date.
92
+ Raises ErrorInvalidYear if the year is out of range.
93
+ """
94
+ if not _is_valid_date(jy, jm, jd):
95
+ raise ValueError(
96
+ f"invalid jalaali date: year={jy}, month={jm}, day={jd}"
97
+ )
98
+ jdn = _j2d(jy, jm, jd)
99
+ return _d2g(jdn)
100
+
101
+
102
+ def is_leap_year(jy):
103
+ """Return True if the given Jalaali year is a leap year."""
104
+ leap, _, _, _ = _jal_cal(jy)
105
+ return leap == 0
106
+
107
+
108
+ def month_length(jy, jm):
109
+ """Return the number of days in the given Jalaali month."""
110
+ if jm <= 6:
111
+ return 31
112
+ elif jm <= 11:
113
+ return 30
114
+ if is_leap_year(jy):
115
+ return 30
116
+ return 29
117
+
118
+
119
+ def _is_valid_date(jy, jm, jd):
120
+ if jy < -61 or jy > 3177 or jm < 1 or jm > 12 or jd < 1:
121
+ return False
122
+ return jd <= month_length(jy, jm)
123
+
124
+
125
+ def _j2d(jy, jm, jd):
126
+ _, gy, march, _ = _jal_cal(jy)
127
+ return _g2d(gy, 3, march) + (jm - 1) * 31 - _div(jm, 7) * (jm - 7) + jd - 1
128
+
129
+
130
+ def _d2j(jdn):
131
+ gy, _, _ = _d2g(jdn)
132
+ jy = gy - 621
133
+ leap, _, march, _ = _jal_cal(jy)
134
+ jdn1f = _g2d(gy, 3, march)
135
+ k = jdn - jdn1f
136
+ if k >= 0:
137
+ if k <= 185:
138
+ jm = 1 + _div(k, 31)
139
+ jd = _mod(k, 31) + 1
140
+ return jy, jm, jd
141
+ k -= 186
142
+ else:
143
+ jy -= 1
144
+ k += 179
145
+ if leap == 1:
146
+ k += 1
147
+ jm = 7 + _div(k, 30)
148
+ jd = _mod(k, 30) + 1
149
+ return jy, jm, jd
150
+
151
+
152
+ def _jal_cal(jy):
153
+ bl = len(BREAKS)
154
+ gy = jy + 621
155
+ leap_j = -14
156
+ jp = BREAKS[0]
157
+ jump = 0
158
+
159
+ if jy < jp or jy >= BREAKS[bl - 1]:
160
+ raise ErrorInvalidYear(jy)
161
+
162
+ for i in range(1, bl): # pragma: no branch
163
+ jm = BREAKS[i]
164
+ jump = jm - jp
165
+ if jy < jm:
166
+ break
167
+ leap_j += _div(jump, 33) * 8 + _div(_mod(jump, 33), 4)
168
+ jp = jm
169
+
170
+ n = jy - jp
171
+ leap_j += _div(n, 33) * 8 + _div(_mod(n, 33) + 3, 4)
172
+ if _mod(jump, 33) == 4 and jump - n == 4:
173
+ leap_j += 1
174
+
175
+ leap_g = _div(gy, 4) - _div((_div(gy, 100) + 1) * 3, 4) - 150
176
+ march = 20 + leap_j - leap_g
177
+
178
+ if jump - n < 6:
179
+ n = n - jump + _div(jump + 4, 33) * 33
180
+
181
+ leap = _mod(_mod(n + 1, 33) - 1, 4)
182
+ if leap == -1:
183
+ leap = 4
184
+
185
+ return leap, gy, march, None
186
+
187
+
188
+ def _g2d(gy, gm, gd):
189
+ d = (
190
+ _div((gy + _div(gm - 8, 6) + 100100) * 1461, 4)
191
+ + _div(153 * _mod(gm + 9, 12) + 2, 5)
192
+ + gd
193
+ - 34840408
194
+ )
195
+ d = d - _div(_div(gy + 100100 + _div(gm - 8, 6), 100) * 3, 4) + 752
196
+ return d
197
+
198
+
199
+ def _d2g(jdn):
200
+ j = 4 * jdn + 139361631
201
+ j = j + _div(_div(4 * jdn + 183187720, 146097) * 3, 4) * 4 - 3908
202
+ i = _div(_mod(j, 1461), 4) * 5 + 308
203
+ gd = _div(_mod(i, 153), 5) + 1
204
+ gm = _mod(_div(i, 153), 12) + 1
205
+ gy = _div(j, 1461) - 100100 + _div(8 - gm, 6)
206
+ return gy, gm, gd
207
+
208
+
209
+ # Keep the pure-Python implementations reachable (tests compare them against
210
+ # the C accelerator), then swap in the C versions when they are available.
211
+ _py_to_jalaali = to_jalaali
212
+ _py_to_gregorian = to_gregorian
213
+ _py_is_leap_year = is_leap_year
214
+ _py_month_length = month_length
215
+
216
+ HAS_C_EXTENSION = False
217
+
218
+ if not os.environ.get("DJALAALI_PURE_PYTHON"): # pragma: no cover
219
+ try:
220
+ from . import _calgo
221
+ except ImportError:
222
+ pass
223
+ else:
224
+ _calgo.setup(
225
+ ErrorInvalidYear,
226
+ _py_to_jalaali,
227
+ _py_to_gregorian,
228
+ _py_is_leap_year,
229
+ _py_month_length,
230
+ )
231
+ to_jalaali = _calgo.to_jalaali
232
+ to_gregorian = _calgo.to_gregorian
233
+ is_leap_year = _calgo.is_leap_year
234
+ month_length = _calgo.month_length
235
+ HAS_C_EXTENSION = True