Skip to content

Commit c8b009e

Browse files
committed
Conversions to and from COM VT_DATE types should no longer lose milliseconds.
Should fix mhammond#1385, fix mhammond#387
1 parent 33b3bb8 commit c8b009e

File tree

4 files changed

+120
-18
lines changed

4 files changed

+120
-18
lines changed

CHANGES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ However contributors are encouraged to add their own entries for their work.
66

77
Since build 225:
88
----------------
9+
* Conversions to and from COM VT_DATE types should no longer lose milliseconds.
910

1011
Since build 224:
1112
----------------

com/win32com/test/testDates.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from __future__ import print_function
2+
3+
from datetime import datetime
4+
import unittest
5+
6+
import pywintypes
7+
import win32com.client
8+
import win32com.test.util
9+
import win32com.server.util
10+
from win32timezone import TimeZoneInfo
11+
12+
# A COM object so we can pass dates to and from the COM boundary.
13+
class Tester:
14+
_public_methods_ = [ 'TestDate' ]
15+
def TestDate(self, d):
16+
assert isinstance(d, datetime)
17+
return d
18+
19+
20+
def test_ob():
21+
return win32com.client.Dispatch(win32com.server.util.wrap(Tester()))
22+
23+
class TestCase(win32com.test.util.TestCase):
24+
def check(self, d, expected = None):
25+
if not issubclass(pywintypes.TimeType, datetime):
26+
self.skipTest("this is testing pywintypes and datetime")
27+
got = test_ob().TestDate(d)
28+
self.assertEqual(got, expected or d)
29+
30+
def testUTC(self):
31+
self.check(datetime(year=2000, month=12, day=25, microsecond=500000, tzinfo=TimeZoneInfo.utc()))
32+
33+
def testLocal(self):
34+
self.check(datetime(year=2000, month=12, day=25, microsecond=500000, tzinfo=TimeZoneInfo.local()))
35+
36+
def testMSTruncated(self):
37+
# milliseconds are kept but microseconds are lost after rounding.
38+
self.check(datetime(year=2000, month=12, day=25, microsecond=500500, tzinfo=TimeZoneInfo.utc()),
39+
datetime(year=2000, month=12, day=25, microsecond=500000, tzinfo=TimeZoneInfo.utc()))
40+
41+
if __name__=='__main__':
42+
unittest.main()

com/win32com/test/testDictionary.py

Lines changed: 31 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
# testDictionary.py
22
#
33
import sys
4+
import datetime
5+
import time
46
import win32com.server.util
57
import win32com.test.util
68
import win32com.client
79
import traceback
810
import pythoncom
911
import pywintypes
1012
import winerror
13+
import win32timezone
1114

1215
import unittest
1316

14-
error = "dictionary test error"
15-
1617
def MakeTestDictionary():
1718
return win32com.client.Dispatch("Python.Dictionary")
1819

1920
def TestDictAgainst(dict,check):
20-
for key, value in check.iteritems():
21+
for key, value in check.items():
2122
if dict(key) != value:
22-
raise error("Indexing for '%s' gave the incorrect value - %s/%s" % (repr(key), repr(dict[key]), repr(check[key])))
23+
raise Exception("Indexing for '%s' gave the incorrect value - %s/%s" % (repr(key), repr(dict[key]), repr(check[key])))
2324

2425
# Ensure we have the correct version registered.
2526
def Register(quiet):
@@ -32,7 +33,7 @@ def TestDict(quiet=None):
3233
quiet = not "-v" in sys.argv
3334
Register(quiet)
3435

35-
if not quiet: print "Simple enum test"
36+
if not quiet: print("Simple enum test")
3637
dict = MakeTestDictionary()
3738
checkDict = {}
3839
TestDictAgainst(dict, checkDict)
@@ -45,31 +46,45 @@ def TestDict(quiet=None):
4546
del checkDict["NewKey"]
4647
TestDictAgainst(dict, checkDict)
4748

49+
if issubclass(pywintypes.TimeType, datetime.datetime):
50+
now = win32timezone.now()
51+
# We want to keep the milliseconds but discard microseconds as they
52+
# don't survive the conversion.
53+
now = now.replace(microsecond = round(now.microsecond / 1000) * 1000)
54+
else:
55+
now = pythoncom.MakeTime(time.gmtime(time.time()))
56+
dict["Now"] = now
57+
checkDict["Now"] = now
58+
TestDictAgainst(dict, checkDict)
59+
4860
if not quiet:
49-
print "Failure tests"
61+
print("Failure tests")
5062
try:
5163
dict()
52-
raise error("default method with no args worked when it shouldnt have!")
53-
except pythoncom.com_error, (hr, desc, exc, argErr):
64+
raise Exception("default method with no args worked when it shouldnt have!")
65+
except pythoncom.com_error as xxx_todo_changeme:
66+
(hr, desc, exc, argErr) = xxx_todo_changeme.args
5467
if hr != winerror.DISP_E_BADPARAMCOUNT:
55-
raise error("Expected DISP_E_BADPARAMCOUNT - got %d (%s)" % (hr, desc))
68+
raise Exception("Expected DISP_E_BADPARAMCOUNT - got %d (%s)" % (hr, desc))
5669

