مطالب
نحوه ایجاد یک گزارش فاکتور فروش توسط PdfReport
شکل زیر را که شبیه به یک فاکتور فروش است درنظر بگیرید:



نکته‌ای که در اینجا مدنظر است، دسترسی به عدد جمع آخر گزارش و سپس بر اساس آن، ساخت دو ستون اضافی ذیل جدول اصلی گزارش است که موارد مالیات، عوارض، جمع کل و مبلغ به حروف را نسبت به مثال‌های قبلی، اضافه‌تر دارد.

در ادامه کدهای کامل این مثال را مشاهده می‌کنید. همچنین این کد و کلاس‌های وابسته به آن مانند User و TransparentTemplate به سورس‌های کتابخانه PdfReport نیز اضافه شده‌اند.
using System;
using System.Collections.Generic;
using iTextSharp.text;
using iTextSharp.text.pdf;
using PdfReportSamples.Models;
using PdfReportSamples.Templates;
using PdfRpt.Core.Contracts;
using PdfRpt.Core.Helper;
using PdfRpt.FluentInterface;

namespace PdfReportSamples.Tax
{
    public class TaxPdfReport
    {
        public IPdfReportData CreatePdfReport()
        {
            return new PdfReport().DocumentPreferences(doc =>
            {
                doc.RunDirection(PdfRunDirection.RightToLeft);
                doc.Orientation(PageOrientation.Portrait);
                doc.PageSize(PdfPageSize.A4);
                doc.DocumentMetadata(new DocumentMetadata { Author = "Vahid", Application = "PdfRpt", Keywords = "Test", Subject = "Test Rpt", Title = "Test" });
            })
            .DefaultFonts(fonts =>
            {
                fonts.Path(AppPath.ApplicationPath + "\\fonts\\irsans.ttf",
                                  Environment.GetEnvironmentVariable("SystemRoot") + "\\fonts\\verdana.ttf");
            })
            .PagesFooter(footer =>
            {
                footer.DefaultFooter(DateTime.Now.ToString("MM/dd/yyyy"));
            })
            .PagesHeader(header =>
            {
                header.DefaultHeader(defaultHeader =>
                {
                    defaultHeader.ImagePath(AppPath.ApplicationPath + "\\Images\\01.png");
                    defaultHeader.Message("گزارش جدید ما");
                });
            })
            .MainTableTemplate(template =>
            {
                template.CustomTemplate(new TransparentTemplate());
            })
            .MainTablePreferences(table =>
            {
                table.ColumnsWidthsType(TableColumnWidthType.Relative);
            })
            .MainTableDataSource(dataSource =>
            {
                var listOfRows = new List<User>();
                for (int i = 0; i < 7; i++)
                {
                    listOfRows.Add(new User { Id = i, LastName = "نام خانوادگی " + i, Name = "نام " + i, Balance = i + 1000 });
                }
                dataSource.StronglyTypedList<User>(listOfRows);
            })
            .MainTableSummarySettings(summarySettings =>
            {
                summarySettings.OverallSummarySettings("جمع کل");
                summarySettings.PreviousPageSummarySettings("نقل از صفحه قبل");
                summarySettings.PageSummarySettings("جمع صفحه");
            })
            .MainTableColumns(columns =>
            {
                columns.AddColumn(column =>
                {
                    column.PropertyName("rowNo");
                    column.IsRowNumber(true);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(0);
                    column.Width(1);
                    column.HeaderCell("ردیف");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Id);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(1);
                    column.Width(2);
                    column.HeaderCell("شماره");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Name);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(2);
                    column.Width(3);
                    column.HeaderCell("نام");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.LastName);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(3);
                    column.Width(3);
                    column.HeaderCell("نام خانوادگی");
                });

