[PATCH 0/1] MR10456: vbscript: Implement DatePart built-in function.
Implement DatePart(interval, date [, firstdayofweek [, firstweekofyear]]) with support for all interval types: yyyy, q, m, y, d, w, ww, h, n, s. The week-of-year ("ww") calculation handles all three firstweekofyear modes (vbFirstJan1, vbFirstFourDays, vbFirstFullWeek) including the edge case where dates before week 1 belong to the last week of the previous year. Uses VarUdateFromDate for day-of-year information. -- https://gitlab.winehq.org/wine/wine/-/merge_requests/10456
From: Francis De Brabandere <francisdb@gmail.com> Implement DatePart(interval, date [, firstdayofweek [, firstweekofyear]]) with support for all interval types: yyyy, q, m, y, d, w, ww, h, n, s. The week-of-year ("ww") calculation handles all three firstweekofyear modes (vbFirstJan1, vbFirstFourDays, vbFirstFullWeek) including the edge case where dates before week 1 belong to the last week of the previous year. Uses VarUdateFromDate for day-of-year information. --- dlls/vbscript/global.c | 163 +++++++++++++++++++++++++++++++++++- dlls/vbscript/tests/api.vbs | 107 +++++++++++++++++++++++ 2 files changed, 267 insertions(+), 3 deletions(-) diff --git a/dlls/vbscript/global.c b/dlls/vbscript/global.c index f38ff895ef7..1cab2b0cfc2 100644 --- a/dlls/vbscript/global.c +++ b/dlls/vbscript/global.c @@ -2463,10 +2463,167 @@ static HRESULT Global_DateDiff(BuiltinDisp *This, VARIANT *arg, unsigned args_cn return E_NOTIMPL; } -static HRESULT Global_DatePart(BuiltinDisp *This, VARIANT *arg, unsigned args_cnt, VARIANT *res) +static HRESULT Global_DatePart(BuiltinDisp *This, VARIANT *args, unsigned args_cnt, VARIANT *res) { - FIXME("\n"); - return E_NOTIMPL; + BSTR interval = NULL; + int firstday = 0, firstweek = 0; + UDATE ud; + DATE date; + VARIANT date_var; + HRESULT hres; + int result; + + TRACE("\n"); + + assert(args_cnt >= 2 && args_cnt <= 4); + + if(V_VT(args) == VT_NULL) + return MAKE_VBSERROR(VBSE_ILLEGAL_NULL_USE); + + if(args_cnt >= 3) + { + if(V_VT(args + 2) == VT_NULL) + return MAKE_VBSERROR(VBSE_ILLEGAL_NULL_USE); + hres = to_int(args + 2, &firstday); + if(FAILED(hres)) + return hres; + if(firstday < 0 || firstday > 7) + return MAKE_VBSERROR(VBSE_ILLEGAL_FUNC_CALL); + } + + if(args_cnt >= 4) + { + if(V_VT(args + 3) == VT_NULL) + return MAKE_VBSERROR(VBSE_ILLEGAL_NULL_USE); + hres = to_int(args + 3, &firstweek); + if(FAILED(hres)) + return hres; + if(firstweek < 0 || firstweek > 3) + return MAKE_VBSERROR(VBSE_ILLEGAL_FUNC_CALL); + } + + if(V_VT(args + 1) == VT_NULL) + return return_null(res); + + hres = to_string(args, &interval); + if(FAILED(hres)) + return hres; + + V_VT(&date_var) = VT_EMPTY; + hres = VariantChangeType(&date_var, args + 1, 0, VT_DATE); + if(FAILED(hres)) + { + SysFreeString(interval); + return hres; + } + date = V_DATE(&date_var); + + hres = VarUdateFromDate(date, 0, &ud); + if(FAILED(hres)) + { + SysFreeString(interval); + return hres; + } + + /* Resolve firstdayofweek: 0 = system default, 1-7 = vbSunday-vbSaturday */ + if(!firstday) + { + GetLocaleInfoW(This->ctx->lcid, LOCALE_RETURN_NUMBER | LOCALE_IFIRSTDAYOFWEEK, + (LPWSTR)&firstday, sizeof(firstday) / sizeof(WCHAR)); + firstday = (firstday + 1) % 7; + } + else + { + firstday--; + } + + /* Resolve firstweekofyear: 0 = system default */ + if(!firstweek) + { + GetLocaleInfoW(This->ctx->lcid, LOCALE_RETURN_NUMBER | LOCALE_IFIRSTWEEKOFYEAR, + (LPWSTR)&firstweek, sizeof(firstweek) / sizeof(WCHAR)); + firstweek++; + } + + if(!wcsicmp(interval, L"yyyy")) + result = ud.st.wYear; + else if(!wcsicmp(interval, L"q")) + result = (ud.st.wMonth - 1) / 3 + 1; + else if(!wcsicmp(interval, L"m")) + result = ud.st.wMonth; + else if(!wcsicmp(interval, L"y")) + result = ud.wDayOfYear; + else if(!wcsicmp(interval, L"d")) + result = ud.st.wDay; + else if(!wcsicmp(interval, L"w")) + result = 1 + (ud.st.wDayOfWeek - firstday + 7) % 7; + else if(!wcsicmp(interval, L"ww")) + { + int jan1_wday, jan1_pos, week1_start; + + /* Day of week for Jan 1 of this year */ + jan1_wday = (ud.st.wDayOfWeek - (ud.wDayOfYear - 1) % 7 + 7) % 7; + /* Position of Jan 1 within its week (0 = starts week, 6 = ends week) */ + jan1_pos = (jan1_wday - firstday + 7) % 7; + + switch(firstweek) + { + case 1: /* vbFirstJan1: week containing Jan 1 is week 1 */ + default: + week1_start = 1 - jan1_pos; + break; + case 2: /* vbFirstFourDays: first week has >= 4 days in year */ + week1_start = (jan1_pos <= 3) ? 1 - jan1_pos : 8 - jan1_pos; + break; + case 3: /* vbFirstFullWeek: first week is entirely in year */ + week1_start = (jan1_pos == 0) ? 1 : 8 - jan1_pos; + break; + } + + if(ud.wDayOfYear >= week1_start) + { + result = (ud.wDayOfYear - week1_start) / 7 + 1; + } + else + { + /* Date falls before week 1 — belongs to last week of previous year */ + int prev_year = ud.st.wYear - 1; + int prev_days = (prev_year % 4 == 0 && (prev_year % 100 != 0 || prev_year % 400 == 0)) ? 366 : 365; + int jan1_prev_wday = (jan1_wday - prev_days % 7 + 7) % 7; + int jan1_prev_pos = (jan1_prev_wday - firstday + 7) % 7; + int prev_week1_start; + + switch(firstweek) + { + case 1: + default: + prev_week1_start = 1 - jan1_prev_pos; + break; + case 2: + prev_week1_start = (jan1_prev_pos <= 3) ? 1 - jan1_prev_pos : 8 - jan1_prev_pos; + break; + case 3: + prev_week1_start = (jan1_prev_pos == 0) ? 1 : 8 - jan1_prev_pos; + break; + } + result = (prev_days - prev_week1_start) / 7 + 1; + } + } + else if(!wcsicmp(interval, L"h")) + result = ud.st.wHour; + else if(!wcsicmp(interval, L"n")) + result = ud.st.wMinute; + else if(!wcsicmp(interval, L"s")) + result = ud.st.wSecond; + else + { + WARN("Unrecognized interval %s.\n", debugstr_w(interval)); + SysFreeString(interval); + return MAKE_VBSERROR(VBSE_ILLEGAL_FUNC_CALL); + } + + SysFreeString(interval); + return return_short(res, result); } static HRESULT Global_TypeName(BuiltinDisp *This, VARIANT *arg, unsigned args_cnt, VARIANT *res) diff --git a/dlls/vbscript/tests/api.vbs b/dlls/vbscript/tests/api.vbs index e18d887306b..a911d982e5d 100644 --- a/dlls/vbscript/tests/api.vbs +++ b/dlls/vbscript/tests/api.vbs @@ -2342,6 +2342,113 @@ call testDateAdd(DateSerial(2000, 1, 1), "ww", -1, DateSerial(1999, 12, 25)) call testDateAdd(DateSerial(2000, 1, 1), "Ww", -1, DateSerial(1999, 12, 25)) call testDateAddError() +sub testDatePart(interval, d, expected) + dim x + x = DatePart(interval, d) + call ok(x = expected, "DatePart(""" & interval & """, " & d & ") = " & x & " expected " & expected) + call ok(getVT(x) = "VT_I2*", "getVT = " & getVT(x)) +end sub + +sub testDatePartFdow(interval, d, fdow, expected) + dim x + x = DatePart(interval, d, fdow) + call ok(x = expected, "DatePart(""" & interval & """, " & d & ", " & fdow & ") = " & x & " expected " & expected) +end sub + +sub testDatePartFull(interval, d, fdow, fwoy, expected) + dim x + x = DatePart(interval, d, fdow, fwoy) + call ok(x = expected, "DatePart(""" & interval & """, " & d & ", " & fdow & ", " & fwoy & ") = " & x & " expected " & expected) +end sub + +dim datepartDate +datepartDate = DateSerial(2000, 3, 15) + TimeSerial(14, 30, 45) + +' Basic interval tests +call testDatePart("yyyy", datepartDate, 2000) +call testDatePart("q", datepartDate, 1) +call testDatePart("m", datepartDate, 3) +call testDatePart("y", datepartDate, 75) +call testDatePart("d", datepartDate, 15) +call testDatePart("w", datepartDate, 4) +call testDatePart("ww", datepartDate, 12) +call testDatePart("h", datepartDate, 14) +call testDatePart("n", datepartDate, 30) +call testDatePart("s", datepartDate, 45) + +' Case insensitive +call testDatePart("YYYY", datepartDate, 2000) +call testDatePart("Q", datepartDate, 1) + +' Quarter boundaries +call testDatePart("q", DateSerial(2000, 1, 1), 1) +call testDatePart("q", DateSerial(2000, 3, 31), 1) +call testDatePart("q", DateSerial(2000, 4, 1), 2) +call testDatePart("q", DateSerial(2000, 6, 30), 2) +call testDatePart("q", DateSerial(2000, 7, 1), 3) +call testDatePart("q", DateSerial(2000, 9, 30), 3) +call testDatePart("q", DateSerial(2000, 10, 1), 4) +call testDatePart("q", DateSerial(2000, 12, 31), 4) + +' Day of year +call testDatePart("y", DateSerial(2000, 1, 1), 1) +call testDatePart("y", DateSerial(2000, 12, 31), 366) +call testDatePart("y", DateSerial(2000, 3, 1), 61) + +' Weekday with firstdayofweek (2000-01-01 is Saturday) +call testDatePartFdow("w", DateSerial(2000, 1, 1), vbSunday, 7) +call testDatePartFdow("w", DateSerial(2000, 1, 1), vbMonday, 6) +call testDatePartFdow("w", DateSerial(2000, 1, 1), vbSaturday, 1) + +' Week of year with firstdayofweek and firstweekofyear +call testDatePartFull("ww", DateSerial(2000, 1, 1), vbSunday, vbFirstJan1, 1) +call testDatePartFull("ww", DateSerial(2000, 1, 1), vbMonday, vbFirstJan1, 1) +call testDatePartFull("ww", DateSerial(2000, 1, 1), vbSunday, vbFirstFourDays, 52) +call testDatePartFull("ww", DateSerial(2000, 1, 1), vbSunday, vbFirstFullWeek, 52) + +sub testDatePartError() + on error resume next + dim x + + ' Null date returns Null + err.clear + x = DatePart("yyyy", null) + call ok(getVT(x) = "VT_NULL*", "null date getVT = " & getVT(x)) + call ok(err.number = 0, "null date err = " & err.number) + + ' Null interval is error 94 + err.clear + x = DatePart(null, datepartDate) + call ok(err.number = 94, "null interval err = " & err.number) + + ' Invalid interval is error 5 + err.clear + x = DatePart("k", datepartDate) + call ok(err.number = 5, "invalid interval err = " & err.number) + + ' String date conversion + err.clear + x = DatePart("yyyy", "2000-03-15") + call ok(x = 2000, "string date = " & x) + call ok(err.number = 0, "string date err = " & err.number) + + ' Invalid firstdayofweek + err.clear + x = DatePart("w", datepartDate, 8) + call ok(err.number = 5, "fdow=8 err = " & err.number) + + err.clear + x = DatePart("w", datepartDate, -1) + call ok(err.number = 5, "fdow=-1 err = " & err.number) + + ' Invalid firstweekofyear + err.clear + x = DatePart("ww", datepartDate, vbSunday, 4) + call ok(err.number = 5, "fwoy=4 err = " & err.number) +end sub + +call testDatePartError() + sub testWeekday(d, firstday, wd) dim x, x2 x = Weekday(d, firstday) -- GitLab https://gitlab.winehq.org/wine/wine/-/merge_requests/10456
participants (2)
-
Francis De Brabandere -
Francis De Brabandere (@francisdb)