5770
try:
5871
dict("hi", "there")
59-
raise error("multiple args worked when it shouldnt have!")
60-
except pythoncom.com_error, (hr, desc, exc, argErr):
72+
raise Exception("multiple args worked when it shouldnt have!")
73+
except pythoncom.com_error as xxx_todo_changeme1:
74+
(hr, desc, exc, argErr) = xxx_todo_changeme1.args
6175
if hr != winerror.DISP_E_BADPARAMCOUNT:
62-
raise error("Expected DISP_E_BADPARAMCOUNT - got %d (%s)" % (hr, desc))
76+
raise Exception("Expected DISP_E_BADPARAMCOUNT - got %d (%s)" % (hr, desc))
6377

6478
try:
6579
dict(0)
66-
raise error("int key worked when it shouldnt have!")
67-
except pythoncom.com_error, (hr, desc, exc, argErr):
80+
raise Exception("int key worked when it shouldnt have!")
81+
except pythoncom.com_error as xxx_todo_changeme2:
82+
(hr, desc, exc, argErr) = xxx_todo_changeme2.args
6883
if hr != winerror.DISP_E_TYPEMISMATCH:
69-
raise error("Expected DISP_E_TYPEMISMATCH - got %d (%s)" % (hr, desc))
84+
raise Exception("Expected DISP_E_TYPEMISMATCH - got %d (%s)" % (hr, desc))
7085

7186
if not quiet:
72-
print "Python.Dictionary tests complete."
87+
print("Python.Dictionary tests complete.")
7388

7489
class TestCase(win32com.test.util.TestCase):
7590
def testDict(self):

win32/src/PyTime.cpp

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
#include "tchar.h"
1414
#include "math.h"
1515

16+
// Each second as stored in a DATE.
17+
const double ONETHOUSANDMILLISECONDS = 0.00001157407407407407407407407407;
18+
1619
PyObject *PyWin_NewTime(PyObject *timeOb);
1720

1821
BOOL PyWinTime_Check(PyObject *ob)
@@ -737,10 +740,22 @@ BOOL PyWinObject_AsDATE(PyObject *ob, DATE *pDate)
737740
SYSTEMTIME st;
738741
if (!PyWinObject_AsSYSTEMTIME(ob, &st))
739742
return FALSE;
740-
if (!SystemTimeToVariantTime(&st, pDate)) {
743+
// Extra work to get milliseconds, via
744+
// https://www.codeproject.com/Articles/17576/SystemTime-to-VariantTime-with-Milliseconds
745+
WORD wMilliseconds = st.wMilliseconds;
746+
// not clear why we need to zero this since we always seem to get ms ignored
747+
// but...
748+
st.wMilliseconds = 0;
749+
750+
double dWithoutms;
751+
if (!SystemTimeToVariantTime(&st, &dWithoutms)) {
741752
PyWin_SetAPIError("SystemTimeToVariantTime");
742753
return FALSE;
743754
}
755+
// manually convert the millisecond information into variant
756+
// fraction and add it to system converted value
757+
double OneMilliSecond = ONETHOUSANDMILLISECONDS / 1000;
758+
*pDate = dWithoutms + (OneMilliSecond * wMilliseconds);
744759
return TRUE;
745760
}
746761

@@ -956,12 +971,41 @@ PyObject *PyWin_NewTime(PyObject *timeOb)
956971
return new PyTime(t);
957972
#endif
958973
}
974+
975+
#ifdef PYWIN_HAVE_DATETIME_CAPI
976+
static double round(double Value, int Digits)
977+
{
978+
assert(Digits >= -4 && Digits <= 4);
979+
int Idx = Digits + 4;
980+
double v[] = {1e-4, 1e-3, 1e-2, 1e-1, 1, 10, 1e2, 1e3, 1e4};
981+
return floor(Value * v[Idx] + 0.5) / (v[Idx]);
982+
}
983+
#endif
984+
959985
PyObject *PyWinObject_FromDATE(DATE t)
960986
{
961987
#ifdef PYWIN_HAVE_DATETIME_CAPI
988+
// via https://www.codeproject.com/Articles/17576/SystemTime-to-VariantTime-with-Milliseconds
989+
// (in particular, see the comments)
990+
double fraction = t - (int)t; // extracts the fraction part
991+
double hours = (fraction - (int)fraction) * 24.0;
992+
double minutes = (hours - (int)hours) * 60.0;
993+
double seconds = round((minutes - (int)minutes) * 60.0, 4);
994+
double milliseconds = round((seconds - (int)seconds) * 1000.0, 0);
995+
// assert(milliseconds>=0.0 && milliseconds<=999.0);
996+
997+
// Strip off the msec part of time
998+
double TimeWithoutMsecs = t - (ONETHOUSANDMILLISECONDS / 1000.0 * milliseconds);
999+
1000+
// Let the OS translate the variant date/time
9621001
SYSTEMTIME st;
963-
if (!VariantTimeToSystemTime(t, &st))
1002+
if (!VariantTimeToSystemTime(TimeWithoutMsecs, &st)) {
9641003
return PyWin_SetAPIError("VariantTimeToSystemTime");
1004+
}
1005+
if (milliseconds > 0.0) {
1006+
// add the msec part to the systemtime object
1007+
st.wMilliseconds = (WORD)milliseconds;
1008+
}
9651009
return PyWinObject_FromSYSTEMTIME(st);
9661010
#endif // PYWIN_HAVE_DATETIME_CAPI
9671011

0 commit comments

Comments
 (0)