                columns.AddColumn(column =>
                {
                    column.PropertyName<User>(x => x.Balance);
                    column.CellsHorizontalAlignment(HorizontalAlignment.Center);
                    column.IsVisible(true);
                    column.Order(4);
                    column.Width(2);
                    column.HeaderCell("موجودی");
                    column.ColumnItemsTemplate(template =>
                    {
                        template.TextBlock();
                        template.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                    column.AggregateFunction(aggregateFunction =>
                    {
                        aggregateFunction.NumericAggregateFunction(AggregateFunction.Sum);
                        aggregateFunction.DisplayFormatFormula(obj => obj == null ? string.Empty : string.Format("{0:n0}", obj));
                    });
                });

            })
            .MainTableEvents(events =>
            {
                events.DataSourceIsEmpty(message: "There is no data available to display.");

                events.MainTableAdded(args =>
                {
                    var balanceData = args.LastOverallAggregateValueOf<User>(u => u.Balance);
                    var balance = double.Parse(balanceData, System.Globalization.NumberStyles.AllowThousands);

                    var others = Math.Round(balance * 1.8 / 100);
                    var tax = Math.Round(balance * 2.2 / 100);
                    var total = balance + tax + others;

                    var taxTable = new PdfPTable(args.Table); // Create a clone of the MainTable's structure                   

                    taxTable.AddSimpleRow(
                        null /* null = empty cell */, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "مالیات";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", tax);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "عوارض";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", others);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "جمع کل";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", total);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });

                    taxTable.AddSimpleRow(
                        null, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "قابل پرداخت";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = total.NumberToText(Language.Persian) + " ریال";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                            cellProperties.PdfFontStyle = DocumentFontStyle.Bold;
                        });

                    args.PdfDoc.Add(taxTable);
                });
            })
            .Export(export =>
            {
                export.ToExcel();
            })
            .Generate(data => data.AsPdfFile(AppPath.ApplicationPath + "\\Pdf\\TaxReportSample.pdf"));
        }
    }
}
توضیحات:
تنها تفاوت این مثال با مثال‌های قبلی، کدهای مرتبط با متد events.MainTableAdded می‌باشند.
توسط متد args.LastOverallAggregateValueOf، می‌توان به مقدار نهایی متد تجمعی تعریف شده برای یک ستون خاص دسترسی یافت:
var balanceData = args.LastOverallAggregateValueOf<User>(u => u.Balance);
var balance = double.Parse(balanceData, System.Globalization.NumberStyles.AllowThousands);
سپس بر این اساس، امکان محاسبه مالیات و عوارض میسر می‌شود:
var others = Math.Round(balance * 1.8 / 100);
var tax = Math.Round(balance * 2.2 / 100);
var total = balance + tax + others;
در ادامه نیاز داریم تا یک جدول جدید را ذیل جدول اصلی ایجاد کنیم. نکته مهم این جدول جدید، هماهنگی عرض ستون‌های آن با ستون‌های جدول اصلی است. به همین منظور می‌توان از خاصیت args.Table جهت دسترسی به خواص جدول اصلی استفاده کرد و جدول جدیدی را ایجاد نمود:
var taxTable = new PdfPTable(args.Table);
از اینجا به بعد دیگر به عهده خودتان است. می‌توانید از دانش iTextSharp خود استفاده کرده و ردیف‌های این جدول جدید را پر کنید. یا اینکه می‌توانید از متد کمکی توکار AddSimpleRow به نحو زیر استفاده نمائید:
taxTable.AddSimpleRow(
                        null /* null = empty cell */, null, null,
                        (data, cellProperties) =>
                        {
                            data.Value = "مالیات";
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                        },
                        (data, cellProperties) =>
                        {
                            data.Value = string.Format("{0:n0}", tax);
                            cellProperties.PdfFont = args.PdfFont;
                            cellProperties.BorderColor = BaseColor.LIGHT_GRAY;
                            cellProperties.ShowBorder = true;
                        });
با توجه به اینکه قصد نداریم در سه ستون اول این جدول جدید، عنصری را نمایش دهیم، آن‌ها را با null مقدار دهی کرده و سپس ستون برچسب و ستون مقدار را اضافه می‌کنیم (آرگومان‌های این متد به صورت params تعریف شده‌اند. بنابراین هر تعداد ستون که نیاز باشد قابل تعریف است).
با مقدار دهی data، مقدار مورد نظر در آن سلول ثبت می‌گردد. با مقدار دهی خواص cellProperties، نوع قلم، جهت قرارگیری و سایر تنظیماتی را که ملاحظه می‌کنید، می‌توان اعمال کرد.
و در آخر لازم است که این جدول جدید را به شیء Document اضافه کنیم تا نمایش داده شود:
args.PdfDoc.Add(taxTable);

یک نکته:
متد NumberToText جزئی از کتابخانه PdfReport (تعریف شده در فضای نام PdfRpt.Core.Helper) است و برای نمایش رقم به حروف می‌تواند مورد استفاده قرار گیرد:
total.NumberToText(Language.Persian)

 
مطالب
مشکل همزمانی خواندن و به روز رسانی اطلاعات در برنامه‌های وب
فرض کنید در برنامه‌ی خود «کیف پولی» را طراحی کرده‌اید که بر اساس آن، کاربر می‌تواند خرید کند. این کیف پول، از Id کاربر و موجودی فعلی او تشکیل می‌شود:
CREATE TABLE accounts (
user_id INTEGER PRIMARY KEY,
balance INTEGER NOT NULL
);
و برای مثال موجودی فعلی کاربر 1، مقدار 300 است:
INSERT INTO accounts(user_id, balance)
VALUES (1, 300);
اکنون کوئری‌های متداول زیر را که از یک read و سپس update تشکیل شده‌اند، درنظر بگیرید:
DECLARE @amount INT;

SET @amount = (
SELECT balance
FROM accounts
WHERE user_id = 1
);

SELECT @amount as 'balance'

UPDATE accounts
SET balance =  @amount - 100
WHERE user_id = 1;

SELECT balance as 'balance after shopping'
FROM accounts
WHERE user_id = 1
- دو عمل read و سپس update صورت گرفته‌ی فوق، مربوط به یک درخواست خرید است.
- در اینجا مقدار متغیر amount در ابتدای کار، مساوی 300 است که مربوط به همان insert ابتدایی است.
- سپس از این مقدار در کوئری دومی (برای مثال حاصل از خرید شماره یک)، 100 واحد کم می‌شود (برای مثال قیمت کل خرید است).
- در این حالت نتیجه‌ی آن یا همان موجودی جدید کاربر، 200 خواهد بود.

معادل این عملیات در EF-Core چنین دستورات متداولی است:
var account1 =  context.Accounts.First(x => x.UserId == 1);
account1.Balance -= 100;
context.SaveChanges();

