سالهاست که برای ذخیره تاریخ در SQL Server یک فیلد رشتهای ده کاراکتری در نظر میگیرم و باقی تغییر و تحوّلات را میگذارم بر عهده کدهای برنامه
دیروز اما قصد داشتم گزارشی بنویسم از یکسری لاگ و تاریخها همه میلادی ذخیره میشوند به صورت خودکار و با تابع GETDATE داخل SQL Server مبدّلهایی که داشتم مربوط به کدهای داخل Net. در اینترنت جستجو کردم و برای SQL Server اینکه در داخل آن تبدیل را انجام دهم این موارد را یافتم:
GETTING SHAMSI DATE FROM GREGORIAN DATE
How to convert Persian Or Jalali Calendar to Gregorian Calendar & vice versa In Sql Server
Creating a CLR Persian Date Convertor Function for SQL Server
Persian Date/Time support for MsSQL
وقتی تبدیل در قالب یک تابع در وسطهای SELECT انجام میشود کار سریعتر و سادهتر به انجام میرسد دیگر نیاز نیست رکورد به رکورد بگیری و داخل کدهای برنامه تبدیل کنی یکسره دیتا را میگیری و میفرستی برای کلاینت تاریخها هم همه شمسی شده
اما با کدهای فوق به مشکلاتی برخوردم؛
دو مورد اول نتایج اشتباهی میدادند ظاهراً در محاسبه سال کبیسه ایراد داشتند و یک روز جلوتر حساب کردند
مورد سوم و چهارم هم رفته بودند سراغ کدهای داخل Net. اساساً از قابلیتی استفاده کرده که اجازه میدهد از داخل SQL Server به توابع Net. ارجاع شود دنبال این پیچیدگیها نبودم یک کد ساده و روان میخواستم که در قالب UDF مثلاً قابل اجرا باشد
ولی اتفاق جالبی افتاد کد بسیار سادهای در کامنتهای این مطلب یافتم کد مذکور از منطق سادهای پیروی میکرد ولی ایراد داشت تصمیم گرفتم و کمی وقت گذاشتم کد را یکبار به صورت کامل بررسی کردم و بر اساس همان منطق سادهای که داشت بازنویسی نمودم
CREATE FUNCTION [G2J] ( @intDate DATETIME ) RETURNS NVARCHAR(max) BEGIN
DECLARE @shYear AS INT ,@shMonth AS INT ,@shDay AS INT ,@intYY AS INT ,@intMM AS INT ,@intDD AS INT ,@Kabiseh1 AS INT ,@Kabiseh2 AS INT ,@d1 AS INT ,@m1 AS INT, @shMaah AS NVARCHAR(max),@shRooz AS NVARCHAR(max),@DayCnt AS INT DECLARE @DayDate AS NVARCHAR(max)
SET @intYY = DATEPART(yyyy, @intDate)
IF @intYY < 1000 SET @intYY = @intYY + 2000
SET @intMM = MONTH(@intDate) SET @intDD = DAY(@intDate) SET @shYear = @intYY - 622 SET @DayCnt = datepart(dw, "01/02/" + CONVERT(CHAR(4), @intYY))
SET @m1 = 1 SET @d1 = 1 SET @shMonth = 10 SET @shDay = 11
IF ( ( @intYY - 1993 ) % 4 = 0 ) SET @shDay = 12
WHILE ( @m1 != @intMM ) OR ( @d1 != @intDD ) BEGIN
SET @d1 = @d1 + 1 SET @DayCnt = @DayCnt + 1
IF ( ( @intYY - 1992 ) % 4 = 0) SET @Kabiseh1 = 1 ELSE SET @Kabiseh1 = 0
IF ( ( @shYear - 1371 ) % 4 = 0) SET @Kabiseh2 = 1 ELSE SET @Kabiseh2 = 0
IF (@d1 = 32 AND (@m1 = 1 OR @m1 = 3 OR @m1 = 5 OR @m1 = 7 OR @m1 = 8 OR @m1 = 10 OR @m1 = 12)) OR (@d1 = 31 AND (@m1 = 4 OR @m1 = 6 OR @m1 = 9 OR @m1 = 11)) OR (@d1 = 30 AND @m1 = 2 AND @Kabiseh1 = 1) OR (@d1 = 29 AND @m1 = 2 AND @Kabiseh1 = 0) BEGIN SET @m1 = @m1 + 1 SET @d1 = 1 END
IF @m1 > 12 BEGIN SET @intYY = @intYY + 1 SET @m1 = 1 END IF @DayCnt > 7 SET @DayCnt = 1
SET @shDay = @shDay + 1 IF (@shDay = 32 AND @shMonth < 7) OR (@shDay = 31 AND @shMonth > 6 AND @shMonth < 12) OR (@shDay = 31 AND @shMonth = 12 AND @Kabiseh2 = 1) OR (@shDay = 30 AND @shMonth = 12 AND @Kabiseh2 = 0) BEGIN SET @shMonth = @shMonth + 1 SET @shDay = 1 END
IF @shMonth > 12 BEGIN SET @shYear = @shYear + 1 SET @shMonth = 1 END END
IF @shMonth=1 SET @shMaah=N"فروردین" IF @shMonth=2 SET @shMaah=N"اردیبهشت" IF @shMonth=3 SET @shMaah=N"خرداد" IF @shMonth=4 SET @shMaah=N"تیر" IF @shMonth=5 SET @shMaah=N"مرداد" IF @shMonth=6 SET @shMaah=N"شهریور" IF @shMonth=7 SET @shMaah=N"مهر" IF @shMonth=8 SET @shMaah=N"آبان" IF @shMonth=9 SET @shMaah=N"آذر" IF @shMonth=10 SET @shMaah=N"دی" IF @shMonth=11 SET @shMaah=N"بهمن" IF @shMonth=12 SET @shMaah=N"اسفند"
IF @DayCnt=1 SET @shRooz=N"شنبه" IF @DayCnt=2 SET @shRooz=N"یکشنبه" IF @DayCnt=3 SET @shRooz=N"دوشنبه" IF @DayCnt=4 SET @shRooz=N"سهشنبه" IF @DayCnt=5 SET @shRooz=N"چهارشنبه" IF @DayCnt=6 SET @shRooz=N"پنجشنبه" IF @DayCnt=7 SET @shRooz=N"جمعه"
SET @DayDate = @shRooz + " " + LTRIM(STR(@shDay,2)) + " " + @shMaah + " " + STR(@shYear,4) --پنجشنبه 17 اردیبهشت 1394
/* SET @DayDate = LTRIM(STR(@shDay,2)) + " " + @shMaah + " " + STR(@shYear,4) --17 اردیبهشت 1394
SET @DayDate = STR(@shYear,4) + "/"+LTRIM(STR(@shMonth,2)) + "/" + LTRIM(STR(@shDay,2)) --1394/2/17
SET @DayDate = REPLACE(RIGHT(STR(@shYear, 4), 4), " ", "0") + "/"+ REPLACE(STR(@shMonth, 2), " ", "0") + "/" + REPLACE(( STR(@shDay,2) ), " ", "0") --1394/02/17 */ RETURN @DayDate END
منطق آن چنین است: از یک مبدأ مشترک میان تقویم شمسی و میلادی استفاده میکند روزهای سال را میشمرد یکبار برای میلادی و یکبار برای شمسی سر هر ماه که میرسد میپرد کبیسهها را هم به خوبی حساب مینماید در نهایت وقتی تاریخ میلادی به روز مورد نظر رسید همانجا میایستد و تاریخ شمسی را به عنوان خروجی عرضه مینماید روش بسیار روشن و تمیزی به نظرم رسید
وقتی اصلاحات تمام شد پاسخ خیلی دقیقی داد درست همان پاسخی که انتظار داشتم
کد را گذاشتم که مورد استفاده مردمانم قرار گیرد کافیست آن را در قالب یک تابع تعریفشده توسط کاربر (UDF) استفاده کنید مانند این:
SELECT CreateDate, G2J(CreateDate) AS Tarikh FROM MyTable
CreateDate Tarikh - - - - - - - - - - - - - - - - - - - - - - - 06/19/2015 جمعه 29 خرداد 1394 06/23/2015 سهشنبه 2 تیر 1394
پینوشت اول: (4 شهریور 94) در یکی از نظرات پیشنهادی طرح شد مبنی بر اینکه فرمت خروجی تابع از طریق پارامترهای ارسالی به آن تعیین شود و دیگر نیازی به دستکاری کدها نباشد این شد که تابع را به شکل زیر توسعه دادم:
CREATE FUNCTION dbo.[G2J] ( @intDate DATETIME, @Format NVARCHAR(max)) RETURNS NVARCHAR(max) BEGIN /* Format Rules: (پنجشنبه 7 اردیبهشت 1394) ChandShanbe -> پنجشنبه (روز هفته به حروف) ChandShanbeAdadi -> 6 (روز هفته به عدد) Rooz -> 7 (چندمین روز از ماه) Rooz2 -> 07 (چندمین روز از ماه دو کاراکتری) Maah -> 2 (چندمین ماه از سال) Maah2 -> 02 (چندمین ماه از سال دو کاراکتری) MaahHarfi -> اردیبهشت (نام ماه به حروف) Saal -> 1394 (سال چهار کاراکتری) Saal2 -> 94 (سال دو کاراکتری) Saal4 -> 1394 (سال چهار کاراکتری) SaalRooz -> 38 (چندمین روز سال) Default Format -> "ChandShanbe Rooz MaahHarfi Saal" */ DECLARE @shYear AS INT ,@shMonth AS INT ,@shDay AS INT ,@intYY AS INT ,@intMM AS INT ,@intDD AS INT ,@Kabiseh1 AS INT ,@Kabiseh2 AS INT ,@d1 AS INT ,@m1 AS INT, @shMaah AS NVARCHAR(max),@shRooz AS NVARCHAR(max),@DayCnt AS INT, @YearDay AS INT DECLARE @DayDate AS NVARCHAR(max)
SET @intYY = DATEPART(yyyy, @intDate)
IF @intYY < 1000 SET @intYY = @intYY + 2000
SET @intMM = MONTH(@intDate) SET @intDD = DAY(@intDate) SET @shYear = @intYY - 622 IF (@Format IS NULL) OR NOT LEN(@Format)>0 SET @Format = "ChandShanbe Rooz MaahHarfi Saal"
SET @m1 = 1 SET @d1 = 1 SET @shMonth = 10 SET @shDay = 11 SET @DayCnt = datepart(dw, "01/02/" + CONVERT(CHAR(4), @intYY)) SET @YearDay = 276
IF ( ( @intYY - 1993 ) % 4 = 0 ) SET @shDay = 12 SET @YearDay = @YearDay + @shDay
WHILE ( @m1 != @intMM ) OR ( @d1 != @intDD ) BEGIN
SET @d1 = @d1 + 1 SET @DayCnt = @DayCnt + 1
IF ( ( @intYY - 1992 ) % 4 = 0) SET @Kabiseh1 = 1 ELSE SET @Kabiseh1 = 0
IF ( ( @shYear - 1371 ) % 4 = 0) SET @Kabiseh2 = 1 ELSE SET @Kabiseh2 = 0
IF (@d1 = 32 AND (@m1 = 1 OR @m1 = 3 OR @m1 = 5 OR @m1 = 7 OR @m1 = 8 OR @m1 = 10 OR @m1 = 12)) OR (@d1 = 31 AND (@m1 = 4 OR @m1 = 6 OR @m1 = 9 OR @m1 = 11)) OR (@d1 = 30 AND @m1 = 2 AND @Kabiseh1 = 1) OR (@d1 = 29 AND @m1 = 2 AND @Kabiseh1 = 0) BEGIN SET @m1 = @m1 + 1 SET @d1 = 1 END
IF @m1 > 12 BEGIN SET @intYY = @intYY + 1 SET @m1 = 1 END IF @DayCnt > 7 SET @DayCnt = 1
SET @shDay = @shDay + 1 SET @YearDay = @YearDay + 1 IF (@shDay = 32 AND @shMonth < 7) OR (@shDay = 31 AND @shMonth > 6 AND @shMonth < 12) OR (@shDay = 31 AND @shMonth = 12 AND @Kabiseh2 = 1) OR (@shDay = 30 AND @shMonth = 12 AND @Kabiseh2 = 0) BEGIN SET @shMonth = @shMonth + 1 SET @shDay = 1 END
IF @shMonth > 12 BEGIN SET @shYear = @shYear + 1 SET @shMonth = 1 SET @YearDay = 1 END END
IF @shMonth=1 SET @shMaah=N"فروردین" IF @shMonth=2 SET @shMaah=N"اردیبهشت" IF @shMonth=3 SET @shMaah=N"خرداد" IF @shMonth=4 SET @shMaah=N"تیر" IF @shMonth=5 SET @shMaah=N"مرداد" IF @shMonth=6 SET @shMaah=N"شهریور" IF @shMonth=7 SET @shMaah=N"مهر" IF @shMonth=8 SET @shMaah=N"آبان" IF @shMonth=9 SET @shMaah=N"آذر" IF @shMonth=10 SET @shMaah=N"دی" IF @shMonth=11 SET @shMaah=N"بهمن" IF @shMonth=12 SET @shMaah=N"اسفند"
IF @DayCnt=1 SET @shRooz=N"شنبه" IF @DayCnt=2 SET @shRooz=N"یکشنبه" IF @DayCnt=3 SET @shRooz=N"دوشنبه" IF @DayCnt=4 SET @shRooz=N"سه شنبه" IF @DayCnt=5 SET @shRooz=N"چهارشنبه" IF @DayCnt=6 SET @shRooz=N"پنجشنبه" IF @DayCnt=7 SET @shRooz=N"جمعه"
SET @DayDate = REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(@Format,"MaahHarfi",@shMaah),"SaalRooz",LTRIM(STR(@YearDay,3))),"ChandShanbeAdadi",@DayCnt),"ChandShanbe",@shRooz),"Rooz2",REPLACE(STR(@shDay,2), " ", "0")),"Maah2",REPLACE(STR(@shMonth, 2), " ", "0")),"Saal2",SUBSTRING(STR(@shYear,4),3,2)),"Saal4",STR(@shYear,4)),"Saal",LTRIM(STR(@shYear,4))),"Maah",LTRIM(STR(@shMonth,2))),"Rooz",LTRIM(STR(@shDay,2))) /* Format Samples: Format="ChandShanbe Rooz MaahHarfi Saal" -> پنجشنبه 17 اردیبهشت 1394 Format="Rooz MaahHarfi Saal" -> ـ 17 اردیبهشت 1394 Format="Rooz/Maah/Saal" -> 1394/2/17 Format="Rooz2/Maah2/Saal2" -> 94/02/17 Format="Rooz روز گذشته از MaahHarfi در سال Saal2" -> ـ 17 روز گذشته از اردیبهشت در سال 94 */ RETURN @DayDate END
اکنون باید اینطور استفاده کنید:
SELECT CreateDate, G2J(CreateDate,"ChandShanbe Rooz MaahHarfi Saal") AS Tarikh FROM MyTable
CreateDate Tarikh - - - - - - - - - - - - - - - - - - - - - - - 06/19/2015 جمعه 29 خرداد 1394 06/23/2015 سهشنبه 2 تیر 1394
اگر پارامتر دوم را خالی بفرستید دو آپستروف مثلاً: ("",G2J(CreateDate همان پیشفرض قبلی را خروجی میدهد ولی یازده کلمه کلیدی وجود دارد که با ارسال هر کدام یا ترکیبشان میتوانید خروجیهای متنوعی بگیرید.
پینوشت دوم: (4 شهریور 94) شاید شما هم متوجه شده باشید قبلاً هم در سایتهای معرفی شده فوق دیده بودم اینکه از کندی چنین توابعی سخن به میان آمده بالاخره یک فرآیند است و این الگوریتم منابع زیادی از سرور را مصروف میدارد هر بار اجرای این کد که در سرور شخصی من حدود 40 میلیثانیه زمان میبرد ولی وقتی حجم کار بالا میرود خودم هم با این مشکل مواجه شدم وقتی خواستم حدود یازدههزار رکورد را براساس تاریخ شمسی گروهبندی نمایم در حالی که تاریخ میلادی در جدول ذخیره شده بود من هم با تایماوت مواجه شدم!
دیده بودم کسانی میآیند و جدول میسازند برای تبدیل میلادی به شمسی یا بالعکس به نظر میرسد سرعت را بسیار بیشتر میکند
یک پروسیجر نوشتم سال ابتدا و سال انتها را وارد میکنید (در این نمونه 2015 و 2016) تمام روزها را استخراج کرده میلادی و شمسی و حتی به تفکیک در یک جدول میریزد (Miladi_Shamsi)
البته برای اینکار از همان تابع بالا استفاده میکند ببینید:
CREATE PROCEDURE dbo.Make_Convert_Date_Table AS BEGIN
DECLARE @AzYear AS INT, @TaYear AS INT SET @AzYear = 2015 SET @TaYear = 2016 IF OBJECT_ID("dbo.Miladi_Shamsi", "U") IS NOT NULL DROP TABLE [dbo].[Miladi_Shamsi] CREATE TABLE [dbo].[Miladi_Shamsi] ( [Miladi] smalldatetime NOT NULL, [ShamsiSlash] char(10) NULL, [ShamsiFull] nvarchar(30) COLLATE Persian_100_CI_AI NULL, [ShamsiRooz] int NULL, [ShamsiMaah] int NULL, [ShamsiSaal] int NULL, [ShamsiWeekDay] int NULL, PRIMARY KEY CLUSTERED ([Miladi]) )
DECLARE @CurDate AS DATETIME, @LastDate AS DATETIME SET @CurDate = CONVERT(DATETIME, "1/1/" + CONVERT(CHAR(4),@AzYear)) --MM/DD/YYYY SET @LastDate = CONVERT(DATETIME, "12/31/" + CONVERT(CHAR(4),@TaYear)) DECLARE @Shamsi AS NVARCHAR(max), @Saal AS CHAR(4), @Maah AS CHAR(2), @Rooz AS CHAR(2), @ShanbeInt AS CHAR(1), @Shanbe AS NVARCHAR(10), @MaahH AS NVARCHAR(10) WHILE @CurDate <= @LastDate BEGIN
SET @Shamsi = dbo.G2J(@CurDate,"Saal4Maah2Rooz2ChandShanbeAdadiChandShanbe|MaahHarfi") --139402175.$. SET @Saal = SUBSTRING(@Shamsi,1,4) SET @Maah = SUBSTRING(@Shamsi,5,2) SET @Rooz = SUBSTRING(@Shamsi,7,2) SET @ShanbeInt = SUBSTRING(@Shamsi,9,1) SET @Shanbe = SUBSTRING(@Shamsi,10,CHARINDEX("|",@Shamsi)-10) SET @MaahH = SUBSTRING(@Shamsi,CHARINDEX("|",@Shamsi)+1,10)
INSERT INTO [dbo].[Miladi_Shamsi] ([Miladi],[ShamsiSlash],[ShamsiFull],[ShamsiRooz],[ShamsiMaah],[ShamsiSaal],[ShamsiWeekDay]) VALUES (@CurDate,@Saal+"/"+@Maah+"/"+@Rooz,@Shanbe+" "+LTRIM(STR(CONVERT(INT,@Rooz),2))+" "+@MaahH+" "+@Saal,@Rooz,@Maah,@Saal,@ShanbeInt)
SET @CurDate = @CurDate + 1 END
END
پس از اینکه این جدول را ساختید جدولی شبیه به این:
Miladi ShamsiSlash ShamsiRooz ShamsiMaah ShamsiSaal ShamsiWeekDay ShamsiFull - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 1/1/2015 1393/10/11 11 10 1393 6 پنجشنبه 11 دی 1393 1/2/2015 1393/10/12 12 10 1393 7 جمعه 12 دی 1393
و در اختیار گرفتید میتوانید یک تابع میانی بسازید مانند زیر:
CREATE FUNCTION dbo.[G2J_Fast] ( @intDate DATETIME) RETURNS NVARCHAR(max) BEGIN
DECLARE @Shamsi AS NVARCHAR(max)
SELECT @Shamsi=ShamsiFull FROM [Miladi_Shamsi] WHERE Miladi=@intDate
IF @Shamsi IS NULL SET @Shamsi=dbo.G2J(@intDate,"")
RETURN @Shamsi END
حالا یک تابع جدید به نام G2J_Fast داریم که سریعتر از قبلی عمل مینماید زیرا ابتدا به سراغ جدول Miladi_Shamsi میرود اگر تاریخ میلادی را یافت که هیچ شمسی را میگیرد و تقدیم مینماید اما اگر تاریخ مذکور در گستره ذخیره شده نبود آن را از تابع G2J فراخوانی میکند
بدینترتیب ما یک روش هیبریدی در اختیار داریم میتوانیم جدول را در گسترهای که نیاز معمولمان است بسازیم و برای حالتهای خاص گاهی که تاریخ مربوط به گذشته یا آینده دور است از تابع تبدیل استفاده کنیم
از نظرات ارزشمندتان بهرهمندم سازید و اگر اصلاحی به نظرتان رسید بفرمایید تا اعمال شود.
پینوشت سوم: (4 شهریور 94) فراموش نکنید که به جای کوتیشنمارکها ("...") در کدها باید آپستروف بگذارید (") پارسیبلاگ به جهات امنیتی اجازه درج آپستروف در متن نمیدهد این است که آپستروفها را خودبهخود تبدیل به کوتیشن میکند روی کدهای فوق قبل از درج در دیتابیس باید یک جستجوجایگزینی انجام دهید تا مشکل حل شود!
پینوشت چهارم: (5 شهریور 94) برای آنان که جدول آماده معادلسازیشده میلادی_شمسی را میخواهند این فایل را آماده کردم (http://movashah.id.ir/o/MiladiShamsi.txt) با همین پروسیجر فوق ساخته شده از سال 2010 تا 2020 را خروجی گرفتم (دی 1388 تا دی 1399) یازده سال که میشود 4018 روز (با احتساب سه روز اضافه به خاطر سه سال کبیسه) میتوانید با دیدن این نمونه صحّت تبدیل تابع G2J را مشاهده و بررسی بفرمایید و یا به دیتابیس خود منتقل نموده و برای تبدیل استفاده کنید
پینوشت پنجم: (17 آبان 94) اشکالی در کد تابع وجود داشت که در شماره روز هفته و نام روز اشتباه میکرد یکی از بازدیدکنندگان وبلاگ تذکّر دادند و مشکل مزبور مرتفع گردید
پینوشت ششم: (17 آبان 94) فراموش نکنیم که به دلیل مبتنی بودن تابع فوق بر دورههای چهارساله کبیسه و عدم لحاظ کبیسه پنجساله تنها در یک بازه زمانی حدوداً 33 ساله صحیح عمل میکند یعنی از 1371 تا تقریباً 1404 هـ ش در قبل و بعد این دوره احتمال بروز یک روز خطا وجود دارد مگر اینکه تابع اصلاح شود و کبیسههای پنجساله نیز در آن محاسبه گردند!
پینوشت هفتم: (18 آبان 94) یک کد سریع نوشته شده است قطعاً بسیار سریعتر از کد فوق زیرا این حلقه یکساله را ندارد یک لوپ که همیشه از نخستین روز سال آغاز میشود تا به روز مورد نظر برسد توسط یکی از برنامهنویسان که در نشانی http://mamehdi.parsiblog.com/Posts/1 قابل دسترسیست تست کردم خروجیهای کاملاً صحیحی میداد فرمت ورودی و خروجی آن نیز کاملاً شبیه به همین تابع G2J است کافیست هنگام نیاز به تبدیل تاریخ و استفاده از تابع به جای dbo.G2J بنویسید dbo.SDAT همان پاسخ را البته که بسیار سریعتر خواهد داد با تشکر از این برادر بزرگوار! برچسبهای مرتبط با این نوشته:
|