diff --git a/expression-src/main/src/interpreter/std-lib/DateAndTimeFunctions.cls b/expression-src/main/src/interpreter/std-lib/DateAndTimeFunctions.cls index 0e9166b9..7014488b 100644 --- a/expression-src/main/src/interpreter/std-lib/DateAndTimeFunctions.cls +++ b/expression-src/main/src/interpreter/std-lib/DateAndTimeFunctions.cls @@ -519,16 +519,59 @@ public with sharing class DateAndTimeFunctions { Object dateValue = evaluate(arguments.get(0)); if (!(dateValue instanceof Date)) { throw new FunctionExecutionException( - 'Error executing "ISOWEEK" function: the argument must evaluate to a date value.' + 'Error executing "ISOWEEK" function: the argument must evaluate to a date value.' ); } - return Integer.valueOf(Datetime.newInstanceGmt((Date)dateValue, Time.newInstance(0, 0, 0, 0)).format('w')); + return getIsoWeekNumber((Date) dateValue); } public override Arity getArity() { return Arity.exactly(1); } + + // --- ISO WEEK IMPLEMENTATION --- + + private Integer getIsoWeekNumber(Date inputDate) { + Integer isoWeekday = getIsoWeekday(inputDate); // Monday=1 ... Sunday=7 + Integer week = floorDiv(inputDate.dayOfYear() - isoWeekday + 10, 7); + + if (week < 1) { + return getIsoWeeksInYear(inputDate.year() - 1); + } + + Integer weeksInYear = getIsoWeeksInYear(inputDate.year()); + if (week > weeksInYear) { + return 1; + } + + return week; + } + + private Integer getIsoWeeksInYear(Integer year) { + // Dec 28 is always in the last ISO week + Date dec28 = Date.newInstance(year, 12, 28); + Integer isoWeekday = getIsoWeekday(dec28); + return floorDiv(dec28.dayOfYear() - isoWeekday + 10, 7); + } + + private Integer getIsoWeekday(Date inputDate) { + // 1900-01-01 was Monday + Integer diff = Date.newInstance(1900, 1, 1).daysBetween(inputDate); + Integer mod = Math.mod(diff, 7); + if (mod < 0) { + mod += 7; + } + return mod + 1; + } + + private Integer floorDiv(Integer a, Integer b) { + Integer result = a / b; + if (a < 0 && Math.mod(a, b) != 0) { + result--; + } + return result; + } } /** diff --git a/expression-src/spec/language/std-functions/DateAndTimeFunctionsTest.cls b/expression-src/spec/language/std-functions/DateAndTimeFunctionsTest.cls index 66d4b53c..d91185f2 100644 --- a/expression-src/spec/language/std-functions/DateAndTimeFunctionsTest.cls +++ b/expression-src/spec/language/std-functions/DateAndTimeFunctionsTest.cls @@ -84,6 +84,20 @@ private class DateAndTimeFunctionsTest { private static void isoweekFunctionReturnsTheIsoWeek() { Assert.areEqual(1, Evaluator.run('ISOWEEK(DATE(2015, 1, 1))')); Assert.areEqual(52, Evaluator.run('ISOWEEK(DATE(2015, 12, 27))')); + + // --- START OF YEAR EDGE CASES --- + Assert.areEqual(53, Evaluator.run('ISOWEEK(DATE(2016, 1, 1))')); // belongs to 2015 + Assert.areEqual(1, Evaluator.run('ISOWEEK(DATE(2016, 1, 4))')); // first ISO week + + Assert.areEqual(52, Evaluator.run('ISOWEEK(DATE(2017, 1, 1))')); // Sunday → previous year + Assert.areEqual(1, Evaluator.run('ISOWEEK(DATE(2017, 1, 2))')); // Monday → week 1 + + // --- END OF YEAR EDGE CASES --- + Assert.areEqual(53, Evaluator.run('ISOWEEK(DATE(2015, 12, 31))')); // 53-week year + Assert.areEqual(1, Evaluator.run('ISOWEEK(DATE(2014, 12, 31))')); // 2014-12-31 = Wednesday + + Assert.areEqual(1, Evaluator.run('ISOWEEK(DATE(2018, 12, 31))')); // next ISO year + Assert.areEqual(1, Evaluator.run('ISOWEEK(DATE(2019, 12, 30))')); // next ISO year } @IsTest