سؤال: اگر کوئری‌های فوق را در یک برنامه‌ی ذاتا چند ریسمانی وب، دوبار به صورت همزمان اجرا کنیم، یعنی دو عمل خرید موازی را شبیه سازی کنیم، چه اتفاقی رخ می‌دهد؟ آیا موجودی نهایی اینبار برای مثال 100 می‌شود (با فرض 300 بودن موجودی ابتدایی)؟
پاسخ خیر است! و آن‌را می‌توانید در تصویر زیر مشاهده کنید:



در اینجا برای شبیه سازی اجرای موازی دو کوئری، از دستور WAITFOR TIME استفاده شده‌است که برای برای آزمایش آن می‌توانید مقدار آن‌را به یک دقیقه بعد تنظیم کرده و سپس آن‌را در دو پنجره‌ی SQL server management studio اجرا کنید.
همانطور که مشاهده می‌کنید، با اجرای موازی این دو کوئری، یعنی دوبار خرید کردن همزمان، 100 واحد گم شده‌است ! به این مشکل همزمانی read و سپس update رخ داده، یک «race condition» گفته می‌شود و این روزها که مطالب منتشر شده‌ی از آسیب پذیری‌های برنامه‌های وب ایرانی را بررسی می‌کنم، این مورد در صدر آن‌ها قرار دارد!
علت اینجا است که عموما برنامه نویس‌ها، برنامه‌های وب را در یک تک سشن باز شده‌ی توسط مرورگر خود آزمایش می‌کنند و در این حالت، همه چیز خوب است و اعمال آن به ترتیب پیش می‌روند. اما فراموش می‌کنند که می‌توان قسمت‌های مختلف برنامه‌های وب را به صورت همزمان، موازی و چندباره نیز اجرا کرد؛ حتی اگر آن قسمت متعلق به یک کاربر باشد.


سؤال: آیا استفاده تراکنش‌ها این مشکل را حل نمی‌کنند؟!

عموما برنامه نویس‌ها تصور می‌کنند که می‌توانند تمام اینگونه مشکلات را با تراکنش‌ها حل کنند:



همانطور که مشاهده می‌کنید، اینبار هرچند هر دو عملیات خرید داخل BEGIN TRAN و COMMIT TRAN قرار گرفته‌اند، اما ... مشکل همزمانی هنوز پابرجا است! چون نوع پیش‌فرض تراکنش مورد استفاده، READ COMMITTED isolation level است و عدم دقت به آن ممکن است این تصور را ایجاد کند که با تعریف تراکنش‌ها، تمام مشکلات همزمانی برطرف می‌شوند.


راه‌حل‌های پیشنهادی جهت حل مشکل همزمانی عملیات read/update

برای حل مشکلات مرتبط با race condition و همزمانی درخواست‌های read/update، می‌توان از یکی از روش‌های زیر استفاده کرد:
الف) بجای اینکه یکبار کوئری read و یکبار کوئری update به صورت جداگانه صادر شوند، فقط یکبار کوئری update داشته باشیم.
ب) پیاده سازی Row level locking؛ در صورت پشتیبانی بانک اطلاعاتی مورد استفاده از آن
ج) استفاده از تراکنش‌هایی از نوع SERIALIZABLE
د) پیاده سازی optimistic locking

این موارد را در ادامه با توضیحات بیشتری بررسی می‌کنیم.


الف) پرهیز از خواندن و به روز رسانی جداگانه

بجای اینکه مانند اعمال فوق، یکبار select داشته باشیم و یکبار  update، بهتر است فقط یک دستور update بکارگرفته شود:
UPDATE accounts
SET balance =  balance - 100
WHERE user_id = 1;


اینبار با خلاصه شدن دو دستور select و update به یک دستور update، دیگر پس از دو خرید همزمان، 100 واحد گم شده مشاهده نمی‌شود (!) و موجودی نهایی صحیح است.


ب) پیاده سازی Row level locking

همیشه امکان تغییر عملیات مورد نیاز، به سادگی حالت الف نیست. در یک چنین حالت‌هایی جهت حداقل شدن تغییرات مورد نیاز، می‌توان از row level locking استفاده کرد:
WAITFOR TIME '13:47:00';

SET NOCOUNT, XACT_ABORT ON;

BEGIN TRAN;

DECLARE @amount INT;

SET @amount = (
 SELECT balance
 FROM accounts WITH (UPDLOCK, HOLDLOCK)
 WHERE user_id = 1
 );

SELECT @amount as 'initial user''s balance'

UPDATE accounts
SET balance =  @amount - 100
WHERE user_id = 1;

SELECT balance as 'user''s balance after shopping 1'
FROM accounts
WHERE user_id = 1;

COMMIT TRAN;



در اینجا اضافه شدن WITH (UPDLOCK, HOLDLOCK) را به Select تعریف شده، مشاهده می‌کنید که به آن‌ها locking hints هم گفته می‌شود و داخل BEGIN TRAN و COMMIT TRAN عمل می‌کنند (که نوع پیش‌فرض آن READ COMMITTED isolation level است). کار UPDLOCK، تبدیل shared lock پیش‌فرض، به update lock است و کار HOLDLOCK، نگه داشتن قفل صورت گرفته تا پایان کار تراکنش تعریف شده‌است.
با این تغییرات، هر تراکنش همزمان دیگری، تا زمانیکه قفل صورت گرفته‌ی بر روی ردیف select، رها نشود (یعنی تا زمانیکه تراکنش قفل کننده، به COMMIT TRAN برسد)، نمی‌تواند آن‌را تغییر دهد. به همین جهت است که در تصویر فوق، هرچند هر دو عملیات همزمان اجرا شده‌اند، اما یکی موجودی ابتدایی 300 را می‌بیند و دیگری پس از صبر کردن تا پایان تراکنش و رها شدن قفل، موجودی تغییر یافته‌ی جدیدی را مشاهده کرده و از آن استفاده می‌کند. به این ترتیب دیگر 100 واحدی که در اولین تصویر این مطلب مشاهده کردید، گم نشده‌است.


