Skip to content

Commit

Permalink
Conversions to and from COM VT_DATE types should no longer lose milli…
Browse files Browse the repository at this point in the history
…seconds.

Should fix mhammond#1385, fix mhammond#387
  • Loading branch information
mhammond committed Nov 5, 2019
1 parent 33b3bb8 commit c8b009e
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 18 deletions.
1 change: 1 addition & 0 deletions CHANGES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ However contributors are encouraged to add their own entries for their work.

Since build 225:
----------------
* Conversions to and from COM VT_DATE types should no longer lose milliseconds.

Since build 224:
----------------
Expand Down
42 changes: 42 additions & 0 deletions com/win32com/test/testDates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from __future__ import print_function

from datetime import datetime
import unittest

import pywintypes
import win32com.client
import win32com.test.util
import win32com.server.util
from win32timezone import TimeZoneInfo

# A COM object so we can pass dates to and from the COM boundary.
class Tester:
_public_methods_ = [ 'TestDate' ]
def TestDate(self, d):
assert isinstance(d, datetime)
return d


def test_ob():
return win32com.client.Dispatch(win32com.server.util.wrap(Tester()))

class TestCase(win32com.test.util.TestCase):
def check(self, d, expected = None):
if not issubclass(pywintypes.TimeType, datetime):
self.skipTest("this is testing pywintypes and datetime")
got = test_ob().TestDate(d)
self.assertEqual(got, expected or d)

def testUTC(self):
self.check(datetime(year=2000, month=12, day=25, microsecond=500000, tzinfo=TimeZoneInfo.utc()))

def testLocal(self):
self.check(datetime(year=2000, month=12, day=25, microsecond=500000, tzinfo=TimeZoneInfo.local()))

def testMSTruncated(self):
# milliseconds are kept but microseconds are lost after rounding.
self.check(datetime(year=2000, month=12, day=25, microsecond=500500, tzinfo=TimeZoneInfo.utc()),
datetime(year=2000, month=12, day=25, microsecond=500000, tzinfo=TimeZoneInfo.utc()))

if __name__=='__main__':
unittest.main()
47 changes: 31 additions & 16 deletions com/win32com/test/testDictionary.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
# testDictionary.py
#
import sys
import datetime
import time
import win32com.server.util
import win32com.test.util
import win32com.client
import traceback
import pythoncom
import pywintypes
import winerror
import win32timezone

import unittest

error = "dictionary test error"

def MakeTestDictionary():
return win32com.client.Dispatch("Python.Dictionary")

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

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

if not quiet: print "Simple enum test"
if not quiet: print("Simple enum test")
dict = MakeTestDictionary()
checkDict = {}
TestDictAgainst(dict, checkDict)
Expand All @@ -45,31 +46,45 @@ def TestDict(quiet=None):
del checkDict["NewKey"]
TestDictAgainst(dict, checkDict)

if issubclass(pywintypes.TimeType, datetime.datetime):
now = win32timezone.now()
# We want to keep the milliseconds but discard microseconds as they
# don't survive the conversion.
now = now.replace(microsecond = round(now.microsecond / 1000) * 1000)
else:
now = pythoncom.MakeTime(time.gmtime(time.time()))
dict["Now"] = now
checkDict["Now"] = now
TestDictAgainst(dict, checkDict)

if not quiet:
print "Failure tests"
print("Failure tests")
try:
dict()
raise error("default method with no args worked when it shouldnt have!")
except pythoncom.com_error, (hr, desc, exc, argErr):
raise Exception("default method with no args worked when it shouldnt have!")
except pythoncom.com_error as xxx_todo_changeme:
(hr, desc, exc, argErr) = xxx_todo_changeme.args
if hr != winerror.DISP_E_BADPARAMCOUNT:
raise error("Expected DISP_E_BADPARAMCOUNT - got %d (%s)" % (hr, desc))
raise Exception("Expected DISP_E_BADPARAMCOUNT - got %d (%s)" % (hr, desc))

try:
dict("hi", "there")
raise error("multiple args worked when it shouldnt have!")
except pythoncom.com_error, (hr, desc, exc, argErr):
raise Exception("multiple args worked when it shouldnt have!")
except pythoncom.com_error as xxx_todo_changeme1:
(hr, desc, exc, argErr) = xxx_todo_changeme1.args
if hr != winerror.DISP_E_BADPARAMCOUNT:
raise error("Expected DISP_E_BADPARAMCOUNT - got %d (%s)" % (hr, desc))
raise Exception("Expected DISP_E_BADPARAMCOUNT - got %d (%s)" % (hr, desc))

