Вот версия, которая делает что-то похожее на алгоритм, который вы описываете. Я понятия не имею, очищает ли он код для вас или он работает лучше:
// General-purpose utility functions
const range = (lo, hi) =>
[... Array (hi - lo + 1)] .map ((_, i) => lo + i)
const parseDate = (s, [y, m, d] = s.split('-') .map (Number)) =>
[y, m -1 , d] // m - 1 because JS Dates are screwy about months
const isLeapYear = (y) =>
(y % 4 == 0) && (y % 100 != 0 || y % 400 == 0)
const daysInMonth = (y, m) =>
isLeapYear (y)
? [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] [m]
: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] [m]
// Helper functions
const fullMonthsBetween = (y1, m1, y2, m2) => // excludes both endpoints
Math.max((12 * y2 + m2) - (12 * y1 + m1) - 1, 0)
const workingDaysInRemainderOfMonth = (y, m, d, day = new Date (y, m, d) .getDay ()) =>
range (d, daysInMonth (y, m))
.filter ((_, i) => [1, 2, 3, 4, 5] .includes((day + i) % 7))
.length
const workingDaysInStartOfMonth = (y, m, d) =>
range (1, d)
.filter ((_, i) => [1, 2, 3, 4, 5] .includes ((d + i) % 7))
.length
// Main function
const workingMonthsBetween = (start, end) => {
const [y1, m1, d1] = parseDate (start)
const [y2, m2, d2] = parseDate (end)
return fullMonthsBetween (y1, m1, y2, m2)
+ workingDaysInRemainderOfMonth (y1, m1, d1)
/ workingDaysInRemainderOfMonth(y1, m1, 1)
+ workingDaysInStartOfMonth (y2, m2, d2)
/ workingDaysInRemainderOfMonth(y2, m2, 1)
}
// Demo
console .log (
workingMonthsBetween ('2020-05-20', '2021-08-12') //~> 14.744588744588745
)
Здесь есть несколько небольших функций. Большинство может быть встроено для улучшения производительности, но я предпочитаю работать со многими маленькими вспомогательными функциями, чем с большим монолитом, поэтому я оставлю это вам.
range
создает целочисленный диапазон. range (3, 12) //=> [3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
parseDate
преобразует строку типа '2020-08-12'
в поля года / месяца / дня [2010, 7, 12]
. Это не опечатка. Остальная часть обработки JS даты использует месяц с индексом 0, поэтому мы вычитаем один здесь.
isLeapYear
должно быть очевидным, хотя високосный год правила немного сложны
daysInMonth
принимает год и месяц и возвращает общее количество дней в месяце с учетом високосного года
fullMonthsBetween
сообщает количество месяцев строго между двумя комбинациями год / месяц. Таким образом, между 2020-05 и 2020-09, есть три месяца, июнь, июль и август.
workingDaysInRemainderOfMonth
занимает год, месяц и date и сообщает, сколько рабочих дней осталось в этом месяце (включая нашу указанную дату и последний день месяца). Это выполняется путем фильтрации всех суббот (6 мод. 7) и воскресений (0 мод. 7) из диапазона дней между этой датой и последним днем месяца. Возможно, мы могли бы сделать интересную арифметику c, чтобы вычислить начальный день недели и избежать использования конструктора даты здесь, но это потребовало бы более глубокого размышления.
workingDaysInStartOfMonth
делает нечто похожее для дней между первым месяцем и указанной датой.
workingMonthsBetween
- основная функция, которая принимает две строки даты в формате ISO-8601 и вычисляет количество месяцев между ними, используя различные вспомогательные функции, описанные выше.
Все это, конечно, игнорирует праздники. Хотя добавить их не составит особого труда, это тоже не тривиально.
Обновление
Я рассмотрел использование Соответствие Зеллера , и хотя я понятия не имею, это значительно ускоряет весь алгоритм, он значительно быстрее в изоляции, чем использование конструктора Date. Следующий фрагмент демонстрирует эту альтернативу.
// General-purpose utility functions
const range = (lo, hi) =>
[... Array (hi - lo + 1)] .map ((_, i) => lo + i)
const parseDate = (s, [y, m, d] = s.split('-') .map (Number)) =>
[y, m - 1, d]
const isLeapYear = (y) =>
(y % 4 == 0) && (y % 100 != 0 || y % 400 == 0)
const daysInMonth = (y, m) =>
isLeapYear (y)
? [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] [m]
: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] [m]
// Zeller's Congruence (https://en.wikipedia.org/wiki/Zeller%27s_congruence)
const dayOfWeek = (
year, month, date,
y = month < 3 ? year - 1 : year,
m = ((month + 9) % 12) + 3
) => (
date
+ Math .floor ((13 * m - 1) / 5)
+ Math .floor (y / 4)
- Math .floor (y / 100)
+ Math .floor (y / 400)
) % 7
// Helper functions
const fullMonthsBetween = (y1, m1, y2, m2) => // excludes both endpoints
Math.max((12 * y2 + m2) - (12 * y1 + m1) - 1, 0)
const workingDaysInRemainderOfMonth = (y, m, d, day = dayOfWeek(y, m + 1, d)) =>
range (d, daysInMonth (y, m))
.filter ((_, i) => [1, 2, 3, 4, 5] .includes((day + i) % 7))
.length
const workingDaysInStartOfMonth = (y, m, d) =>
range (1, d)
.filter ((_, i) => [1, 2, 3, 4, 5] .includes ((d + i) % 7))
.length
// Main function
const workingMonthsBetween = (start, end) => {
const [y1, m1, d1] = parseDate (start)
const [y2, m2, d2] = parseDate (end)
return fullMonthsBetween (y1, m1, y2, m2)
+ workingDaysInRemainderOfMonth (y1, m1, d1)
/ workingDaysInRemainderOfMonth(y1, m1, 1)
+ workingDaysInStartOfMonth (y2, m2, d2)
/ workingDaysInRemainderOfMonth(y2, m2, 1)
}
// Demo
console .log (
workingMonthsBetween ('2020-05-20', '2021-08-12') //~> 14.744588744588745
)