ج) استفاده از تراکنش‌هایی از نوع SERIALIZABLE

بجای استفاده از روش row level locking یاد شده، روش دیگری را که می‌توان استفاده کرد، تغییر نوع پیش‌فرض تراکنش مورد استفاده‌است. برای مثال اگر از یک SERIALIZABLE transaction استفاده کنیم؛ یعنی SET TRANSACTION ISOLATION LEVEL SERIALIZABLE  را در ابتدای کار ذکر کنیم و برای مثال دو تراکنش همزمان را اجرا کنیم، اگر در تراکنش اول اطلاعاتی خوانده شود، در هیچ تراکنش دیگری نمی‌توان این اطلاعات خوانده شده را تا پایان کار تراکنش اول، تغییر داد:




د) پیاده سازی optimistic locking

پیاده سازی optimistic locking و یا Optimistic concurrency control عموما در سمت برنامه رخ می‌دهد و توسط ORMها زیاد مورد استفاده قرار می‌گیرد؛ مانند اضافه کردن ستون اضافی version و یا timestamp به جداول تعریف شده. در این حالت تمام updateها به همراه یک where اضافی هستند تا بررسی کنند که آیا version دریافتی در حین خواندن ردیف در حال به روز رسانی، تغییر کرده‌است یا خیر؟ اگر تغییر کرده‌است، تراکنش را با خطایی خاتمه خواهند داد. این روش برخلاف حالت‌های ب و ج، حتی خارج از یک تراکنش نیز کار می‌کند و مشکلات قفل کردن طولانی مدت رکوردها توسط آن‌ها را به همراه ندارد.
نظرات مطالب
Identity و مباحث مربوطه (قسمت دوم) نحوه بدست آوردن مقادیر Identity
با تشکر از آقای نصیری و پاسخ مناسبی که ارائه کرده اند
در مورد استفاده از GUID به جای identity باید به یک نکته هم اشاره کنم که در بیشتر مواقع اگر مقدار GUIDی که به ازای یک فیلد UNIQUEIDENTIFIER تنظبم می‌کنید به صورت SEQUNTIAL نباشد باعث Fragment شدن ایندکس خواهد شد.
برای مقایسه بهتر بین  Fragmentation ایندکس مربوط به Identity و GUID به مثال زیر دقت کنید. هر دو مثال فیلد ID خود را به شکل Clustered Index دارند بعد از درج تعدادی رکورد مساوی در دو جدول Fragmentation مربوط به جدولی که دارای GUID است به شدت بالا است که این موضوع باعث کاهش کارایی خواهد شد

USE TEMPDB
GO
IF OBJECT_ID('TABLE_GUID')>0
DROP TABLE TABLE_GUID
GO
CREATE TABLE TABLE_GUID
(
ID UNIQUEIDENTIFIER PRIMARY KEY,
FirstName NVARCHAR(1000),
LastName NVARCHAR(1000)
)
GO
IF OBJECT_ID('TABLE_IDENTITY')>0
DROP TABLE TABLE_IDENTITY
GO
CREATE TABLE TABLE_IDENTITY
(
ID INT IDENTITY PRIMARY KEY,
FirstName NVARCHAR(1000),
LastName NVARCHAR(1000)
)
GO
INSERT INTO TABLE_GUID(ID,FirstName,LastName) VALUES (NEWID(),REPLICATE('FARID*',100),REPLICATE('Taheri*',100))
GO 10000

INSERT INTO TABLE_IDENTITY(FirstName,LastName) VALUES (REPLICATE('FARID*',100),REPLICATE('Taheri*',100))
GO 10000

--Fragmentation بررسی وضعیت 
SELECT * FROM sys.dm_db_index_physical_stats(DB_ID(),OBJECT_ID('TABLE_GUID'),NULL,NULL,'DETAILED')
DBCC SHOWCONTIG(TABLE_GUID)
GO
SELECT * FROM sys.dm_db_index_physical_stats(DB_ID(),OBJECT_ID('TABLE_IDENTITY'),NULL,NULL,'DETAILED')
DBCC SHOWCONTIG(TABLE_IDENTITY)
GO

