Repository: slashmili/jdatetime

Issue:
strptime Mismatch Error When Parsing Hijri Dates
I encountered an issue when trying to use jdatetime.datetime.strptime to parse a date string in the Hijri calendar. Specifically, I tried to parse the following date:
```
date = 'Friday, 28 10 1403 - 12:03'
jdatetime.datetime.strptime(date, '%A, %d %m %Y - %H:%M')`
```

> ValueError: time data 'Friday, 28 10 1403 - 12:03' does not match format '%A, %d %m %Y - %H:%M'

However, I received a mismatch error. I confirmed that my format string ('%A, %d %m %Y - %H:%M') is correct because it works perfectly with Python's built-in datetime module. Here’s an example:
```

from datetime import datetime
date = 'Friday, 28 10 1403 - 12:03'
parsed_date = datetime.strptime(date, '%A, %d %m %Y - %H:%M')
parsed_date`
```
This works without issues in both Python 3.12 and Python 3.10.6. However, when using jdatetime, the same format string raises a mismatch error.

Hints from issue comments:
@5j9: This is most likely because `%A` is not defined in `_DIRECTIVE_PATTERNS`:
https://github.com/slashmili/python-jalali/blob/9e077064e56a821e6a6cd7a4b29c95bf364af9d0/jdatetime/__init__.py#L624-L636
(There are also several others that I can see are not defined there, like `%p`.)@slashmili: Thanks for reporting the issue. Just want to point out that `%A%` in `strptime` is not useful: 

```python
>>> import datetime
>>> datetime.datetime.strptime  ('Monday, 20 01 2025 - 12:03', '%A, %d %m %Y - %H:%M')
datetime.datetime(2025, 1, 20, 12, 3)
>>> datetime.datetime.strptime('Friday, 20 01 2025 - 12:03', '%A, %d %m %Y - %H:%M')
datetime.datetime(2025, 1, 20, 12, 3)
```

We can implement it. However I think we should just ignore the value?@5j9: Actually, in Python's built-in `datetime.strptime`, `%A` isn't just for syntax validation; it does matter in specific scenarios, for example, when the week number is given:

```python
>>> import datetime
>>> datetime.datetime.strptime('Friday 01 2203 - 12:03', '%A %W %Y - %H:%M')
datetime.datetime(2203, 1, 7, 12, 3)
>>> datetime.datetime.strptime('Saturday 01 2203 - 12:03', '%A %W %Y - %H:%M')
datetime.datetime(2203, 1, 8, 12, 3)
```

However, implementing that in `jdatetime` might get overly complicated, and its use cases are very rare.@saeedmie85: Thank you both for your detailed explanations and insights into the issue. I appreciate the clarification regarding the %A directive and its current limitations in jdatetime.

 I understand that %Ais not currently implemented in jdatetime, which explains the mismatch error I encountered. However, since %A is part of the standard strptime format in Python's datetime module, it would be helpful to have it supported in jdatetime` for consistency, especially when parsing date strings that include the day of the week.

While it's true that %A doesn't affect the parsed date value in most cases (as shown in your example), it can still be useful for validation purposes. For instance, if the day of the week in the input string doesn't match the actual date, it could indicate an error in the input data. Additionally, as pointed out,%A does matter in specific scenarios, such as when combined with %W (week number).

Proposal for implementation:
I agree that implementing %A in jdatetime might add some complexity, especially for rare use cases. However, I think it would be valuable to at least support it for basic validation (e.g., ensuring the day of the week matches the date) without necessarily handling more advanced cases like %A with %W. This would maintain consistency with Python's datetime and improve usability for those working with date strings that include the day of the week.

If implementing %A fully is too complex, perhaps a simpler approach could be to allow %A in the format string but ignore its value during parsing (as suggested). This would at least prevent the mismatch error and allow users to parse date strings that include the day of the week, even if the day itself isn't validated.

Thank you again for your time and consideration.@slashmili: Thanks to @5j9, it's resolved in 5.2.0

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc2e30e..ec2c278 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,11 @@
 # Changelog
 
+## [5.1.1] - 2025-01-26
+
+### Fixed
+
+* Fix __ne__ for different datetime types #164
+
 ## [5.1.0] - 2025-01-13
 
 ### Fixed
diff --git a/jdatetime/__init__.py b/jdatetime/__init__.py
index cf3641c..0534e70 100644
--- a/jdatetime/__init__.py
+++ b/jdatetime/__init__.py
@@ -16,7 +16,7 @@ except ImportError:
 
 from jalali_core import GregorianToJalali, JalaliToGregorian, j_days_in_month
 
-__VERSION__ = "5.1.0"
+__VERSION__ = "5.1.1"
 MINYEAR = 1
 MAXYEAR = 9377
 
@@ -481,17 +481,6 @@ class date:
 
         return not self.__ge__(other_date)
 
-    def __ne__(self, other_date):
-        """x.__ne__(y) <==> x!=y"""
-        if other_date is None:
-            return True
-        if isinstance(other_date, py_datetime.date):
-            return self.__ne__(date.fromgregorian(date=other_date))
-        if not isinstance(other_date, date):
-            return NotImplemented
-
-        return not self.__eq__(other_date)
-
     def __hash__(self):
         """x.__hash__() <==> hash(x)"""
         gd = self.togregorian()
