J'ai actuellement une fonction dans ma base de données SQL qui ajoute un certain nombre de jours ouvrables à une date, par exemple. si vous entrez une date qui est un jeudi et ajoutez deux jours, la date du lundi suivant sera renvoyée. Je ne suis pas dérangé par les vacances, seuls les week-ends sont exclus.
Le problème est que cela est actuellement fait en utilisant une boucle while et cela semble ralentir considérablement la procédure stockée qui l'utilise lors de la génération d'une table. Est-ce que quelqu'un sait s'il existe un moyen d'effectuer ce calcul sans boucles while ni curseurs?
Juste pour information, voici la fonction actuelle:
ALTER FUNCTION [dbo].[AddWorkDaysToDate]
(
@fromDate datetime,
@daysToAdd int
)
RETURNS datetime
AS
BEGIN
DECLARE @toDate datetime
DECLARE @daysAdded integer
-- add the days, ignoring weekends (i.e. add working days)
set @daysAdded = 1
set @toDate = @fromDate
while @daysAdded <= @daysToAdd
begin
-- add a day to the to date
set @toDate = DateAdd(day, 1, @toDate)
-- only move on a day if we've hit a week day
if (DatePart(dw, @toDate) != 1) and (DatePart(dw, @toDate) != 7)
begin
set @daysAdded = @daysAdded + 1
end
end
RETURN @toDate
END
Cette réponse a été considérablement modifiée depuis son acceptation, car l'original était faux. Je suis cependant plus confiant dans la nouvelle requête, et cela ne dépend pas de DATEFIRST
Je pense que cela devrait couvrir:
declare @fromDate datetime
declare @daysToAdd int
select @fromDate = '20130123',@DaysToAdd = 4
declare @Saturday int
select @Saturday = DATEPART(weekday,'20130126')
;with Numbers as (
select 0 as n union all select 1 union all select 2 union all select 3 union all select 4
), Split as (
select @DaysToAdd%5 as PartialDays,@DaysToAdd/5 as WeeksToAdd
), WeekendCheck as (
select WeeksToAdd,PartialDays,MAX(CASE WHEN DATEPART(weekday,DATEADD(day,n.n,@fromDate))=@Saturday THEN 1 ELSE 0 END) as HitWeekend
from
Split t
left join
Numbers n
on
t.PartialDays >= n.n
group by WeeksToAdd,PartialDays
)
select DATEADD(day,WeeksToAdd*7+PartialDays+CASE WHEN HitWeekend=1 THEN 2 ELSE 0 END,@fromDate)
from WeekendCheck
Nous avons divisé le temps à ajouter en un nombre de semaines et un nombre de jours en une semaine. Nous utilisons ensuite un tableau de petit nombre pour déterminer si l’ajout de ces quelques jours nous permet d’atteindre un samedi. Si c'est le cas, nous devons ajouter 2 jours de plus au total.
C'est mieux si quelqu'un recherche une solution TSQL. Pas de boucles, pas de tables, pas de déclarations de cas ET fonctionne avec des négatifs. Quelqu'un peut-il battre ça?
CREATE FUNCTION[dbo].[AddBusinessDays](@Date date,@n INT)
RETURNS DATE AS
BEGIN
DECLARE @d INT;SET @d=4-SIGN(@n)*(4-DATEPART(DW,@Date));
RETURN DATEADD(D,@n+((ABS(@n)+@d-2)/5)*2*SIGN(@n)-@d/7,@Date);
END
En se basant sur la réponse acceptée pour cette question, la fonction définie par l'utilisateur (UDF) suivante devrait fonctionner dans tous les cas, quel que soit le paramètre défini pour @@DateFirst
.
UPDATE: Comme l'indiquent les commentaires ci-dessous, cette fonction est conçue pour que le FromDate soit un jour de semaine. Le comportement n'est pas défini lorsqu'un jour de week-end est transmis en tant que FromDate.
ALTER FUNCTION [dbo].[BusinessDaysDateAdd]
(
@FromDate datetime,
@DaysToAdd int
)
RETURNS datetime
AS
BEGIN
DECLARE @Result datetime
SET @Result = DATEADD(day, (@DaysToAdd % 5) + CASE ((@@DATEFIRST + DATEPART(weekday, @FromDate) + (@DaysToAdd % 5)) % 7)
WHEN 0 THEN 2
WHEN 1 THEN 1
ELSE 0 END, DATEADD(week, (@DaysToAdd / 5), @FromDate))
RETURN @Result
END
Cette réponse est basée sur @ la réponse de ElmerMiller .
Il corrige la valeur négative sur le commentaire du dimanche de @FistOfFury
Les valeurs négatives ne fonctionnent pas si la date indiquée est dimanche
Et le paramètre DATEFIRST de @Damien_The_Unbeliever
Mais celui-ci suppose un réglage DATEFIRST particulier (7), dont certains n’ont pas besoin.
Maintenant la fonction corrigée
CREATE FUNCTION[dbo].[AddBusinessDays](@Date DATE,@n INT)
RETURNS DATE AS
BEGIN
DECLARE @d INT,@f INT,@DW INT;
SET @f=CAST(abs(1^SIGN(DATEPART(DW, @Date)-(7-@@DATEFIRST))) AS BIT)
SET @DW=DATEPART(DW,@Date)-(7-@@DATEFIRST)*(@f^1)+@@DATEFIRST*(@f&1)
SET @d=4-SIGN(@n)*(4-@DW);
RETURN DATEADD(D,@n+((ABS(@n)+(@d%(8+SIGN(@n)))-2)/5)*2*SIGN(@n)-@d/7,@Date);
END
Avez-vous pensé à pré-renseigner une table de correspondance contenant tous les jours ouvrables (à l'aide de votre fonction), par exemple WorkingDays (int DaySequenceId, Date WorkingDate), vous pouvez ensuite utiliser cette table en sélectionnant le DaySequenceId de @fromDate et ajoutez @daysToAdd pour obtenir la nouvelle date de travail. De toute évidence, cette méthode entraîne également une charge supplémentaire liée à l’administration de la table WorkingDays, mais vous pouvez la pré-renseigner avec la plage de dates que vous attendez. L'autre inconvénient est que les dates de travail qui peuvent être calculées ne seront que celles contenues dans la table WorkingDays.
* Je sais que c'est un vieux fil, mais j'ai trouvé quelque chose d'extrêmement utile il y a quelque temps, je l'ai modifié et je l'ai obtenu.
select ((DATEADD(d,DATEDIFF(d,0,(DATEADD (d,2,@fromDate))),@numbOfDays)))*
Mise à jour: je suis désolé de trouver un morceau de code (dans une seule déclaration) et d'éviter d'utiliser une fonction, j'ai posté un code incorrect ici.
Le bit mentionné ci-dessus peut être utilisé si le nombre de jours que vous ajoutez est de 7 ou moins.
J'ai changé le code avec les paramètres requis pour une meilleure compréhension.
Quoi qu'il en soit, j'ai fini par utiliser ce que Nate Cook a mentionné ci-dessus. Et utilisé comme une seule ligne de code. (Parce que je me retiens d'utiliser des fonctions)
Le code de Nate
select(
DATEADD(day, (@days % 5) +
CASE ((@@DATEFIRST + DATEPART(weekday, GETDATE()) + (@days % 5)) % 7)
WHEN 0 THEN 2
WHEN 1 THEN 1
ELSE 0 END, DATEADD(week, (@days / 5), GETDATE()))
)
CREATE FUNCTION DateAddBusinessDays
(
@Days int,
@Date datetime
)
RETURNS datetime
AS
BEGIN
DECLARE @DayOfWeek int;
SET @DayOfWeek = CASE
WHEN @Days < 0 THEN (@@DateFirst + DATEPART(weekday, @Date) - 20) % 7
ELSE (@@DateFirst + DATEPART(weekday, @Date) - 2) % 7
END;
IF @DayOfWeek = 6 SET @Days = @Days - 1
ELSE IF @DayOfWeek = -6 SET @Days = @Days + 1;
RETURN @Date + @Days + (@Days + @DayOfWeek) / 5 * 2;
END;
Cette fonction peut ajouter et soustraire des jours ouvrables, quelle que soit la valeur de @@ DATEFIRST. Pour soustraire les jours ouvrables, utilisez un nombre de jours négatif.
Merci Damien pour le code. Il y avait une légère erreur dans les calculs en ce sens qu’elle n’ajoutait qu’un jour pour le dimanche et que, lorsque le nombre de jours ouvrables se croisait le week-end (sans atterrir le week-end), les deux jours supplémentaires n’étaient pas pris en compte. Voici une version modifiée du code de Damiens qui fonctionne avec la date par défaut à 7 heures. J'espère que cela vous aidera.
CREATE FUNCTION [dbo].[fn_AddBusinessDays]
(
@StartDate datetime,
@BusinessDays int
)
RETURNS datetime
AS
BEGIN
DECLARE @EndDate datetime
SET @EndDate = DATEADD(day, @BusinessDays%5 +
CASE
WHEN DATEPART(weekday,@StartDate) + @BusinessDays%5 > 6 THEN 2
ELSE 0
END,
DATEADD(week,@BusinessDays/5,@StartDate))
RETURN @EndDate
END
GO
C'est ce que j'utilise:
SET DATEFIRST 1;
SELECT DATEADD(dw, (**NumberToAdd**/5)*7+(**NumberToAdd** % 5) +
(CASE WHEN DATEPART(dw,**YourDate**) + (**NumberToAdd** % 5) > 5
THEN 2 ELSE 0 END), **YourDate**) AS IncrementedDate
FROM YourTable t
Le "SET DATEFIRST 1;" une partie est nécessaire pour définir le lundi comme premier jour de la semaine.
C'est un vieux fil mais je viens de créer une table avec toutes les dates puis j'ai fait ceci:
SELECT Count(*)
FROM Date_Table
WHERE [day] BETWEEN @StartDate and @EndDate
AND DATENAME(weekday, [day]) NOT IN ('Sunday', 'Saturday')
J'ai testé toutes les solutions proposées ici et aucune d'entre elles ne fonctionne ... Voici quelques scénarios de test qui ont cassé beaucoup des solutions ci-dessus ... (en supposant que samedi et dimanche soient les jours que vous excluez):
-Ajouter 0 jour à un samedi - Résultat attendu = samedi
-Ajouter 0 jour à un dimanche - Résultat attendu = dimanche
-Ajouter 1 jour au vendredi - Résultat attendu = le lundi suivant
-Ajouter 1 jour au samedi - Résultat attendu = le lundi suivant
-Ajouter 1 jour au dimanche - Résultat attendu = le lundi suivant
-Ajouter 3 jours au vendredi - Résultat attendu = le mercredi suivant
-Ajouter 5 jours au samedi - Résultat attendu = le vendredi suivant
-Ajouter 5 jours au vendredi - Résultat attendu = le vendredi suivant
-Sous soustraire 1 jour du lundi - Résultat attendu = le vendredi précédent
-Sous soustraire 1 jour du dimanche - Résultat attendu = le vendredi précédent
-Sous soustraire 1 jour du samedi - Résultat attendu = le vendredi précédent
-Sous soustraire 3 jours à partir du lundi - Résultat attendu = le mercredi précédent
-Sous soustraire 5 jours du samedi - Résultat attendu = le lundi précédent
-Sous soustraire 5 jours du lundi - Résultat attendu = le lundi précédent
Voici ce que j'ai écrit après avoir lu tout ce fil et repéré les bons éléments de logique:
CREATE FUNCTION [dbo].[BusinessDateAdd]
(
@FromDate DATE
,@DaysToAdd INT
)
RETURNS DATE
AS
BEGIN
--If there are no days to add or subtract, return the day that was passed in
IF @DaysToAdd = 0 RETURN @FromDate
DECLARE @Weeks INT
DECLARE @DMod INT
DECLARE @FromDateIndex INT
--number of weeks
SET @Weeks = @DaysToAdd/5
--remainder of days
SET @dmod = @DaysToAdd%5
--Get the FromDate day of the week, this logic standardizes the @@DateFirst to Sunday = 1
SET @FromDateIndex = (DATEPART(weekday, @FromDate) + @@DATEFIRST - 1) % 7 + 1
/*Splitting the addition vs subtraction logic for readability*/
--Adding business days
IF @DaysToAdd > 0
BEGIN
--If the FromDate is on a weekend, move it to the previous Friday
IF @FromDateIndex IN(1,7)
BEGIN
SET @FromDate = DATEADD(dd,CASE @FromDateIndex WHEN 1 THEN -2 WHEN 7 THEN -1 END,@FromDate)
SET @FromDateIndex = 6
END
SET @FromDate = DATEADD(dd,
CASE
--If the mod goes through the weekend, add 2 days to account for it
WHEN
((@FromDateIndex = 3 --Tuesday
AND @dmod > 3) --Days until Friday
OR
(@FromDateIndex = 4 --Wednesday
AND @dmod > 2)--Days until Friday
OR
(@FromDateIndex = 5 --Thursday
AND @dmod > 1)--Days until Friday
OR
(@FromDateIndex = 6 --Friday
AND @dmod > 0))--Days until Friday
THEN
@DMod+2
--Otherwise just add the mod
ELSE
@DMod
END, @FromDate)
END
--Subtracting business days
IF @DaysToAdd < 0
BEGIN
--If the FromDate is on a weekend, move it to the next Monday
IF @FromDateIndex IN(1,7)
BEGIN
SET @FromDate = DATEADD(dd,CASE @FromDateIndex WHEN 1 THEN 1 WHEN 7 THEN 2 END,@FromDate)
SET @FromDateIndex = 2
END
SET @FromDate = DATEADD(dd,
CASE
--If the mod goes through the weekend, subtract 2 days to account for it
WHEN
((@FromDateIndex = 5 --Thursday
AND @dmod < -3) --Days until Monday
OR
(@FromDateIndex = 4 --Wednesday
AND @dmod < -2)--Days until Monday
OR
(@FromDateIndex = 3 --Tuesday
AND @dmod < -1)--Days until Monday
OR
(@FromDateIndex = 2 --Monday
AND @dmod < 0))--Days until Monday
THEN
@DMod-2
--Otherwise just subtract the mod
ELSE
@DMod
END, @FromDate)
END
--Shift the date by the number of weeks
SET @FromDate = DATEADD(ww,@Weeks,@FromDate)
RETURN @FromDate
END
La réponse acceptée à la question produit des résultats incorrects. Par exemple. select @fromDate = '03-11-1983', @DaysToAdd = 3
donne 03-14-1983
alors que 03-16-1983
est attendu .
J'ai posté une solution de travail ici , mais par souci d'exhaustivité, je vais aussi l'ajouter ici. Si vous êtes intéressé par les détails des deux méthodes, visitez ma réponse initiale. Sinon, copiez-le simplement dans votre projet SQL et utilisez UTL_DateAddWorkingDays
Notez que ma solution ne fonctionne que si DATEFIRST
est défini sur la valeur par défaut 7.
Test Script utilisé pour tester diverses méthodes
CREATE FUNCTION [dbo].[UTL_DateAddWorkingDays]
(
@date datetime,
@days int
)
RETURNS TABLE AS RETURN
(
SELECT
CASE
WHEN @days = 0 THEN @date
WHEN DATEPART(dw, @date) = 1 THEN (SELECT Date FROM [dbo].[UTL_DateAddWorkingDays_Inner](DATEADD(d, 1, @date), @days - 1))
WHEN DATEPART(dw, @date) = 7 THEN (SELECT Date FROM [dbo].[UTL_DateAddWorkingDays_Inner](DATEADD(d, 2, @date), @days - 1))
ELSE (SELECT Date FROM [dbo].[UTL_DateAddWorkingDays_Inner](@date, @days))
END AS Date
)
CREATE FUNCTION [dbo].[UTL_DateAddWorkingDays_Inner]
(
@date datetime,
@days int
)
RETURNS TABLE AS RETURN
(
SELECT
DATEADD(d
, (@days / 5) * 7
+ (@days % 5)
+ (CASE WHEN ((@days%5) + DATEPART(dw, @date)) IN (1,7,8,9,10) THEN 2 ELSE 0 END)
, @date) AS Date
)
Pour développer le commentaire d'Amine et la réponse de Nate Cook ci-dessus, la solution one-liner à ceci est la suivante:
declare @DaysToAdd int , @FromDate datetime
set @DaysToAdd=-5 --5 days prior is 3/28/14
set @FromDate='4/4/14'
select
DATEADD(day, (@DaysToAdd % 5)
+ CASE
WHEN ((@@DATEFIRST + DATEPART(weekday, @FromDate)) % 7 + (@DaysToAdd % 5)) > 6 THEN 2
ELSE 0
END
, DATEADD(week, (@DaysToAdd / 5), @FromDate))
Notez que vous pouvez ajouter ou soustraire des jours pour avancer ou reculer dans le temps, respectivement.
Je n'ai pas encore de serveur SQL pour tester, mais voici l'idée:
ALTER FUNCTION [dbo].[AddWorkDaysToDate]
(
@fromDate datetime,
@daysToAdd int
)
RETURNS datetime
AS
BEGIN
DECLARE @dw integer
DECLARE @toDate datetime
set datefirst 1
set @toDate = dateadd(day, @daysToAdd, @fromDate)
set @dw = datepart(dw, @toDate)
if @dw > 5 set @toDate = dateadd(day, 8 - @dw, @toDate)
RETURN @toDate
END
WITH get_dates
AS
(
SELECT getdate() AS date, 0 as DayNo
UNION ALL
SELECT date + 1 AS date, case when DATEPART(DW, date + 1) IN (1,7) then DayNo else DayNo + 1 end
FROM get_dates
WHERE DayNo < 4
)
SELECT max(date) FROM get_dates
OPTION (MAXRECURSION 0)
Le calcul classique de l'ajout de semaines et de jours restants ne fonctionne pas bien avec l'ajout/la soustraction des jours de semaine en général. C'est pourquoi la méthode de la boucle pour ajouter des jours restants est facile à implémenter. La partie boucle de la logique est conçue pour gérer ces cas spéciaux du week-end.
Sans la boucle, vous devrez identifier les conditions spéciales pour compenser avec une sorte de compensation après l'ajout de semaines et de jours restants. La valeur de décalage dépend également de la direction du delta (positive ou négative).
En outre, vous aurez besoin d'une logique capable d'évaluer de manière cohérente si une date tombe un jour de semaine ou un week-end avec une valeur @@ DATEFIRST.
Cas d'utilisation:
La logique de travail principale pour trouver la date de sortie sera, w.r.t. addition/soustraction par nombre de jours:
Output = Input + (Weeks * 7) + Days
Où: weeks = int (delta/5), days = delta% 5
Cas d'utilisation n ° 1, nous retournons la date d'entrée car aucun calcul n'est nécessaire (en supposant que ce soit l'exigence). Les cas d'utilisation # 2-4 sont des cas d'utilisation spéciaux auxquels nous devons appliquer un décalage pour corriger la date de sortie. Pour # 5, aucune compensation n'est nécessaire. Par conséquent, il deviendra:
Output = Input + (Weeks * 7) + Days + Offset
Notre plage de valeurs de compensation se situe tout au plus entre 2 et -2 car il y a deux jours de week-end.
En utilisant DATEPART (), nous respecterons la règle de 0 = dimanche, 6 = samedi, 1-5 = lundi à vendredi. Nous utiliserons @@ DATEFIRST pour aider à compenser et à maintenir les valeurs avec insistance.
Script w/Commentaires:
DECLARE
@date DATE = '2018-04-25',
@n INT = 5 -- delta
IF @n != 0 -- (opposite of use case #1)
BEGIN
DECLARE @dw TINYINT = (DATEPART(DW, @date) + @@DATEFIRST - 1) % 7,
@offset INT = 0
IF @dw IN (0,6) -- if start = weekend (use case #2 & #3)
BEGIN
IF @n > 0 -- pos delta
BEGIN
IF @n%5 = 0 -- weekend start, weekly adjustment only
SELECT @offset = CASE WHEN @dw = 0 THEN -2 ELSE -1 END
ELSE IF @dw = 6 -- sat start, offset to Sun
SELECT @offset = 1
END
ELSE -- neg delta
BEGIN
IF @n%5 = 0 -- weekend start, weekly adjustment only
SELECT @offset = CASE WHEN @dw = 0 THEN 1 ELSE 2 END
ELSE IF @dw = 0 -- Sun start, offset to sat
SELECT @offset = -1
END
END
ELSE -- otherwise, start = weekday (use case #4)
BEGIN
DECLARE @dw2 TINYINT = (@dw + (@n%5) + 7) % 7
-- if result is/crosses weekend
IF @dw2 IN (0,6) OR (@n > 0 AND @dw2 < @dw) OR (@n < 0 AND @dw < @dw2)
SET @offset = CASE WHEN @n > 0 THEN 2 ELSE -2 END
END
-- adjust date by weeks + remainder + offset (if @offset = 0, use case #5)
SET @date = DATEADD(D, (@n%5) + @offset, DATEADD(D, CAST(@n/5 AS INT) * 7, @date))
END
SELECT @date
Script compacté
DECLARE
@date DATE = '2018-04-27',
@n INT = 3
IF @n != 0
BEGIN
DECLARE @dw TINYINT = (DATEPART(DW, @date) + @@DATEFIRST - 1) % 7, @d INT = @n % 5
DECLARE @dw2 TINYINT = (@dw + @d + 7) % 7
SET @date = DATEADD(D, @d + ISNULL(CASE
WHEN @dw IN (0,6) THEN
CASE WHEN @n > 0
THEN CASE WHEN @d = 0 THEN CASE WHEN @dw = 0 THEN -2 ELSE -1 END WHEN @dw = 6 THEN 1 END
ELSE CASE WHEN @d = 0 THEN CASE WHEN @dw = 0 THEN 1 ELSE 2 END WHEN @dw = 0 THEN -1 END
END
WHEN @dw2 IN (0,6) OR (@n > 0 AND @dw2 < @dw) OR (@n < 0 AND @dw < @dw2) THEN
CASE WHEN @n > 0 THEN 2 ELSE -2 END
END, 0), DATEADD(D, (@n / 5)* 7, @date))
END
SELECT @date
Cette fonction SQL fonctionne de manière similaire à la fonction Excel WORKDAY . J'espère que cela pourra vous aider.
CREATE FUNCTION [dbo].[BusDaysDateAdd]
(
@FromDate date,
@DaysToAdd int
)
RETURNS date
AS
BEGIN
DECLARE @Result date
DECLARE @TempDate date
DECLARE @Remainder int
DECLARE @datePartValue int
SET @TempDate = (DATEADD(week, (@DaysToAdd / 5), @FromDate))
SET @Remainder = (@DaysToAdd % 5)
SET @datePartValue = DATEPART(weekday, @TempDate)
SET @Result = DATEADD(day,@Remainder + CASE WHEN @Remainder > 0 AND @datePartValue = 7 THEN 1
WHEN @Remainder >= 1 AND @datePartValue = 6 THEN 2
WHEN @Remainder >= 2 AND @datePartValue = 5 THEN 2
WHEN @Remainder >= 3 AND @datePartValue = 4 THEN 2
WHEN @Remainder >= 4 AND @datePartValue = 3 THEN 2
WHEN @Remainder >= 5 AND @datePartValue = 2 THEN 2
ELSE 0 END, @TempDate)
RETURN @Result
END
GO
Soupir. Je ne peux pas croire qu'après toutes ces décennies, il y a toujours non: a) "DateAddWorkDays" standard dans Microsoft SQL Server (même si Microsoft a eu une fonction WorkDay
dans Excel pour toujours) et b) une solution claire ici ou ailleurs Je peux trouver que gère tous les problèmes que les gens ont soulevés.
Voici une solution que j'ai développée et qui résout les problèmes suivants, et apparemment, toutes les réponses ci-dessus ici et ailleurs que j'ai pu trouver, en ont un ou plusieurs. Cela gère:
WorkDay
d'Excel, mais je pense que cela est plus utile et intuitif. Ex. Si vous recevez une demande/commande un jour de week-end et que vous disposez d'un SLA (délai de réponse, date de livraison) d'un jour ouvrable, vous ne devriez pas avoir à répondre/à livrer avant la fin d'un jour ouvrable complet. (quel que soit le nombre de jours non ouvrés adjacents qui le précèdent).SUGGESTIONS: Bien sûr, comme avec tout algorithme récursif, celui-ci peut être converti en un algorithme itératif (en implémentant votre propre pile, c'est-à-dire avec une table temporaire), mais je pense que les 32 niveaux d'imbrication sont bien plus que suffisants pour la grande majorité. des cas d'utilisation du monde réel. En outre, vous pouvez bien sûr le rendre plus générique/portable en passant les dates de jours de semaine qui ne fonctionnent pas sous forme de paramètre table-valué par rapport à une référence de table codée en dur.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- ===================================================================================================================================
-- Author: Tom
-- Create date: 03/13/2017
-- Description: Add specified # of working days (+/-) to a specified date-time assuming existence of a list of non-work weekday
-- dates (incl. holidays, weather days, utility outage days, fire days, etc.) in the 'NonWorkDayDate' Column of a 'NonWorkWeekday'
-- Table. If specified # working days is 0, the specified date-time is returned. Working days are not added until the specified
-- date-time has first been incremented (+/-) to the next working day in the direction of the working days increment.
-- NOTE: Uses a forumla (vs. O(n) loop) that uses recusion whenever days incremented (incl. weekends) spans non-work weekdays.
-- !!!WARNING!!!: Will exceed SQL Server nesting level (32) if abs (# of working days) < ~1 / 32 adjacent non-working days.
-- Parameters:
-- @RefDateTime DateTime: Reference date-time to which to add '@WorkDaysIncrement'.
-- @WorkDaysIncrement Int: # of working days (+/-) to add # to the '@RefDateTime'.
-- Returns:
-- 1. Result of @RefDateTime + @WorkDaysIncrement (skipping weekend and holiday dates and retaining the @RefDateTime's time).
-- ===================================================================================================================================
CREATE FUNCTION [dbo].[AddWorkDays_Recursive]
(
-- Add the parameters for the function here
@RefDateTime datetime,
@WorkDaysIncrement int
)
RETURNS DateTime
AS
BEGIN
-- If no days to increment, return passed in date-time (even if weekend day).
if (@WorkDaysIncrement = 0) return @RefDateTime
-- Set the one-day increment used to add or subtract one calendar/work day.
declare @OneDayIncrement int = sign(@WorkDaysIncrement)
-- Initialize # of calendar days added to 0.
declare @DaysAdded int = 0
-- Set reference date to date (i.e. excl. time) of reference date-time.
declare @RefDate datetime = convert
(
date,
convert
(
varchar(10),
@RefDateTime,
101
)
)
--end declare @RefDate
-- Initialize result date to reference date
declare @ResultDate datetime = @RefDate
-- Set U.S. Weekday # to the 1-based U.S. weekday # result date.
declare @USWeekdayNumber tinyint = ((datepart(weekday, @ResultDate) + @@datefirst - 1) % 7) + 1 -- Sun to Sat = 1 to 7
-- If result date is now on a weekend day, set # of weekend days increment so that we can move it +/- 1 to 2 days to next weekday.
declare @WeekendDaysInc smallint =
(
case (@USWeekdayNumber)
when 1 then --Sunday
case
when (@OneDayIncrement > 0) then 1
else -2
end
--end when 1 --Sunday
when 7 then --Saturday
case
when (@OneDayIncrement > 0) then 2
else -1
end
--end when 7 then --Saturday
else 0 -- Not Weekend Day #
end -- case (@USWeekdayNumber)
) -- end declare @WeekendDaysInc smallint =
-- Increment # of calendar days added by # of weekend days increment
set @DaysAdded += @WeekendDaysInc
-- Increment result date by # of weekend days increment
set @ResultDate += @WeekendDaysInc
-- Set # of work weeks increment to # of full 5-day increments in the # (+/-) of work days to increment.
declare @WorkWeeksIncrement int = @WorkDaysIncrement / 5
-- Increment # of calendar days added by 7 times # of work weeks increment, i.e. to add weekday + weekend days for full weeks.
set @DaysAdded += @WorkWeeksIncrement * 7
-- Set result date after full weeks added to reference date + # of calendar days
declare @AfterFullWeeksResultDate datetime = @ResultDate + @DaysAdded
-- Set # partial-work week days to # (+/-) of work days to increment left after adding full weeks.
declare @PartialWorkWeekDays int = @WorkDaysIncrement % 5
-- Increment # of calendar days added by # partial-work week days
set @DaysAdded += @PartialWorkWeekDays
-- Set result date after partial week added to result date after full weeks added + # partial work week days
declare @AfterPartialWeekResultDate datetime = @AfterFullWeeksResultDate + @PartialWorkWeekDays
--Set result date to result date after partial week.
set @ResultDate = @AfterPartialWeekResultDate
-- Set After Full Weeks U.S. Weekday # to the 1-based U.S. weekday # result date.
declare @AfterFullWeeksUSWeekdayNumber tinyint =
(
((datepart(weekday, @AfterFullWeeksResultDate) + @@datefirst - 1) % 7) + 1 -- Sun to Sat = 1 to 7
)
-- Set After Partial Week U.S. Weekday # to the 1-based U.S. weekday # result date.
declare @AfterPartialWeekUSWeekdayNumber tinyint =
(
((datepart(weekday, @AfterPartialWeekResultDate) + @@datefirst - 1) % 7) + 1 -- Sun to Sat = 1 to 7
)
--If (incrementing and After Full Weeks U.S. Weekday # > @AfterPartialWeekUSWeekdayNumber)
-- or (decrementing and After Full Weeks U.S. Weekday # < @AfterPartialWeekUSWeekdayNumber), increment by (+/-) 2 to account for
-- the weekend that was spanned when partial-work week days were added.
if
(
(
(@OneDayIncrement > 0)
and (@AfterFullWeeksUSWeekdayNumber > @AfterPartialWeekUSWeekdayNumber)
)
or (
(@OneDayIncrement < 0)
and (@AfterFullWeeksUSWeekdayNumber < @AfterPartialWeekUSWeekdayNumber)
)
)
begin
set @WeekendDaysInc = 2 * @OneDayIncrement
set @DaysAdded += @WeekendDaysInc
set @ResultDate += @WeekendDaysInc
end -- if need to increment to account for weekend spanned by partial-work week days,
-- Set U.S. Weekday # to the 1-based U.S. weekday # result date.
set @USWeekdayNumber = ((datepart(weekday, @ResultDate) + @@datefirst - 1) % 7) + 1 -- Sun to Sat = 1 to 7
-- If result date is now on a weekend day, set # of weekend days increment so that we can move it +/- 1 to 2 days to next weekday.
set @WeekendDaysInc =
(
case (@USWeekdayNumber)
when 1 then --Sunday
case
when (@OneDayIncrement > 0) then 1
else -2
end
--end when 1 --Sunday
when 7 then --Saturday
case
when (@OneDayIncrement > 0) then 2
else -1
end
--end when 7 then --Saturday
else 0 -- Not Weekend Day #
end -- case (@USWeekdayNumber)
) -- end declare @WeekendDaysInc smallint =
-- Increment # of calendar days added by # of weekend days increment
set @DaysAdded += @WeekendDaysInc
-- Increment result date by # of weekend days increment
set @ResultDate += @WeekendDaysInc
-- Set non-work weedays count to # Rows where NonWorkDayDate between RefDate and ResultDate (if # of work days to increment > 0), else between
-- ResultDate and RefDate.
declare @NonWorkWeekdaysCount int =
(
select count(nw.NonWorkDayDate)
from NonWorkWeekday as nw
where
(
(@OneDayIncrement > 0)
and (nw.NonWorkDayDate between @RefDate and @ResultDate)
)
or (
(@OneDayIncrement < 0)
and (nw.NonWorkDayDate between @ResultDate and @RefDate)
)
--end select count(nw.NonWorkDayDate) from Holidate as nw
) -- end declare @HolidaysSpanned int =
-- Set result date-time to reference date-time + # of calendar days added
declare @ResultDateTime datetime = @RefDateTime + @DaysAdded
-- Set result date-time equal to result of adding (# of holidays x one-day increment).
set @ResultDateTime = dbo.AddWorkDays_Recursive
(
@ResultDateTime, -- @RefDateTime
@NonWorkWeekdaysCount * @OneDayIncrement -- @WorkDaysIncrement
)
--end set @ResultDateTime =
-- Return the result of the function
RETURN @ResultDateTime
END
GO
Je suis un peu en retard pour ce parti mais j'ai fini par écrire ma propre version, à cause des inconvénients des autres solutions. Plus précisément, cette version adresse le décompte en arrière et commence le week-end.
Une situation ambiguë peut survenir si vous ajoutez zéro jour ouvrable à une date de fin de semaine. J'ai conservé la même date, mais vous pouvez omettre ce contrôle si vous souhaitez toujours forcer le renvoi d'un jour de la semaine.
CREATE FUNCTION [dbo].[fn_AddBusinessDays]
(
@date datetime,
@businessDays int
)
RETURNS datetime
AS
BEGIN
--adjust for weeks first
declare @weeksToAdd int = @businessDays / 7
declare @daysToAdd int = @businessDays % 7
--if subtracting days, subtract a week then offset
if @businessDays < 0 begin
set @daysToAdd = @businessDays + 5
set @weeksToAdd = @weeksToAdd - 1
end
--saturday becomes zero using the modulo operator
declare @originalDayOfWeek int = datepart(dw, @date) % 7
declare @newDayOfWeek int = datepart(dw, dateadd(d, @daysToAdd, @date)) % 7
--special case for when beginning date is weekend
--adding zero on a weekend keeps the same date. you can remove the <> 0 check if you want Sunday + 0 => Monday
declare @dateOffset int = case
when @businessDays <> 0 and @originalDayOfWeek = 0 then 2
when @businessDays <> 0 and @originalDayOfWeek = 1 then 1
when @businessDays <> 0 and @newDayOfWeek < @originalDayOfWeek then 2
else 0
end
-- Return the result of the function
return dateadd(d, @daysToAdd + @dateOffset, dateadd(ww, @weeksToAdd, @date))
END
Je sais qu'il est un peu tard, quelqu'un d'autre est peut-être tombé sur ce problème ... J'ai essayé la solution ci-dessus, mais la plupart d'entre eux ne peuvent calculer les vacances.
Voici comment j'ai essayé
CREATE function [dbo].[DateAddWorkDay]
(@days int,@FromDate Date)
returns Date
as
begin
declare @result date
set @result = (
select b
from
(
SELECT
b,
(DATEDIFF(dd, a, b))
-(DATEDIFF(wk, a, b) * 2)
-(CASE WHEN DATENAME(dw, a) = 'Sunday' THEN 1 ELSE 0 END)
-(CASE WHEN DATENAME(dw, b) = 'Saturday' THEN 1 ELSE 0 END)
-COUNT(o.Holiday_Date)
as workday
from
(
select
@FromDate as a,
dateadd(DAY,num +@days,@FromDate) as b
from (select row_number() over (order by (select NULL)) as num
from Information_Schema.columns
) t
where num <= 100
) dt
left join Holiday o on o.Holiday_Date between a and b and DATENAME(dw, o.Holiday_Date) not in('Saturday','Sunday')
where DATENAME(dw, b) not in('Saturday','Sunday')
and b not in (select Holiday_Date from OP_Holiday where Holiday_Date between a and b)
group by a,b
) du
where workday =@days
)
return @result
end
Où vacances est une table avec holiday_date comme référence pour les vacances
J'espère que cela peut aider quelqu'un.
Pour l'Allemagne, toutes les réponses ne fonctionnent pas.
La seule fonction que j'ai testée et travaillée est une traduction d'un ancien formulaire Excel here
Set @EndDate=Dateadd(DAY,@DaysToAdd,@FromDate) +
Cast(((
CASE WHEN 5 <= DATEPART(weekday, @FromDate)%7
THEN 5
ELSE
DATEPART(weekday, @FromDate)%7
END)
-1 + @DaysToAdd )/5
as int)
* 2 -
(Case when DAtepart(weekday, @FromDate)=6 then 1 else 0 end)
Je viens de tester la réponse acceptée et de constater que cela ne fonctionne pas lorsque le dimanche est le jour de départ.
Vous devez ajouter les éléments suivants sous l'élément de ligne Select @Saturday
:
SELECT @fromDate = CASE WHEN DATEPART(weekday,@fromDate) = 1 THEN DATEADD(day,1,@fromDate) ELSE @fromDate END