خوب برای اینکه Fragmentation این نوع جداول را رفع کنید چند راه داریم
1- تولید GUID  به صورت Sequential (لازم می‌دانم اشاره کنم این قابلیت در SQL Server وجود دارد ولی مقدار تولید شده باید به شکل یک Default Constraint باشد که این موضوع نیازمند این است که شما اگر در سورس به این GUID نیاز پیدا کنید مجبور به زدن Select و... شوید. اگر بخواهید در سورس این کار را انجام دهید باید از Extentionهایی که برای اینکار وجود دارند استفاده کنید فکر کنم Nhibernate این حالت رو پشتیبانی کنه در مورد EF دقیقا اطلاع ندارم باید اهل فن نظر بدن)
2- تنظیم مقدار Fillfactor به ازای ایندکس
3-Rebuild و یا Reorganize دوره ای ایندکس
مطالب
روش محاسبه‌ی لحظه‌ی سال تحویل
سال قبل نتیجه‌ی جستجوی من برای یافتن فرمول محاسبه‌ی زمان سال تحویل، برای ارسال ایمیل‌های خودکار تبریک آن، در سایت‌های ایرانی حاصلی نداشت. اما واژه‌ی انگلیسی Equinox سرآغازی شد برای یافتن این الگوریتم.
نام علمی لحظه‌ی سال تحویل، Vernal Equinox است. Equinox به معنای نقطه‌ای است که یک فصل، به فصلی دیگر تبدیل می‌شود:


Equinox واژه‌ای است لاتین به معنای «شب‌های مساوی» و به این نکته اشاره دارد که در Equinox، طول شب و روز یکی می‌شوند. هر سال دارای دو Equinox است: vernal equinox و autumnal equinox (بهاری و پائیزی). البته باید درنظر داشت که Equinox بهاری در نیم کره‌ی شمالی بیشتر معنا پیدا می‌کند؛ زیرا در نیم کره‌ی جنوبی در همین زمان، پائیز شروع می‌شود.
بنابراین می‌توان enum زیر را برای تعریف این چهار ثابت رخدادهای خورشیدی تعریف کرد:
public enum SunEvent
{
    /// <summary>
    /// march equinox
    /// </summary>
    VernalEquinox,
 
    /// <summary>
    /// june solstice
    /// </summary>
    SummerSolstice,
 
    /// <summary>
    /// september equinox
    /// </summary>
    AutumnalEquinox,
 
    /// <summary>
    /// december solstice
    /// </summary>
    WinterSolstice
}

در ادامه برای محاسبه‌ی زمان equinox از فصل 27 کتاب Astronomical Algorithms کمک گرفته شده و تمام اعداد و ارقام و جداولی را که ملاحظه می‌کنید از این کتاب استخراج شده‌اند.
/// <summary>
/// Based on Jean Meeus book _Astronomical Algorithms_
/// </summary>
public static class EquinoxCalculator
{
    /// <summary>
    /// Degrees to Radians conversion factor.
    /// </summary>
    public static readonly double Deg2Radian = Math.PI / 180.0;
 
    public static bool ApproxEquals(double d1, double d2)
    {
        const double epsilon = 2.2204460492503131E-16;
        if (d1 == d2)
            return true;
        var tolerance = ((Math.Abs(d1) + Math.Abs(d2)) + 10.0) * epsilon;
        var difference = d1 - d2;
        return (-tolerance < difference && tolerance > difference);
    }
 
    /// <summary>
    /// Calculates time of the Equinox and Solstice.
    /// </summary>
    /// <param name="year">Year to calculate for.</param>
    /// <param name="sunEvent">Event to calculate.</param>
    /// <returns>Date and time event occurs as a fractional Julian Day.</returns>
    public static DateTime GetSunEventUtc(this int year, SunEvent sunEvent)
    {
        double y;
        double julianEphemerisDay;
 
        if (year >= 1000)
        {
            y = (Math.Floor((double)year) - 2000) / 1000;
 
            switch (sunEvent)
            {
                case SunEvent.VernalEquinox:
                    julianEphemerisDay = 2451623.80984 + 365242.37404 * y + 0.05169 * (y * y) - 0.00411 * (y * y * y) - 0.00057 * (y * y * y * y);
                    break;
                case SunEvent.SummerSolstice:
                    julianEphemerisDay = 2451716.56767 + 365241.62603 * y + 0.00325 * (y * y) - 0.00888 * (y * y * y) - 0.00030 * (y * y * y * y);
                    break;
                case SunEvent.AutumnalEquinox:
                    julianEphemerisDay = 2451810.21715 + 365242.01767 * y + 0.11575 * (y * y) - 0.00337 * (y * y * y) - 0.00078 * (y * y * y * y);
                    break;
                case SunEvent.WinterSolstice:
                    julianEphemerisDay = 2451900.05952 + 365242.74049 * y + 0.06223 * (y * y) - 0.00823 * (y * y * y) - 0.00032 * (y * y * y * y);
                    break;
                default:
                    throw new NotSupportedException();
            }
        }
        else
        {
            y = Math.Floor((double)year) / 1000;
 
            switch (sunEvent)
            {
                case SunEvent.VernalEquinox:
                    julianEphemerisDay = 1721139.29189 + 365242.13740 * y + 0.06134 * (y * y) - 0.00111 * (y * y * y) - 0.00071 * (y * y * y * y);
                    break;
                case SunEvent.SummerSolstice:
                    julianEphemerisDay = 1721233.25401 + 365241.72562 * y + 0.05323 * (y * y) - 0.00907 * (y * y * y) - 0.00025 * (y * y * y * y);
                    break;
                case SunEvent.AutumnalEquinox:
                    julianEphemerisDay = 1721325.70455 + 365242.49558 * y + 0.11677 * (y * y) - 0.00297 * (y * y * y) - 0.00074 * (y * y * y * y);
                    break;
                case SunEvent.WinterSolstice:
                    julianEphemerisDay = 1721414.39987 + 365242.88257 * y + 0.00769 * (y * y) - 0.00933 * (y * y * y) - 0.00006 * (y * y * y * y);
                    break;
                default:
                    throw new NotSupportedException();
            }
        }
 
        var julianCenturies = (julianEphemerisDay - 2451545.0) / 36525;
 
        var w = 35999.373 * julianCenturies - 2.47;
 
        var lambda = 1 + 0.0334 * Math.Cos(w * Deg2Radian) + 0.0007 * Math.Cos(2 * w * Deg2Radian);
 
        var sumOfPeriodicTerms = getSumOfPeriodicTerms(julianCenturies);
 
        return JulianToUtcDate(julianEphemerisDay + (0.00001 * sumOfPeriodicTerms / lambda));
    }
 