try:
dict(0)
raise error("int key worked when it shouldnt have!")
except pythoncom.com_error, (hr, desc, exc, argErr):
raise Exception("int key worked when it shouldnt have!")
except pythoncom.com_error as xxx_todo_changeme2:
(hr, desc, exc, argErr) = xxx_todo_changeme2.args
if hr != winerror.DISP_E_TYPEMISMATCH:
raise error("Expected DISP_E_TYPEMISMATCH - got %d (%s)" % (hr, desc))
raise Exception("Expected DISP_E_TYPEMISMATCH - got %d (%s)" % (hr, desc))

if not quiet:
print "Python.Dictionary tests complete."
print("Python.Dictionary tests complete.")

class TestCase(win32com.test.util.TestCase):
def testDict(self):
Expand Down
48 changes: 46 additions & 2 deletions win32/src/PyTime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@
#include "tchar.h"
#include "math.h"

// Each second as stored in a DATE.
const double ONETHOUSANDMILLISECONDS = 0.00001157407407407407407407407407;

PyObject *PyWin_NewTime(PyObject *timeOb);

BOOL PyWinTime_Check(PyObject *ob)
Expand Down Expand Up @@ -737,10 +740,22 @@ BOOL PyWinObject_AsDATE(PyObject *ob, DATE *pDate)
SYSTEMTIME st;
if (!PyWinObject_AsSYSTEMTIME(ob, &st))
return FALSE;
if (!SystemTimeToVariantTime(&st, pDate)) {
// Extra work to get milliseconds, via
// https://www.codeproject.com/Articles/17576/SystemTime-to-VariantTime-with-Milliseconds
WORD wMilliseconds = st.wMilliseconds;
// not clear why we need to zero this since we always seem to get ms ignored
// but...
st.wMilliseconds = 0;

double dWithoutms;
if (!SystemTimeToVariantTime(&st, &dWithoutms)) {
PyWin_SetAPIError("SystemTimeToVariantTime");
return FALSE;
}
// manually convert the millisecond information into variant
// fraction and add it to system converted value
double OneMilliSecond = ONETHOUSANDMILLISECONDS / 1000;
*pDate = dWithoutms + (OneMilliSecond * wMilliseconds);
return TRUE;
}

Expand Down Expand Up @@ -956,12 +971,41 @@ PyObject *PyWin_NewTime(PyObject *timeOb)
return new PyTime(t);
#endif
}

#ifdef PYWIN_HAVE_DATETIME_CAPI
static double round(double Value, int Digits)
{
assert(Digits >= -4 && Digits <= 4);
int Idx = Digits + 4;
double v[] = {1e-4, 1e-3, 1e-2, 1e-1, 1, 10, 1e2, 1e3, 1e4};
return floor(Value * v[Idx] + 0.5) / (v[Idx]);
}
#endif

PyObject *PyWinObject_FromDATE(DATE t)
{
#ifdef PYWIN_HAVE_DATETIME_CAPI
// via https://www.codeproject.com/Articles/17576/SystemTime-to-VariantTime-with-Milliseconds
// (in particular, see the comments)
double fraction = t - (int)t; // extracts the fraction part
double hours = (fraction - (int)fraction) * 24.0;
double minutes = (hours - (int)hours) * 60.0;
double seconds = round((minutes - (int)minutes) * 60.0, 4);
double milliseconds = round((seconds - (int)seconds) * 1000.0, 0);
// assert(milliseconds>=0.0 && milliseconds<=999.0);

// Strip off the msec part of time
double TimeWithoutMsecs = t - (ONETHOUSANDMILLISECONDS / 1000.0 * milliseconds);

// Let the OS translate the variant date/time
SYSTEMTIME st;
if (!VariantTimeToSystemTime(t, &st))
if (!VariantTimeToSystemTime(TimeWithoutMsecs, &st)) {
return PyWin_SetAPIError("VariantTimeToSystemTime");
}
if (milliseconds > 0.0) {
// add the msec part to the systemtime object
st.wMilliseconds = (WORD)milliseconds;
}
return PyWinObject_FromSYSTEMTIME(st);
#endif // PYWIN_HAVE_DATETIME_CAPI

Expand Down

0 comments on commit c8b009e

Please sign in to comment.