@@ -622,12 +611,16 @@ date.min = date(MINYEAR, 1, 1)
 date.max = date(MAXYEAR, 12, 30)
 
 _DIRECTIVE_PATTERNS = {
+    '%A': '(?:' + '|'.join([*date.j_weekdays_fa, *date.j_weekdays_en]) + ')',
+    '%a': '(?:' + '|'.join([*date.j_weekdays_fa, *date.j_weekdays_short_en]) + ')',
     '%Y': r'(?P<Y>\d{4})',
     '%y': r'(?P<y>\d{2})',
     '%m': r'(?P<m>\d{1,2})',
     '%d': r'(?P<d>\d{1,2})',
     '%H': r'(?P<H>\d{1,2})',
+    '%j': r'[0-3]\d\d',
     '%M': r'(?P<M>\d{1,2})',
+    '%p': '(?:' + '|'.join([*date.j_ampm_fa.values(), *date.j_ampm_en.values()]) + ')',
     '%S': r'(?P<S>\d{1,2})',
     '%f': r'(?P<f>\d{1,6})',
     '%B': r'(?P<B>[a-zA-Z\u0600-\u06EF\uFB8A\u067E\u0686\u06AF]{2,12})',
diff --git a/setup.py b/setup.py
index 2ab22b2..9b6c952 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@ import setuptools
 
 setup(
     name='jdatetime',
-    version='5.1.0',
+    version='5.1.1',
     packages=['jdatetime', ],
     license='Python Software Foundation License',
     keywords='Jalali implementation of Python datetime',


Test patch:
diff --git a/tests/test_jdatetime.py b/tests/test_jdatetime.py
index f8b82cf..997a89b 100644
--- a/tests/test_jdatetime.py
+++ b/tests/test_jdatetime.py
@@ -436,6 +436,14 @@ class TestJDateTime(TestCase):
                 with self.assertRaises(ValueError, msg=msg):
                     jdatetime.datetime.strptime(date_string, date_format)
 
+    def test_strptime_A_p_j_directives(self):
+        dt = jdatetime.datetime(1401, 2, 3, 4)
+        fmt = '%Y %m %d %H %j %p %A %a'
+        self.assertEqual(
+            jdatetime.datetime.strptime(dt.strftime(fmt), fmt),
+            dt,
+        )
+
     def test_timetz(self):
         teh = TehranTime()
 
@@ -763,6 +771,71 @@ class TestJdatetimeComparison(TestCase):
         dt2 = "not a datetime object"
         self.assertFalse(dt1 == dt2)
 
+    # __ne__
+    def test_ne_different_dates(self):
+        dt1 = jdatetime.datetime(1403, 1, 1, 0, 0, 0)
+        dt2 = jdatetime.datetime(1403, 1, 2, 0, 0, 0)
+        self.assertTrue(dt1 != dt2)
+
+    def test_neq_different_times(self):
+        dt1 = jdatetime.datetime(1403, 1, 1, 12, 0, 0)
+        dt2 = jdatetime.datetime(1403, 1, 1, 13, 0, 0)
+        self.assertTrue(dt1 != dt2)
+
+    def test_neq_different_timezones(self):
+        gmt = GMTTime()
+        teh = TehranTime()
+
+        dt1 = jdatetime.datetime(1403, 1, 1, 12, 0, 0, tzinfo=teh)
+        dt2 = jdatetime.datetime(1403, 1, 1, 12, 0, 0, tzinfo=gmt)
+        self.assertTrue(dt1 != dt2)
+
+    def test_neq_same_datetime(self):
+        dt1 = jdatetime.datetime(1403, 1, 1, 12, 0, 0)
+        dt2 = jdatetime.datetime(1403, 1, 1, 12, 0, 0)
+        self.assertFalse(dt1 != dt2)
+
+    def test_neq_different_types(self):
+        dt1 = jdatetime.datetime(1403, 1, 1, 12, 0, 0)
+        self.assertTrue(dt1 != "1403-01-01 12:00:00")
+
+    def test_neq_with_none(self):
+        dt1 = jdatetime.datetime(1403, 1, 1, 12, 0, 0)
+        self.assertTrue(dt1.__ne__(None))
+
+    def test_neq_different_datetime_types(self):
+        dt1 = jdatetime.datetime(1403, 1, 1, 12, 0, 0)
+        dt2 = dt1.togregorian()
+        self.assertFalse(dt1 != dt2)
+
+        # different hour
+        dt2 = dt1.togregorian() + datetime.timedelta(hours=1)
+        self.assertTrue(dt1 != dt2)
+        dt2 = dt1.togregorian() - datetime.timedelta(hours=1)
+        self.assertTrue(dt1 != dt2)
+
+        # different day
+        dt2 = dt1.togregorian() + datetime.timedelta(days=1)
+        self.assertTrue(dt1 != dt2)
+        dt2 = dt1.togregorian() - datetime.timedelta(days=1)
+        self.assertTrue(dt1 != dt2)
+
+        # different month
+        dt2 = dt1.togregorian() + datetime.timedelta(days=31)
+        self.assertTrue(dt1 != dt2)
+        dt2 = dt1.togregorian() - datetime.timedelta(days=31)
+        self.assertTrue(dt1 != dt2)
+
+        # different year
+        dt2 = dt1.togregorian() + datetime.timedelta(days=370)
+        self.assertTrue(dt1 != dt2)
+        dt2 = dt1.togregorian() - datetime.timedelta(days=370)
+        self.assertTrue(dt1 != dt2)
+
+    def test_neq_not_implemented(self):
+        dt1 = jdatetime.datetime(1403, 1, 1, 12, 0, 0)
+        self.assertEqual(dt1.__ne__("not datetime object"), NotImplemented)
+
     # __ge__
     def test_ge_with_same_datetime(self):
         dt1 = jdatetime.datetime(1402, 7, 8, 12, 0, 0)