    /// <summary>
    /// Converts a fractional Julian Day to a .NET DateTime.
    /// </summary>
    /// <param name="julianDay">Fractional Julian Day to convert.</param>
    /// <returns>Date and Time in .NET DateTime format.</returns>
    public static DateTime JulianToUtcDate(double julianDay)
    {
        double a;
        int month, year;
 
        var j = julianDay + 0.5;
        var z = Math.Floor(j);
        var f = j - z;
 
        if (z >= 2299161)
        {
            var alpha = Math.Floor((z - 1867216.25) / 36524.25);
            a = z + 1 + alpha - Math.Floor(alpha / 4);
        }
        else
            a = z;
 
        var b = a + 1524;
 
        var c = Math.Floor((b - 122.1) / 365.25);
 
        var d = Math.Floor(365.25 * c);
 
        var e = Math.Floor((b - d) / 30.6001);
 
        var day = b - d - Math.Floor(30.6001 * e) + f;
 
        if (e < 14)
            month = (int)(e - 1.0);
        else if (ApproxEquals(e, 14) || ApproxEquals(e, 15))
            month = (int)(e - 13.0);
        else
            throw new NotSupportedException("Illegal month calculated.");
 
        if (month > 2)
            year = (int)(c - 4716.0);
        else if (month == 1 || month == 2)
            year = (int)(c - 4715.0);
        else
            throw new NotSupportedException("Illegal year calculated.");
 
        var span = TimeSpan.FromDays(day);
 
        return new DateTime(year, month, (int)day, span.Hours, span.Minutes,
            span.Seconds, span.Milliseconds, new GregorianCalendar(), DateTimeKind.Utc);
    }
 
    /// <summary>
    /// These values are from Table 27.C
    /// </summary>
    private static double getSumOfPeriodicTerms(double julianCenturies)
    {
        return 485 * Math.Cos(Deg2Radian * 324.96 + Deg2Radian * (1934.136 * julianCenturies))
               + 203 * Math.Cos(Deg2Radian * 337.23 + Deg2Radian * (32964.467 * julianCenturies))
               + 199 * Math.Cos(Deg2Radian * 342.08 + Deg2Radian * (20.186 * julianCenturies))
               + 182 * Math.Cos(Deg2Radian * 27.85 + Deg2Radian * (445267.112 * julianCenturies))
               + 156 * Math.Cos(Deg2Radian * 73.14 + Deg2Radian * (45036.886 * julianCenturies))
               + 136 * Math.Cos(Deg2Radian * 171.52 + Deg2Radian * (22518.443 * julianCenturies))
               + 77 * Math.Cos(Deg2Radian * 222.54 + Deg2Radian * (65928.934 * julianCenturies))
               + 74 * Math.Cos(Deg2Radian * 296.72 + Deg2Radian * (3034.906 * julianCenturies))
               + 70 * Math.Cos(Deg2Radian * 243.58 + Deg2Radian * (9037.513 * julianCenturies))
               + 58 * Math.Cos(Deg2Radian * 119.81 + Deg2Radian * (33718.147 * julianCenturies))
               + 52 * Math.Cos(Deg2Radian * 297.17 + Deg2Radian * (150.678 * julianCenturies))
               + 50 * Math.Cos(Deg2Radian * 21.02 + Deg2Radian * (2281.226 * julianCenturies))
               + 45 * Math.Cos(Deg2Radian * 247.54 + Deg2Radian * (29929.562 * julianCenturies))
               + 44 * Math.Cos(Deg2Radian * 325.15 + Deg2Radian * (31555.956 * julianCenturies))
               + 29 * Math.Cos(Deg2Radian * 60.93 + Deg2Radian * (4443.417 * julianCenturies))
               + 28 * Math.Cos(Deg2Radian * 155.12 + Deg2Radian * (67555.328 * julianCenturies))
               + 17 * Math.Cos(Deg2Radian * 288.79 + Deg2Radian * (4562.452 * julianCenturies))
               + 16 * Math.Cos(Deg2Radian * 198.04 + Deg2Radian * (62894.029 * julianCenturies))
               + 14 * Math.Cos(Deg2Radian * 199.76 + Deg2Radian * (31436.921 * julianCenturies))
               + 12 * Math.Cos(Deg2Radian * 95.39 + Deg2Radian * (14577.848 * julianCenturies))
               + 12 * Math.Cos(Deg2Radian * 287.11 + Deg2Radian * (31931.756 * julianCenturies))
               + 12 * Math.Cos(Deg2Radian * 320.81 + Deg2Radian * (34777.259 * julianCenturies))
               + 9 * Math.Cos(Deg2Radian * 227.73 + Deg2Radian * (1222.114 * julianCenturies))
               + 8 * Math.Cos(Deg2Radian * 15.45 + Deg2Radian * (16859.074 * julianCenturies));
    }
}
خروجی‌های زمانی ستاره شناسی، عموما بر اساس فرمت Julian Date است که آغاز آن  4713BCE January 1, 12 hours GMT است. به همین جهت در انتهای این مباحث، تبدیل Julian Date به DateTime دات نت را نیز ملاحظه می‌کنید. همچنین باید دقت داشت که خروجی نهایی بر اساس UTC است و برای زمان ایران، باید 3.5 ساعت به آن اضافه شود.

خروجی این الگوریتم را برای سال‌های 2014 تا 2022 به صورت ذیل مشاهده می‌کنید:
2014 -> 1392/12/29 20:28:08
2015 -> 1394/01/01 02:16:29
2016 -> 1395/01/01 08:01:21
2017 -> 1395/12/30 14:00:00
2018 -> 1396/12/29 19:46:10
2019 -> 1398/01/01 01:29:29
2020 -> 1399/01/01 07:21:03
2021 -> 1399/12/30 13:08:41
2022 -> 1400/12/29 19:04:37
برای نمونه زمان محاسبه شده‌ی 1394/01/01 02:16:29 با زمان رسمی اعلام شده‌ی ساعت 2 و 15 دقیقه و 10 ثانیه روز شنبه 1 فروردین 1394 و یا برای سال 93 زمان محاسبه شده‌ی 1392/12/29 20:28:08 با زمان رسمی ساعت ۲۰ و ۲۷ دقیقه و ۷ ثانیه پنجشنبه ۲۹ اسفند ۱۳۹۲، تقریبا برابری می‌کند.

کدهای کامل این پروژه را از اینجا می‌توانید دریافت کنید
 Equinox.zip
مطالب
استفاده از کتابخانه‌ی moment-jalaali در برنامه‌های Angular
چندی قبل مطلب «نمایش تاریخ شمسی توسط JavaScript در AngularJS» را در این سایت مطالعه کردید. در اینجا قصد داریم معادل Angular آن‌را تهیه کنیم (واژه‌ی AngularJS به نگارش‌های 1x اشاره می‌کند و Angular به تمام نگارش‌های پس از 2).


نصب پیشنیازهای کار با moment-jalaali

ابتدا نیاز است بسته‌ی npm این کتابخانه را نصب کنیم که به همراه فایل‌های js مرتبط با آن می‌باشد:
 npm install moment-jalaali --save

سپس جهت بهبود تجربه‌ی کاربری با آن در IDEهای امروزی، خصوصا VSCode، بهتر است typings آن‌را نیز نصب کنیم؛ تا علاوه بر داشتن Intellisense، بتوان به صورت strongly typed با آن کار کرد:
 npm install @types/moment-jalaali --save-dev


VSCode به صورت خودکار پوشه‌ی مخصوص node_modules\@types را تحت نظر قرار می‌دهد و نصب بسته‌های typings در آن، سبب بارگذاری آنی آن‌ها خواهد شد.
به علاوه اگر به فایل tsconfig.json واقع در ریشه‌ی پروژه نیز دقت کنید، وجود تعریف ذیل، امکان خوانده شدن این تعاریف را توسط کامپایلر TypeScript میسر می‌کند:
{
    "typeRoots": [
      "node_modules/@types"
    ]
}

 
نحوه‌ی کار Strongly Typed با کتابخانه‌ی moment-jalaali در برنامه‌های مبتنی بر TypeScript

پس از نصب پیشنیازهای یاد شده، ابتدا برای دسترسی به امکانات این کتابخانه، ماژول آن‌را import می‌کنیم:
import * as momentJalaali from "moment-jalaali";

export class MomentJalaaliTestComponent implements OnInit {
  now: string;
  nowLongDateFormat: string;
  nowExtraLongDateFormat: string;

  ngOnInit() {
    this.persianDateTests();
  }

  persianDateTests() {
    // https://github.com/jalaali/moment-jalaali
    momentJalaali.loadPersian(/*{ usePersianDigits: true }*/); // نمایش فارسی نام ماه‌ها، روزها و امثال آن

    this.now = momentJalaali().format("jYYYY/jMM/jDD HH:mm");
    this.nowLongDateFormat = momentJalaali().format("jD jMMMM jYYYY [ساعت] LT");
    this.nowExtraLongDateFormat = momentJalaali().format(
      "dddd، jD jMMMM jYYYY [ساعت] LT"
    );
  }
}
- پس از import ماژولی به نام moment-jalaali، اکنون نحوه‌ی استفاده‌ی از آن‌را در متد persianDateTests مشاهده می‌کنید.
- متد momentJalaali.loadPersian باید تنها یکبار فراخوانی شود. کار آن تبدیل نام‌های روزها و ماه‌های میلادی، به شمسی است.
- پس از آن از طریق متد format آن، می‌توان انواع و اقسام حالات مختلف را بررسی کرد که در اینجا سه نمونه را مشاهده می‌کنید.



نوشتن یک Pipe سفارشی برای تبدیل تاریخ‌های میلادی دریافتی از سرور به قالب شمسی

پس آشنا شدن با نحوه‌ی استفاده‌ی از این کتابخانه در یک برنامه‌ی تایپ‌اسکریپتی، تبدیل کردن آن به یک Pipe سفارشی بسیار ساده‌است. برای این منظور ابتدا یک Pipe جدید را به ماژول فرضی custom-pipe.module اضافه می‌کنیم:
 ng g p CustomPipe/moment-jalaali -m custom-pipe.module
با این محتوا:
import { Pipe, PipeTransform } from "@angular/core";

import * as momentJalaali from "moment-jalaali";

@Pipe({
  name: "momentJalaali"
})
export class MomentJalaaliPipe implements PipeTransform {
  transform(value: any, args?: any): any {
    return momentJalaali(value).format(args);
  }
}
در اینجا نیز ابتدا ماژول moment-jalaali تعریف شده‌است و سپس توسط آن، value به عنوان پارامتر متد momentJalaali و args به عنوان پارامتر متد format ارسال شده‌اند. در حین استفاده‌ی از Pipe، مقدار value همان تاریخ دریافتی است و args به فرمت خاصی که توسط استفاده کننده مشخص می‌شود، تنظیم خواهد شد.
به این ترتیب می‌توان یک چنین تبدیلات سمت کاربری را انجام داد که نمونه‌ای از خروجی آن‌را در تصویر فوق نیز ملاحظه می‌کنید:
<h2>Server side dates:</h2>
<div *ngFor="let date of dates">
  <span dir="ltr">{{date | momentJalaali:'jYYYY/jMM/jDD hh:mm' }}</span>,
  <span dir="rtl">{{date | momentJalaali:'jD jMMMM jYYYY [ساعت] LT'}}</span>
</div>


کدهای کامل این قسمت را از اینجا می‌توانید دریافت کنید.
نظرات مطالب
انتخاب پویای فیلد ها در LINQ
متد Select یک Overload دیگر دارد که Index را فراهم می‌کند : 
string[] weekDays = { "Monday", "Tuesday", "Wednesday", "Thursday", "Friday" };
weekDays.Select((day, index) => new { Day = day, Index = index })
        .Where(x => x.Day.Contains("s"))
        .Select(x => x.Index)
        .ToArray();
البته در این کد از Lambda Syntax استفاده شده که برای کد شما هم ممکن است.
نظرات مطالب
آموزش LINQ بخش سوم
روش ComapreTo رو هم تست کرده ام منتها این روش هم درست جواب نمی‌دهد. مثلا دیتای وارد شده در جدول زیر رو در نظر بگیرید:

حال اگر برنامه رو اجرا کنیم خروجی به صورت زیر خواهد بود:

همان طور که ملاحظه می‌فرمایید فقط کدهای 10 و 100 را در خروجی می‌بینیم و اعداد 15، 25، 55 که بین 10 و 100 هستند در خروجی نمایش داده نشده اند.
نظرسنجی‌ها
روزانه چقدر کافئین مصرف می‌کنید؟
کمتر از 100 میلی‌گرم
بین 100 تا 200 میلی‌گرم
بین 200 تا 300 میلی‌گرم
بین 300 تا 400 میلی‌گرم
بین 400 تا 500 میلی‌گرم
بین 500 تا 700 میلی‌گرم
بین 700 تا 1000 میلی‌گرم
بیشتر از 1000 میلی‌گرم
پاسخ به بازخورد‌های پروژه‌ها
نمایش چندی خطی یک فیلد
با سلام
با استفاده از سلول سفارشی مشکل padding شد، ممنون.

منتهی همانطور که مشاهده می‌کنید کماکان با تاریخ مشکل دارم. از تابع FixWeakCharacters نیز استفاده کرده ام ولی باز هم مشکل دارم، کد سلول سفارشی نیز این است : 

public PdfPCell RenderingCell(CellAttributes attributes)
        {
            var value = attributes.RowData.TableRowData.GetSafeStringValueOf(colName);
            var mainTable = new PdfGrid(1)
            {
                WidthPercentage = 100,
                RunDirection = PdfWriter.RUN_DIRECTION_RTL
            };
            string[] seperatedLine = value.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);
            foreach (var line in seperatedLine)
            {
                mainTable.AddSimpleRow((data, cellProperties) =>
                       {
                           cellProperties.BorderWidth = 1f;
                           cellProperties.BackgroundColor = attributes.BasicProperties.BackgroundColor;
                           data.Value = line.FixWeakCharacters();
                           cellProperties.RunDirection = PdfRunDirection.RightToLeft;
                           cellProperties.CellPadding = 2;
                           cellProperties.PdfFont = attributes.BasicProperties.PdfFont;
                           cellProperties.HorizontalAlignment = HorizontalAlignment.Left;
                       });
            }

            var cell = new PdfPCell
            {
                Border = 1,
                RunDirection = PdfWriter.RUN_DIRECTION_RTL,
                HorizontalAlignment = Element.ALIGN_RIGHT
            };
            cell.AddElement(mainTable);
            return cell;

        }

با تشکر

نظرات مطالب
بررسی Bad code smell ها: کلاس بزرگ
مطلبی رو  اینجا دیدم که به نظر طنز هست و مرتبط به مطلب جاری
In the C++ world we don’t create a class witch contains 5 methods and 2 fields like in Java or C#. We do more. A lot more. Our class are consequent and take a lot of stuff inside because it is interesting to have a project with 10 files of 100 KB rather than 300 files of 3 KB organized with fuzzy concepts. We do OOP better. It’s more compact, it’s real, it just works