Selaa lähdekoodia

1. 优化中国农历二十四节气获取
2. 时间范围快捷获取操作
3. 新增文本对比功能

懒得勤快 1 vuosi sitten
vanhempi
sitoutus
1c4c0765d6
29 muutettua tiedostoa jossa 6045 lisäystä ja 3489 poistoa
  1. 1 1
      BenchmarkTest/BenchmarkTest.csproj
  2. 2 2
      Directory.Build.props
  3. 1224 1232
      Masuit.Tools.Abstractions/DateTimeExt/ChineseCalendar.cs
  4. 560 424
      Masuit.Tools.Abstractions/DateTimeExt/DateTimeHelper.cs
  5. 1115 1091
      Masuit.Tools.Abstractions/Extensions/BaseType/IEnumerableExtensions.cs
  6. 734 734
      Masuit.Tools.Abstractions/Extensions/BaseType/StringExtensions.cs
  7. 21 1
      Masuit.Tools.Abstractions/HtmlSanitizer/HtmlSanitizer.cs
  8. 10 0
      Masuit.Tools.Abstractions/HtmlSanitizer/HtmlSanitizerOptions.cs
  9. 145 0
      Masuit.Tools.AspNetCore/TextDiff/BitapAlgorithm.cs
  10. 341 0
      Masuit.Tools.AspNetCore/TextDiff/DiffAlgorithm.cs
  11. 631 0
      Masuit.Tools.AspNetCore/TextDiff/DiffExtension.cs
  12. 8 0
      Masuit.Tools.AspNetCore/TextDiff/DiffOperation.cs
  13. 220 0
      Masuit.Tools.AspNetCore/TextDiff/DiffPatch.cs
  14. 55 0
      Masuit.Tools.AspNetCore/TextDiff/DifferBuilder.cs
  15. 139 0
      Masuit.Tools.AspNetCore/TextDiff/Extensions.cs
  16. 14 0
      Masuit.Tools.AspNetCore/TextDiff/HalfMatchResult.cs
  17. 5 0
      Masuit.Tools.AspNetCore/TextDiff/IsExternalInit.cs
  18. 12 0
      Masuit.Tools.AspNetCore/TextDiff/MatchOption.cs
  19. 341 0
      Masuit.Tools.AspNetCore/TextDiff/PatchExtension.cs
  20. 6 0
      Masuit.Tools.AspNetCore/TextDiff/PatchOption.cs
  21. 78 0
      Masuit.Tools.AspNetCore/TextDiff/SemanticsImmutableList.cs
  22. 51 0
      Masuit.Tools.AspNetCore/TextDiff/StringCompressor.cs
  23. 6 0
      Masuit.Tools.AspNetCore/TextDiff/TextDiffConstants.cs
  24. 44 0
      Masuit.Tools.AspNetCore/TextDiff/TextDiffer.cs
  25. 238 0
      Masuit.Tools.AspNetCore/TextDiff/TextUtil.cs
  26. 1 1
      Masuit.Tools/Masuit.Tools.csproj
  27. 1 1
      NetCoreTest/NetCoreTest.csproj
  28. 41 1
      README.md
  29. 1 1
      Test/Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.csproj

+ 1 - 1
BenchmarkTest/BenchmarkTest.csproj

@@ -7,7 +7,7 @@
   </PropertyGroup>
 
   <ItemGroup>
-    <PackageReference Include="BenchmarkDotNet" Version="0.13.12" />
+    <PackageReference Include="BenchmarkDotNet" Version="0.14.0" />
   </ItemGroup>
 
   <ItemGroup>

+ 2 - 2
Directory.Build.props

@@ -1,6 +1,6 @@
-<Project>
+<Project>
  <PropertyGroup>
-   <Version>2024.4.5</Version>
+   <Version>2024.5</Version>
    <Deterministic>true</Deterministic>
  </PropertyGroup>
 </Project>

+ 1224 - 1232
Masuit.Tools.Abstractions/DateTimeExt/ChineseCalendar.cs

@@ -4,1241 +4,1233 @@ using System.Linq;
 
 namespace Masuit.Tools.DateTimeExt
 {
-    /// <summary>
-    /// 中国农历类 支持 1900.1.31日起至 2069.12.31日止的数据
-    /// </summary>
-    /// <remarks>
-    /// 本程序使用数据来源于网上的万年历查询,并综合了一些其它数据
-    /// </remarks>
-    public class ChineseCalendar
-    {
-        private class ChineseCalendarException(string msg) : Exception(msg);
-
-        private readonly DateTime _datetime;
-
-        #region 基础数据
-
-        private const int MinYear = 1900;
-        private const int MaxYear = 2100;
-        private static readonly DateTime MinDay = new DateTime(1900, 1, 30);
-        private const int GanZhiStartYear = 1864; //干支计算起始年
-        private static readonly DateTime GanZhiStartDay = new DateTime(1899, 12, 22); //起始日
-        private const string HzNum = "零一二三四五六七八九";
-        private const int AnimalStartYear = 1900; //1900年为鼠年
-        private static readonly DateTime ChineseConstellationReferDay = new DateTime(2007, 9, 13); //28星宿参考值,本日为角
-
-        /// <summary>
-        /// 来源于网上的农历数据
-        /// </summary>
-        /// <remarks>
-        /// 数据结构如下,共使用17位数据<br/>
-        /// 说明如下:<br/>
-        ///1-4:判断当年是否为闰年,若为闰年,则为闰年的月份,反之为0;<br/>
-        ///5-16:为除了闰月外的正常月份是大月还是小月,1为30天,0为29天。(注意:1月对应第16位,2月对应第15位……12月对应第5位)<br/>
-        ///17-20: 表示闰月是大月还是小月,若为1,则为大月,若为0,则为小月。(注意:仅当存在闰月的情况下有意义)<br/>
-        /// -<br/>
-        ///举例说明:<br/>
-        ///例一:0x04bd8<br/>
-        ///对应二进制:0000    0100     1011    1101    1000<br/>
-        ///则表示当年有闰月8月,且闰月为小月29天<br/>
-        ///该年1-12月的天数为:29    30   29   29   30   29   30    29(闰月)  30     30   29    30<br/>
-        ///例二:0x04ae0<br/>
-        ///对应二进制:0000    0100     1010    1110    0000<br/>
-        ///则表示当年没有闰月<br/>
-        ///该年1-12月的天数为:29    30    29     29    30    29    30    29    30   30    30    29<br/>
-        ///补充闰月介绍:<br/>
-        ///闰月是阴阳历中为使历年平均长度接近会归年而增设的月。阴阳历以朔望月的长度(29.5306日)为一个月的平均值,全年12个月,同回归年(365.2422日)相差约10日21时,故需要置闰,三年闰一个月,五年闰二个月,十九年闰七个月。阴阳历以朔望月的长度(29.5306日)为一个月的平均值,全年12个月,同回归年(365.2422日)相差约10日21时,故需要置闰,三年闰一个月,五年闰二个月,十九年闰七个月。
-        ///</remarks>
-        private static readonly int[] LunarDateArray =
-        {
-            0x04BD8, 0x04AE0, 0x0A570, 0x054D5, 0x0D260, 0x0D950, 0x16554, 0x056A0, 0x09AD0, 0x055D2, 0x04AE0,
-            0x0A5B6, 0x0A4D0, 0x0D250, 0x1D255, 0x0B540, 0x0D6A0, 0x0ADA2, 0x095B0, 0x14977, 0x04970, 0x0A4B0,
-            0x0B4B5, 0x06A50, 0x06D40, 0x1AB54, 0x02B60, 0x09570, 0x052F2, 0x04970, 0x06566, 0x0D4A0, 0x0EA50,
-            0x06E95, 0x05AD0, 0x02B60, 0x186E3, 0x092E0, 0x1C8D7, 0x0C950, 0x0D4A0, 0x1D8A6, 0x0B550, 0x056A0,
-            0x1A5B4, 0x025D0, 0x092D0, 0x0D2B2, 0x0A950, 0x0B557, 0x06CA0, 0x0B550, 0x15355, 0x04DA0, 0x0A5B0,
-            0x14573, 0x052B0, 0x0A9A8, 0x0E950, 0x06AA0, 0x0AEA6, 0x0AB50, 0x04B60, 0x0AAE4, 0x0A570, 0x05260,
-            0x0F263, 0x0D950, 0x05B57, 0x056A0, 0x096D0, 0x04DD5, 0x04AD0, 0x0A4D0, 0x0D4D4, 0x0D250, 0x0D558,
-            0x0B540, 0x0B6A0, 0x195A6, 0x095B0, 0x049B0, 0x0A974, 0x0A4B0, 0x0B27A, 0x06A50, 0x06D40, 0x0AF46,
-            0x0AB60, 0x09570, 0x04AF5, 0x04970, 0x064B0, 0x074A3, 0x0EA50, 0x06B58, 0x055C0, 0x0AB60, 0x096D5,
-            0x092E0, 0x0C960, 0x0D954, 0x0D4A0, 0x0DA50, 0x07552, 0x056A0, 0x0ABB7, 0x025D0, 0x092D0, 0x0CAB5,
-            0x0A950, 0x0B4A0, 0x0BAA4, 0x0AD50, 0x055D9, 0x04BA0, 0x0A5B0, 0x15176, 0x052B0, 0x0A930, 0x07954,
-            0x06AA0, 0x0AD50, 0x05B52, 0x04B60, 0x0A6E6, 0x0A4E0, 0x0D260, 0x0EA65, 0x0D530, 0x05AA0, 0x076A3,
-            0x096D0, 0x04BD7, 0x04AD0, 0x0A4D0, 0x1D0B6, 0x0D250, 0x0D520, 0x0DD45, 0x0B5A0, 0x056D0, 0x055B2,
-            0x049B0, 0x0A577, 0x0A4B0, 0x0AA50, 0x1B255, 0x06D20, 0x0ADA0, 0x14B63, 0x9370, 0x49f8, 0x4970,
-            0x64b0, 0x168a6, 0xea50, 0x6aa0, 0x1a6c4, 0xaae0, 0x92e0, 0xd2e3, 0xc960, 0xd557, 0xd4a0, 0xda50,
-            0x5d55, 0x56a0, 0xa6d0, 0x55d4, 0x52d0, 0xa9b8, 0xa950, 0xb4a0, 0xb6a6, 0xad50, 0x55a0, 0xaba4,
-            0xa5b0, 0x52b0, 0xb273, 0x6930, 0x7337, 0x6aa0, 0xad50, 0x14b55, 0x4b60, 0xa570, 0x54e4, 0xd160,
-            0xe968, 0xd520, 0xdaa0, 0x16aa6, 0x56d0, 0x4ae0, 0xa9d4, 0xa2d0, 0xd150, 0xf252, 0xd520
-        };
-
-        /// <summary>
-        /// 星座
-        /// </summary>
-        private static readonly string[] ConstellationName =
-        {
-            "白羊座", "金牛座", "双子座", "巨蟹座",
-            "狮子座", "处女座", "天秤座", "天蝎座",
-            "射手座", "摩羯座", "水瓶座", "双鱼座"
-        };
-
-        /// <summary>
-        /// 二十八星宿
-        /// </summary>
-        private static readonly string[] ChineseConstellationName =
-        {
+	/// <summary>
+	/// 中国农历类 支持 1900.1.31日起至 2069.12.31日止的数据
+	/// </summary>
+	/// <remarks>
+	/// 本程序使用数据来源于网上的万年历查询,并综合了一些其它数据
+	/// </remarks>
+	public class ChineseCalendar
+	{
+		private class ChineseCalendarException(string msg) : Exception(msg);
+
+		private readonly DateTime _datetime;
+
+		#region 基础数据
+
+		private const int MinYear = 1900;
+		private const int MaxYear = 2100;
+		private static readonly DateTime MinDay = new DateTime(1900, 1, 30);
+		private const int GanZhiStartYear = 1864; //干支计算起始年
+		private static readonly DateTime GanZhiStartDay = new DateTime(1899, 12, 22); //起始日
+		private const string HzNum = "零一二三四五六七八九";
+		private const int AnimalStartYear = 1900; //1900年为鼠年
+		private static readonly DateTime ChineseConstellationReferDay = new DateTime(2007, 9, 13); //28星宿参考值,本日为角
+
+		/// <summary>
+		/// 来源于网上的农历数据
+		/// </summary>
+		/// <remarks>
+		/// 数据结构如下,共使用17位数据<br/>
+		/// 说明如下:<br/>
+		///1-4:判断当年是否为闰年,若为闰年,则为闰年的月份,反之为0;<br/>
+		///5-16:为除了闰月外的正常月份是大月还是小月,1为30天,0为29天。(注意:1月对应第16位,2月对应第15位……12月对应第5位)<br/>
+		///17-20: 表示闰月是大月还是小月,若为1,则为大月,若为0,则为小月。(注意:仅当存在闰月的情况下有意义)<br/>
+		/// -<br/>
+		///举例说明:<br/>
+		///例一:0x04bd8<br/>
+		///对应二进制:0000    0100     1011    1101    1000<br/>
+		///则表示当年有闰月8月,且闰月为小月29天<br/>
+		///该年1-12月的天数为:29    30   29   29   30   29   30    29(闰月)  30     30   29    30<br/>
+		///例二:0x04ae0<br/>
+		///对应二进制:0000    0100     1010    1110    0000<br/>
+		///则表示当年没有闰月<br/>
+		///该年1-12月的天数为:29    30    29     29    30    29    30    29    30   30    30    29<br/>
+		///补充闰月介绍:<br/>
+		///闰月是阴阳历中为使历年平均长度接近会归年而增设的月。阴阳历以朔望月的长度(29.5306日)为一个月的平均值,全年12个月,同回归年(365.2422日)相差约10日21时,故需要置闰,三年闰一个月,五年闰二个月,十九年闰七个月。阴阳历以朔望月的长度(29.5306日)为一个月的平均值,全年12个月,同回归年(365.2422日)相差约10日21时,故需要置闰,三年闰一个月,五年闰二个月,十九年闰七个月。
+		///</remarks>
+		private static readonly int[] LunarDateArray =
+		{
+			0x04BD8, 0x04AE0, 0x0A570, 0x054D5, 0x0D260, 0x0D950, 0x16554, 0x056A0, 0x09AD0, 0x055D2, 0x04AE0,
+			0x0A5B6, 0x0A4D0, 0x0D250, 0x1D255, 0x0B540, 0x0D6A0, 0x0ADA2, 0x095B0, 0x14977, 0x04970, 0x0A4B0,
+			0x0B4B5, 0x06A50, 0x06D40, 0x1AB54, 0x02B60, 0x09570, 0x052F2, 0x04970, 0x06566, 0x0D4A0, 0x0EA50,
+			0x06E95, 0x05AD0, 0x02B60, 0x186E3, 0x092E0, 0x1C8D7, 0x0C950, 0x0D4A0, 0x1D8A6, 0x0B550, 0x056A0,
+			0x1A5B4, 0x025D0, 0x092D0, 0x0D2B2, 0x0A950, 0x0B557, 0x06CA0, 0x0B550, 0x15355, 0x04DA0, 0x0A5B0,
+			0x14573, 0x052B0, 0x0A9A8, 0x0E950, 0x06AA0, 0x0AEA6, 0x0AB50, 0x04B60, 0x0AAE4, 0x0A570, 0x05260,
+			0x0F263, 0x0D950, 0x05B57, 0x056A0, 0x096D0, 0x04DD5, 0x04AD0, 0x0A4D0, 0x0D4D4, 0x0D250, 0x0D558,
+			0x0B540, 0x0B6A0, 0x195A6, 0x095B0, 0x049B0, 0x0A974, 0x0A4B0, 0x0B27A, 0x06A50, 0x06D40, 0x0AF46,
+			0x0AB60, 0x09570, 0x04AF5, 0x04970, 0x064B0, 0x074A3, 0x0EA50, 0x06B58, 0x055C0, 0x0AB60, 0x096D5,
+			0x092E0, 0x0C960, 0x0D954, 0x0D4A0, 0x0DA50, 0x07552, 0x056A0, 0x0ABB7, 0x025D0, 0x092D0, 0x0CAB5,
+			0x0A950, 0x0B4A0, 0x0BAA4, 0x0AD50, 0x055D9, 0x04BA0, 0x0A5B0, 0x15176, 0x052B0, 0x0A930, 0x07954,
+			0x06AA0, 0x0AD50, 0x05B52, 0x04B60, 0x0A6E6, 0x0A4E0, 0x0D260, 0x0EA65, 0x0D530, 0x05AA0, 0x076A3,
+			0x096D0, 0x04BD7, 0x04AD0, 0x0A4D0, 0x1D0B6, 0x0D250, 0x0D520, 0x0DD45, 0x0B5A0, 0x056D0, 0x055B2,
+			0x049B0, 0x0A577, 0x0A4B0, 0x0AA50, 0x1B255, 0x06D20, 0x0ADA0, 0x14B63, 0x9370, 0x49f8, 0x4970,
+			0x64b0, 0x168a6, 0xea50, 0x6aa0, 0x1a6c4, 0xaae0, 0x92e0, 0xd2e3, 0xc960, 0xd557, 0xd4a0, 0xda50,
+			0x5d55, 0x56a0, 0xa6d0, 0x55d4, 0x52d0, 0xa9b8, 0xa950, 0xb4a0, 0xb6a6, 0xad50, 0x55a0, 0xaba4,
+			0xa5b0, 0x52b0, 0xb273, 0x6930, 0x7337, 0x6aa0, 0xad50, 0x14b55, 0x4b60, 0xa570, 0x54e4, 0xd160,
+			0xe968, 0xd520, 0xdaa0, 0x16aa6, 0x56d0, 0x4ae0, 0xa9d4, 0xa2d0, 0xd150, 0xf252, 0xd520
+		};
+
+		/// <summary>
+		/// 星座
+		/// </summary>
+		private static readonly string[] ConstellationName =
+		{
+			"白羊座", "金牛座", "双子座", "巨蟹座",
+			"狮子座", "处女座", "天秤座", "天蝎座",
+			"射手座", "摩羯座", "水瓶座", "双鱼座"
+		};
+
+		/// <summary>
+		/// 二十八星宿
+		/// </summary>
+		private static readonly string[] ChineseConstellationName =
+		{
             //四           五          六         日          一         二         三
             "角木蛟", "亢金龙","氐土貉" , "房日兔", "心月狐", "尾火虎", "箕水豹",
-            "斗木獬", "牛金牛", "女土蝠", "虚日鼠", "危月燕", "室火猪", "壁水獝",
-            "奎木狼", "娄金狗", "胃土彘", "昴日鸡", "毕月乌", "觜火猴", "参水猿",
-            "井木犴", "鬼金羊", "柳土獐", "星日马", "张月鹿", "翼火蛇", "轸水蚓"
-        };
-
-        #region 节气数据
-
-        /// <summary>
-        /// 节气数据
-        /// </summary>
-        private static readonly string[] SolarTerm =
-        {
-            "小寒", "大寒", "立春", "雨水", "惊蛰", "春分",
-            "清明", "谷雨", "立夏", "小满", "芒种", "夏至",
-            "小暑", "大暑", "立秋", "处暑", "白露", "秋分",
-            "寒露", "霜降", "立冬", "小雪", "大雪", "冬至"
-        };
-
-        private static readonly int[] STermInfo =
-        {
-            0,
-            21208,
-            42467,
-            63836,
-            85337,
-            107014,
-            128867,
-            150921,
-            173149,
-            195551,
-            218072,
-            240693,
-            263343,
-            285989,
-            308563,
-            331033,
-            353350,
-            375494,
-            397447,
-            419210,
-            440795,
-            462224,
-            483532,
-            504758
-        };
-
-        #endregion 节气数据
-
-        #region 农历相关数据
-
-        private const string TianGan = "甲乙丙丁戊己庚辛壬癸";
-        private const string DiZhi = "子丑寅卯辰巳午未申酉戌亥";
-        private const string AnimalStr = "鼠牛虎兔龙蛇马羊猴鸡狗猪";
-        private const string NStr1 = "日一二三四五六七八九";
-        private const string NStr2 = "初十廿卅";
-
-        private static readonly string[] MonthString =
-        {
-            "出错", "正月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "冬月", "腊月"
-        };
-
-        #endregion 农历相关数据
-
-        #region 节日数据
-
-        /// <summary>
-        /// 自定义的工作日
-        /// </summary>
-        public static HashSet<DateTime> CustomWorkDays { get; } = [];
-
-        /// <summary>
-        /// 自定义的节假日
-        /// </summary>
-        public static Dictionary<DateTime, string> CustomHolidays { get; } = new();
-
-        /// <summary>
-        /// 按公历计算的通用节假日
-        /// </summary>
-        private static HashSet<DateInfoStruct> SolarHolidayInfo { get; } =
-        [
-            new DateInfoStruct(1, 1, 1, "元旦"),
-            new DateInfoStruct(2, 2, 0, "世界湿地日"),
-            new DateInfoStruct(2, 10, 0, "国际气象节"),
-            new DateInfoStruct(2, 14, 0, "情人节"),
-            new DateInfoStruct(3, 1, 0, "国际海豹日"),
-            new DateInfoStruct(3, 5, 0, "学雷锋纪念日"),
-            new DateInfoStruct(3, 8, 0, "妇女节"),
-            new DateInfoStruct(3, 12, 0, "植树节 孙中山逝世纪念日"),
-            new DateInfoStruct(3, 14, 0, "国际警察日"),
-            new DateInfoStruct(3, 15, 0, "消费者权益日"),
-            new DateInfoStruct(3, 17, 0, "中国国医节 国际航海日"),
-            new DateInfoStruct(3, 21, 0, "世界森林日 消除种族歧视国际日 世界儿歌日"),
-            new DateInfoStruct(3, 22, 0, "世界水日"),
-            new DateInfoStruct(3, 24, 0, "世界防治结核病日"),
-            new DateInfoStruct(4, 1, 0, "愚人节"),
-            new DateInfoStruct(4, 5, 1, "清明节"),
-            new DateInfoStruct(4, 7, 0, "世界卫生日"),
-            new DateInfoStruct(4, 22, 0, "世界地球日"),
-            new DateInfoStruct(5, 1, 1, "劳动节"),
-            new DateInfoStruct(5, 4, 0, "青年节"),
-            new DateInfoStruct(5, 8, 0, "世界红十字日"),
-            new DateInfoStruct(5, 12, 0, "国际护士节"),
-            new DateInfoStruct(5, 31, 0, "世界无烟日"),
-            new DateInfoStruct(6, 1, 0, "国际儿童节"),
-            new DateInfoStruct(6, 5, 0, "世界环境保护日"),
-            new DateInfoStruct(6, 26, 0, "国际禁毒日"),
-            new DateInfoStruct(7, 1, 0, "建党节 香港回归纪念 世界建筑日"),
-            new DateInfoStruct(7, 11, 0, "世界人口日"),
-            new DateInfoStruct(8, 1, 0, "建军节"),
-            new DateInfoStruct(8, 8, 0, "中国男子节 父亲节"),
-            new DateInfoStruct(8, 15, 0, "抗日战争胜利纪念"),
-            new DateInfoStruct(9, 9, 0, "  逝世纪念"),
-            new DateInfoStruct(9, 10, 0, "教师节"),
-            new DateInfoStruct(9, 18, 0, "九·一八事变纪念日"),
-            new DateInfoStruct(9, 20, 0, "国际爱牙日"),
-            new DateInfoStruct(9, 27, 0, "世界旅游日"),
-            new DateInfoStruct(9, 28, 0, "孔子诞辰"),
-            new DateInfoStruct(10, 1, 7, "国庆节 国际音乐日"),
-            new DateInfoStruct(10, 24, 0, "联合国日"),
-            new DateInfoStruct(11, 10, 0, "世界青年节"),
-            new DateInfoStruct(11, 12, 0, "孙中山诞辰纪念"),
-            new DateInfoStruct(12, 1, 0, "世界艾滋病日"),
-            new DateInfoStruct(12, 3, 0, "世界残疾人日"),
-            new DateInfoStruct(12, 20, 0, "澳门回归纪念"),
-            new DateInfoStruct(12, 24, 0, "平安夜"),
-            new DateInfoStruct(12, 25, 0, "圣诞节"),
-            new DateInfoStruct(12, 26, 0, " 诞辰纪念")
-        ];
-
-        /// <summary>
-        /// 按农历计算的通用节假日
-        /// </summary>
-        private static HashSet<DateInfoStruct> LunarHolidayInfo { get; } =
-        [
-            new DateInfoStruct(1, 1, 6, "春节"),
-            new DateInfoStruct(1, 15, 0, "元宵节"),
-            new DateInfoStruct(5, 5, 1, "端午节"),
-            new DateInfoStruct(7, 7, 0, "七夕情人节"),
-            new DateInfoStruct(7, 15, 0, "中元节 盂兰盆节"),
-            new DateInfoStruct(8, 15, 1, "中秋节"),
-            new DateInfoStruct(9, 9, 0, "重阳节"),
-            new DateInfoStruct(12, 8, 0, "腊八节"),
-            new DateInfoStruct(12, 23, 0, "北方小年(扫房)"),
-            new DateInfoStruct(12, 24, 0, "南方小年(掸尘)")
+			"斗木獬", "牛金牛", "女土蝠", "虚日鼠", "危月燕", "室火猪", "壁水獝",
+			"奎木狼", "娄金狗", "胃土彘", "昴日鸡", "毕月乌", "觜火猴", "参水猿",
+			"井木犴", "鬼金羊", "柳土獐", "星日马", "张月鹿", "翼火蛇", "轸水蚓"
+		};
+
+		#region 节气数据
+
+		/// <summary>
+		/// 节气数据
+		/// </summary>
+		private static readonly string[] SolarTerm =
+		{
+			"小寒", "大寒", "立春", "雨水", "惊蛰", "春分",
+			"清明", "谷雨", "立夏", "小满", "芒种", "夏至",
+			"小暑", "大暑", "立秋", "处暑", "白露", "秋分",
+			"寒露", "霜降", "立冬", "小雪", "大雪", "冬至"
+		};
+
+		private static readonly int[] STermInfo =
+		{
+			0,
+			21208,
+			42467,
+			63836,
+			85337,
+			107014,
+			128867,
+			150921,
+			173149,
+			195551,
+			218072,
+			240693,
+			263343,
+			285989,
+			308563,
+			331033,
+			353350,
+			375494,
+			397447,
+			419210,
+			440795,
+			462224,
+			483532,
+			504758
+		};
+
+		#endregion 节气数据
+
+		#region 农历相关数据
+
+		private const string TianGan = "甲乙丙丁戊己庚辛壬癸";
+		private const string DiZhi = "子丑寅卯辰巳午未申酉戌亥";
+		private const string AnimalStr = "鼠牛虎兔龙蛇马羊猴鸡狗猪";
+		private const string NStr1 = "日一二三四五六七八九";
+		private const string NStr2 = "初十廿卅";
+
+		private static readonly string[] MonthString =
+		{
+			"出错", "正月", "二月", "三月", "四月", "五月", "六月", "七月", "八月", "九月", "十月", "冬月", "腊月"
+		};
+
+		#endregion 农历相关数据
+
+		#region 节日数据
+
+		/// <summary>
+		/// 自定义的工作日
+		/// </summary>
+		public static HashSet<DateTime> CustomWorkDays { get; } = [];
+
+		/// <summary>
+		/// 自定义的节假日
+		/// </summary>
+		public static Dictionary<DateTime, string> CustomHolidays { get; } = new();
+
+		/// <summary>
+		/// 按公历计算的通用节假日
+		/// </summary>
+		private static HashSet<DateInfoStruct> SolarHolidayInfo { get; } =
+		[
+			new DateInfoStruct(1, 1, 1, "元旦"),
+			new DateInfoStruct(2, 2, 0, "世界湿地日"),
+			new DateInfoStruct(2, 10, 0, "国际气象节"),
+			new DateInfoStruct(2, 14, 0, "情人节"),
+			new DateInfoStruct(3, 1, 0, "国际海豹日"),
+			new DateInfoStruct(3, 5, 0, "学雷锋纪念日"),
+			new DateInfoStruct(3, 8, 0, "妇女节"),
+			new DateInfoStruct(3, 12, 0, "植树节 孙中山逝世纪念日"),
+			new DateInfoStruct(3, 14, 0, "国际警察日"),
+			new DateInfoStruct(3, 15, 0, "消费者权益日"),
+			new DateInfoStruct(3, 17, 0, "中国国医节 国际航海日"),
+			new DateInfoStruct(3, 21, 0, "世界森林日 消除种族歧视国际日 世界儿歌日"),
+			new DateInfoStruct(3, 22, 0, "世界水日"),
+			new DateInfoStruct(3, 24, 0, "世界防治结核病日"),
+			new DateInfoStruct(4, 1, 0, "愚人节"),
+			new DateInfoStruct(4, 5, 1, "清明节"),
+			new DateInfoStruct(4, 7, 0, "世界卫生日"),
+			new DateInfoStruct(4, 22, 0, "世界地球日"),
+			new DateInfoStruct(5, 1, 1, "劳动节"),
+			new DateInfoStruct(5, 4, 0, "青年节"),
+			new DateInfoStruct(5, 8, 0, "世界红十字日"),
+			new DateInfoStruct(5, 12, 0, "国际护士节"),
+			new DateInfoStruct(5, 31, 0, "世界无烟日"),
+			new DateInfoStruct(6, 1, 0, "国际儿童节"),
+			new DateInfoStruct(6, 5, 0, "世界环境保护日"),
+			new DateInfoStruct(6, 26, 0, "国际禁毒日"),
+			new DateInfoStruct(7, 1, 0, "建党节 香港回归纪念 世界建筑日"),
+			new DateInfoStruct(7, 11, 0, "世界人口日"),
+			new DateInfoStruct(8, 1, 0, "建军节"),
+			new DateInfoStruct(8, 8, 0, "中国男子节 父亲节"),
+			new DateInfoStruct(8, 15, 0, "抗日战争胜利纪念"),
+			new DateInfoStruct(9, 9, 0, "  逝世纪念"),
+			new DateInfoStruct(9, 10, 0, "教师节"),
+			new DateInfoStruct(9, 18, 0, "九·一八事变纪念日"),
+			new DateInfoStruct(9, 20, 0, "国际爱牙日"),
+			new DateInfoStruct(9, 27, 0, "世界旅游日"),
+			new DateInfoStruct(9, 28, 0, "孔子诞辰"),
+			new DateInfoStruct(10, 1, 7, "国庆节 国际音乐日"),
+			new DateInfoStruct(10, 24, 0, "联合国日"),
+			new DateInfoStruct(11, 10, 0, "世界青年节"),
+			new DateInfoStruct(11, 12, 0, "孙中山诞辰纪念"),
+			new DateInfoStruct(12, 1, 0, "世界艾滋病日"),
+			new DateInfoStruct(12, 3, 0, "世界残疾人日"),
+			new DateInfoStruct(12, 20, 0, "澳门回归纪念"),
+			new DateInfoStruct(12, 24, 0, "平安夜"),
+			new DateInfoStruct(12, 25, 0, "圣诞节"),
+			new DateInfoStruct(12, 26, 0, " 诞辰纪念")
+		];
+
+		/// <summary>
+		/// 按农历计算的通用节假日
+		/// </summary>
+		private static HashSet<DateInfoStruct> LunarHolidayInfo { get; } =
+		[
+			new DateInfoStruct(1, 1, 6, "春节"),
+			new DateInfoStruct(1, 15, 0, "元宵节"),
+			new DateInfoStruct(5, 5, 1, "端午节"),
+			new DateInfoStruct(7, 7, 0, "七夕情人节"),
+			new DateInfoStruct(7, 15, 0, "中元节 盂兰盆节"),
+			new DateInfoStruct(8, 15, 1, "中秋节"),
+			new DateInfoStruct(9, 9, 0, "重阳节"),
+			new DateInfoStruct(12, 8, 0, "腊八节"),
+			new DateInfoStruct(12, 23, 0, "北方小年(扫房)"),
+			new DateInfoStruct(12, 24, 0, "南方小年(掸尘)")
 
             //new HolidayStruct(12, 30, 0, "除夕")  //注意除夕需要其它方法进行计算
         ];
 
-        private static readonly WeekHolidayStruct[] WHolidayInfo =
-        {
-            new(5, 2, 1, "母亲节"),
-            new(5, 3, 1, "全国助残日"),
-            new(6, 3, 1, "父亲节"),
-            new(9, 3, 3, "国际和平日"),
-            new(9, 4, 1, "国际聋人节"),
-            new(10, 1, 2, "国际住房日"),
-            new(10, 1, 4, "国际减轻自然灾害日"),
-            new(11, 4, 5, "感恩节")
-        };
-
-        #endregion 节日数据
-
-        #endregion 基础数据
-
-        /// <summary>
-        /// 用一个标准的公历日期来初使化
-        /// </summary>
-        /// <param name="dt"></param>
-        public ChineseCalendar(in DateTime dt)
-        {
-            if (dt.Year > MaxYear)
-            {
-                throw new ChineseCalendarException("最大年份支持2100年");
-            }
-
-            int i;
-            Date = dt.Date;
-            _datetime = dt;
-
-            //农历日期计算部分
-            var temp = 0;
-            var ts = Date - MinDay; //计算两天的基本差距
-            var offset = ts.Days;
-            for (i = MinYear; i <= MaxYear; i++)
-            {
-                temp = GetChineseYearDays(i); //求当年农历年天数
-                if (offset - temp < 1)
-                {
-                    break;
-                }
-
-                offset = offset - temp;
-            }
-
-            ChineseYear = i;
-            var leap = GetChineseLeapMonth(ChineseYear);
-
-            //设定当年是否有闰月
-            IsChineseLeapYear = leap > 0;
-            IsChineseLeapMonth = false;
-            for (i = 1; i <= 12; i++)
-            {
-                //闰月
-                if (leap > 0 && i == leap + 1 && IsChineseLeapMonth == false)
-                {
-                    IsChineseLeapMonth = true;
-                    i = i - 1;
-                    temp = GetChineseLeapMonthDays(ChineseYear); //计算闰月天数
-                }
-                else
-                {
-                    IsChineseLeapMonth = false;
-                    temp = GetChineseMonthDays(ChineseYear, i); //计算非闰月天数
-                }
-
-                offset = offset - temp;
-                if (offset <= 0)
-                {
-                    break;
-                }
-            }
-
-            offset = offset + temp;
-            ChineseMonth = i;
-            ChineseDay = offset;
-        }
-
-        /// <summary>
-        /// 用农历的日期来初使化
-        /// </summary>
-        /// <param name="cy">农历年</param>
-        /// <param name="cm">农历月</param>
-        /// <param name="cd">农历日</param>
-        /// <param name="leapMonthFlag">闰月标志</param>
-        public ChineseCalendar(int cy, int cm, int cd, bool leapMonthFlag)
-        {
-            int i, temp;
-            CheckChineseDateLimit(cy, cm, cd, leapMonthFlag);
-            ChineseYear = cy;
-            ChineseMonth = cm;
-            ChineseDay = cd;
-            var offset = 0;
-            for (i = MinYear; i < cy; i++)
-            {
-                temp = GetChineseYearDays(i); //求当年农历年天数
-                offset = offset + temp;
-            }
-
-            var leap = GetChineseLeapMonth(cy);
-            IsChineseLeapYear = leap != 0;
-            IsChineseLeapMonth = cm == leap && leapMonthFlag;
-            if (IsChineseLeapYear == false || cm < leap) //当年没有闰月||计算月份小于闰月
-            {
-                for (i = 1; i < cm; i++)
-                {
-                    temp = GetChineseMonthDays(cy, i); //计算非闰月天数
-                    offset = offset + temp;
-                }
-
-                //检查日期是否大于最大天
-                if (cd > GetChineseMonthDays(cy, cm))
-                {
-                    throw new ChineseCalendarException("不合法的农历日期");
-                }
-
-                offset = offset + cd; //加上当月的天数
-            }
-            else //是闰年,且计算月份大于或等于闰月
-            {
-                for (i = 1; i < cm; i++)
-                {
-                    temp = GetChineseMonthDays(cy, i); //计算非闰月天数
-                    offset = offset + temp;
-                }
-
-                if (cm > leap) //计算月大于闰月
-                {
-                    temp = GetChineseLeapMonthDays(cy); //计算闰月天数
-                    offset = offset + temp; //加上闰月天数
-                    if (cd > GetChineseMonthDays(cy, cm))
-                    {
-                        throw new ChineseCalendarException("不合法的农历日期");
-                    }
-
-                    offset = offset + cd;
-                }
-                else //计算月等于闰月
-                {
-                    //如果需要计算的是闰月,则应首先加上与闰月对应的普通月的天数
-                    if (IsChineseLeapMonth) //计算月为闰月
-                    {
-                        temp = GetChineseMonthDays(cy, cm); //计算非闰月天数
-                        offset = offset + temp;
-                    }
-
-                    if (cd > GetChineseLeapMonthDays(cy))
-                    {
-                        throw new ChineseCalendarException("不合法的农历日期");
-                    }
-
-                    offset = offset + cd;
-                }
-            }
-
-            Date = MinDay.AddDays(offset);
-        }
-
-        #region 私有函数
-
-        /// <summary>
-        /// 传回农历 y年m月的总天数
-        /// </summary>
-        /// <param name="year"></param>
-        /// <param name="month"></param>
-        /// <returns></returns>
-        private int GetChineseMonthDays(int year, int month)
-        {
-            return BitTest32(LunarDateArray[year - MinYear] & 0x0000FFFF, 16 - month) ? 30 : 29;
-        }
-
-        /// <summary>
-        /// 传回农历 y年闰哪个月 1-12 , 没闰传回 0
-        /// </summary>
-        /// <param name="year"></param>
-        /// <returns></returns>
-        private int GetChineseLeapMonth(int year)
-        {
-            return LunarDateArray[year - MinYear] & 0xF;
-        }
-
-        /// <summary>
-        /// 传回农历 y年闰月的天数
-        /// </summary>
-        /// <param name="year"></param>
-        /// <returns></returns>
-        private int GetChineseLeapMonthDays(int year)
-        {
-            return GetChineseLeapMonth(year) != 0 ? (LunarDateArray[year - MinYear] & 0x10000) != 0 ? 30 : 29 : 0;
-        }
-
-        /// <summary>
-        /// 取农历年一年的天数
-        /// </summary>
-        /// <param name="year"></param>
-        /// <returns></returns>
-        private int GetChineseYearDays(int year)
-        {
-            var sumDay = 348;
-            var i = 0x8000;
-            var info = LunarDateArray[year - MinYear] & 0x0FFFF;
-
-            //计算12个月中有多少天为30天
-            for (int m = 0; m < 12; m++)
-            {
-                var f = info & i;
-                if (f != 0)
-                {
-                    sumDay++;
-                }
-
-                i >>= 1;
-            }
-
-            return sumDay + GetChineseLeapMonthDays(year);
-        }
-
-        /// <summary>
-        /// 获得当前时间的时辰
-        /// </summary>
-        /// <param name="dt"></param>
-        /// <returns></returns>
-        ///
-        private string GetChineseHour(DateTime dt)
-        {
-            //计算时辰的地支
-            var hour = dt.Hour;
-            var minute = dt.Minute;
-            if (minute != 0)
-            {
-                hour += 1;
-            }
-
-            var offset = hour / 2;
-            if (offset >= 12)
-            {
-                offset = 0;
-            }
-
-            //计算天干
-            var ts = Date - GanZhiStartDay;
-            var i = ts.Days % 60;
-            var indexGan = ((i % 10 + 1) * 2 - 1) % 10 - 1;
-            var tmpGan = TianGan.Substring(indexGan) + TianGan.Substring(0, indexGan + 2);
-            return tmpGan[offset].ToString() + DiZhi[offset];
-        }
-
-        /// <summary>
-        /// 检查农历日期是否合理
-        /// </summary>
-        /// <param name="year"></param>
-        /// <param name="month"></param>
-        /// <param name="day"></param>
-        /// <param name="leapMonth"></param>
-        private void CheckChineseDateLimit(int year, int month, int day, bool leapMonth)
-        {
-            if (year < MinYear || year > MaxYear)
-            {
-                throw new ChineseCalendarException("非法农历日期");
-            }
-
-            if (month < 1 || month > 12)
-            {
-                throw new ChineseCalendarException("非法农历日期");
-            }
-
-            if (day is < 1 or > 30) //中国的月最多30天
-            {
-                throw new ChineseCalendarException("非法农历日期");
-            }
-
-            int leap = GetChineseLeapMonth(year); // 计算该年应该闰哪个月
-            if (leapMonth && month != leap)
-            {
-                throw new ChineseCalendarException("非法农历日期");
-            }
-        }
-
-        /// <summary>
-        /// 将0-9转成汉字形式
-        /// </summary>
-        /// <param name="n"></param>
-        /// <returns></returns>
-        private string ConvertNumToChineseNum(char n)
-        {
-            if (n < '0' || n > '9')
-            {
-                return default;
-            }
-
-            return n switch
-            {
-                '0' => HzNum[0].ToString(),
-                '1' => HzNum[1].ToString(),
-                '2' => HzNum[2].ToString(),
-                '3' => HzNum[3].ToString(),
-                '4' => HzNum[4].ToString(),
-                '5' => HzNum[5].ToString(),
-                '6' => HzNum[6].ToString(),
-                '7' => HzNum[7].ToString(),
-                '8' => HzNum[8].ToString(),
-                '9' => HzNum[9].ToString(),
-                _ => default
-            };
-        }
-
-        /// <summary>
-        /// 测试某位是否为真
-        /// </summary>
-        /// <param name="num"></param>
-        /// <param name="bitpostion"></param>
-        /// <returns></returns>
-        private bool BitTest32(int num, int bitpostion)
-        {
-            if (bitpostion > 31 || bitpostion < 0)
-            {
-                throw new ArgumentException("参数错误: 取值范围0-31", nameof(bitpostion));
-            }
-
-            int bit = 1 << bitpostion;
-            return (num & bit) != 0;
-        }
-
-        /// <summary>
-        /// 将星期几转成数字表示
-        /// </summary>
-        /// <param name="dayOfWeek"></param>
-        /// <returns></returns>
-        private int ConvertDayOfWeek(DayOfWeek dayOfWeek)
-        {
-            return dayOfWeek switch
-            {
-                DayOfWeek.Sunday => 1,
-                DayOfWeek.Monday => 2,
-                DayOfWeek.Tuesday => 3,
-                DayOfWeek.Wednesday => 4,
-                DayOfWeek.Thursday => 5,
-                DayOfWeek.Friday => 6,
-                DayOfWeek.Saturday => 7,
-                _ => 0
-            };
-        }
-
-        /// <summary>
-        /// 比较当天是不是指定的第周几
-        /// </summary>
-        /// <param name="date"></param>
-        /// <param name="month"></param>
-        /// <param name="week"></param>
-        /// <param name="day"></param>
-        /// <returns></returns>
-        private bool CompareWeekDayHoliday(DateTime date, int month, int week, int day)
-        {
-            bool result = false;
-            if (date.Month == month) //月份相同
-            {
-                if (ConvertDayOfWeek(date.DayOfWeek) == day) //星期几相同
-                {
-                    DateTime firstDay = new DateTime(date.Year, date.Month, 1); //生成当月第一天
-                    int i = ConvertDayOfWeek(firstDay.DayOfWeek);
-                    int firWeekDays = 7 - ConvertDayOfWeek(firstDay.DayOfWeek) + 1; //计算第一周剩余天数
-                    if (i > day)
-                    {
-                        if ((week - 1) * 7 + day + firWeekDays == date.Day)
-                        {
-                            result = true;
-                        }
-                    }
-                    else
-                    {
-                        if (day + firWeekDays + (week - 2) * 7 == date.Day)
-                        {
-                            result = true;
-                        }
-                    }
-                }
-            }
-
-            return result;
-        }
-
-        #endregion 私有函数
-
-        #region 节日
-
-        /// <summary>
-        /// 计算中国农历节日
-        /// </summary>
-        public string ChineseCalendarHoliday
-        {
-            get
-            {
-                string tempStr = "";
-                if (IsChineseLeapMonth)
-                {
-                    return tempStr;
-                }
-
-                foreach (var date in LunarHolidayInfo)
-                {
-                    var end = date.Recess > 0 ? date.Day + date.Recess - 1 : date.Day + date.Recess;
-                    if (date.Month != ChineseMonth || ChineseDay < date.Day || ChineseDay > end)
-                    {
-                        continue;
-                    }
-
-                    tempStr = date.HolidayName;
-                    break;
-                }
-
-                //对除夕进行特别处理
-                if (ChineseMonth != 12)
-                {
-                    return tempStr;
-                }
-
-                int i = GetChineseMonthDays(ChineseYear, 12); //计算当年农历12月的总天数
-                if (ChineseDay == i) //如果为最后一天
-                {
-                    tempStr = "除夕";
-                }
-
-                return tempStr;
-            }
-        }
-
-        /// <summary>
-        /// 按某月第几周第几日计算的节日
-        /// </summary>
-        public string WeekDayHoliday
-        {
-            get
-            {
-                string tempStr = "";
-                foreach (var wh in WHolidayInfo)
-                {
-                    if (!CompareWeekDayHoliday(Date, wh.Month, wh.WeekAtMonth, wh.WeekDay))
-                    {
-                        continue;
-                    }
-
-                    tempStr = wh.HolidayName;
-                    break;
-                }
-
-                return tempStr;
-            }
-        }
-
-        /// <summary>
-        /// 按公历日计算的节日
-        /// </summary>
-        public string DateHoliday
-        {
-            get
-            {
-                string tempStr = "";
-                foreach (DateInfoStruct sh in SolarHolidayInfo)
-                {
-                    var end = sh.Recess > 0 ? sh.Day + sh.Recess - 1 : sh.Day + sh.Recess;
-                    if ((sh.Month == Date.Month) && Date.Day >= sh.Day && Date.Day <= end)
-                    {
-                        tempStr = sh.HolidayName;
-                        break;
-                    }
-
-                    if (CustomHolidays.Keys.Any(d => d.Date == Date))
-                    {
-                        tempStr = CustomHolidays[Date];
-                        break;
-                    }
-                }
-
-                return tempStr;
-            }
-        }
-
-        /// <summary>
-        /// 今天是否是假期
-        /// </summary>
-        public bool IsHoliday => !IsWorkDay;
-
-        /// <summary>
-        /// 今天是否是工作日
-        /// </summary>
-        public bool IsWorkDay
-        {
-            get
-            {
-                bool isHoliday = false;
-                foreach (var date in SolarHolidayInfo)
-                {
-                    var end = date.Recess > 0 ? date.Day + date.Recess - 1 : date.Day + date.Recess;
-                    if ((date.Month == Date.Month) && Date.Day >= date.Day && Date.Day <= end && date.Recess > 0)
-                    {
-                        isHoliday = true;
-                        break;
-                    }
-
-                    if (CustomHolidays.Keys.Any(d => d.Date == Date))
-                    {
-                        isHoliday = true;
-                        break;
-                    }
-                }
-
-                if (!isHoliday)
-                {
-                    foreach (DateInfoStruct lh in LunarHolidayInfo)
-                    {
-                        var end = lh.Recess > 0 ? lh.Day + lh.Recess - 1 : lh.Day + lh.Recess;
-                        if (lh.Month == ChineseMonth && ChineseDay >= lh.Day && ChineseDay <= end && lh.Recess > 0)
-                        {
-                            isHoliday = true;
-                            break;
-                        }
-                    }
-
-                    //对除夕进行特别处理
-                    if (ChineseMonth == 12)
-                    {
-                        int i = GetChineseMonthDays(ChineseYear, 12); //计算当年农历12月的总天数
-                        if (ChineseDay == i) //如果为最后一天
-                        {
-                            isHoliday = true;
-                        }
-                    }
-                }
-
-                return !isHoliday && !IsWeekend() || CustomWorkDays.Any(s => s.Date == Date);
-            }
-        }
-
-        /// <summary>
-        /// 是否是周末
-        /// </summary>
-        /// <returns></returns>
-        private bool IsWeekend()
-        {
-            return Date.DayOfWeek == DayOfWeek.Saturday || Date.DayOfWeek == DayOfWeek.Sunday;
-        }
-
-        #endregion 节日
-
-        #region 公历日期
-
-        /// <summary>
-        /// 取对应的公历日期
-        /// </summary>
-        public DateTime Date { get; set; }
-
-        /// <summary>
-        /// 取星期几
-        /// </summary>
-        public DayOfWeek WeekDay => Date.DayOfWeek;
-
-        /// <summary>
-        /// 周几的字符
-        /// </summary>
-        public string WeekDayStr => Date.DayOfWeek switch
-        {
-            DayOfWeek.Sunday => "星期日",
-            DayOfWeek.Monday => "星期一",
-            DayOfWeek.Tuesday => "星期二",
-            DayOfWeek.Wednesday => "星期三",
-            DayOfWeek.Thursday => "星期四",
-            DayOfWeek.Friday => "星期五",
-            _ => "星期六"
-        };
-
-        /// <summary>
-        /// 公历日期中文表示法 如一九九七年七月一日
-        /// </summary>
-        public string DateString => "公元" + Date.ToLongDateString();
-
-        /// <summary>
-        /// 当前是否公历闰年
-        /// </summary>
-        public bool IsLeapYear => DateTime.IsLeapYear(Date.Year);
-
-        /// <summary>
-        /// 28星宿计算
-        /// </summary>
-        public string ChineseConstellation
-        {
-            get
-            {
-                var ts = Date - ChineseConstellationReferDay;
-                var offset = ts.Days;
-                var modStarDay = offset % 28;
-                return (modStarDay >= 0 ? ChineseConstellationName[modStarDay] : ChineseConstellationName[27 + modStarDay]);
-            }
-        }
-
-        /// <summary>
-        /// 时辰
-        /// </summary>
-        public string ChineseHour => GetChineseHour(_datetime);
-
-        #endregion 公历日期
-
-        #region 农历日期
-
-        /// <summary>
-        /// 农历今天
-        /// </summary>
-        public static ChineseCalendar Today => new(DateTime.Today);
-
-        /// <summary>
-        /// 是否闰月
-        /// </summary>
-        public bool IsChineseLeapMonth { get; }
-
-        /// <summary>
-        /// 当年是否有闰月
-        /// </summary>
-        public bool IsChineseLeapYear { get; }
-
-        /// <summary>
-        /// 农历日
-        /// </summary>
-        public int ChineseDay { get; }
-
-        /// <summary>
-        /// 农历日中文表示
-        /// </summary>
-        public string ChineseDayString => ChineseDay switch
-        {
-            0 => "",
-            10 => "初十",
-            20 => "二十",
-            30 => "三十",
-            _ => (NStr2[ChineseDay / 10] + NStr1[ChineseDay % 10].ToString())
-        };
-
-        /// <summary>
-        /// 农历的月份
-        /// </summary>
-        public int ChineseMonth { get; }
-
-        /// <summary>
-        /// 农历月份字符串
-        /// </summary>
-        public string ChineseMonthString => MonthString[ChineseMonth];
-
-        /// <summary>
-        /// 取农历年份
-        /// </summary>
-        public int ChineseYear { get; }
-
-        /// <summary>
-        /// 取农历年字符串如,一九九七年
-        /// </summary>
-        public string ChineseYearString
-        {
-            get
-            {
-                string tempStr = "";
-                string num = ChineseYear.ToString();
-                for (int i = 0; i < 4; i++)
-                {
-                    tempStr += ConvertNumToChineseNum(num[i]);
-                }
-
-                return tempStr + "年";
-            }
-        }
-
-        /// <summary>
-        /// 取农历日期表示法:农历一九九七年正月初五
-        /// </summary>
-        public string ChineseDateString
-        {
-            get
-            {
-                if (IsChineseLeapMonth)
-                {
-                    return ChineseYearString + "闰" + ChineseMonthString + ChineseDayString;
-                }
-
-                return ChineseYearString + ChineseMonthString + ChineseDayString;
-            }
-        }
-
-        /// <summary>
-        /// 定气法计算二十四节气,二十四节气是按地球公转来计算的,并非是阴历计算的
-        /// </summary>
-        /// <remarks>
-        /// 节气的定法有两种。古代历法采用的称为"恒气",即按时间把一年等分为24份,
-        /// 每一节气平均得15天有余,所以又称"平气"。现代农历采用的称为"定气",即
-        /// 按地球在轨道上的位置为标准,一周360°,两节气之间相隔15°。由于冬至时地
-        /// 球位于近日点附近,运动速度较快,因而太阳在黄道上移动15°的时间不到15天。
-        /// 夏至前后的情况正好相反,太阳在黄道上移动较慢,一个节气达16天之多。采用
-        /// 定气时可以保证春、秋两分必然在昼夜平分的那两天。
-        /// </remarks>
-        public string ChineseTwentyFourDay
-        {
-            get
-            {
-                var baseDateAndTime = new DateTime(1900, 1, 6, 2, 5, 0, DateTimeKind.Local); //#1/6/1900 2:05:00 AM#
-                string tempStr = "";
-                var y = Date.Year;
-                for (int i = 1; i <= 24; i++)
-                {
-                    var num = 525948.76 * (y - 1900) + STermInfo[i - 1];
-                    var newDate = baseDateAndTime.AddMinutes(num);
-                    if (newDate.DayOfYear != Date.DayOfYear)
-                    {
-                        continue;
-                    }
-
-                    tempStr = SolarTerm[i - 1];
-                    break;
-                }
-
-                return tempStr;
-            }
-        }
-
-        /// <summary>
-        /// 当前日期前一个最近节气
-        /// </summary>
-        public string ChineseTwentyFourPrevDay
-        {
-            get
-            {
-                DateTime baseDateAndTime = new DateTime(1900, 1, 6, 2, 5, 0, DateTimeKind.Local); //#1/6/1900 2:05:00 AM#
-                string tempStr = "";
-                var y = Date.Year;
-                for (int i = 24; i >= 1; i--)
-                {
-                    var num = 525948.76 * (y - 1900) + STermInfo[i - 1];
-                    var newDate = baseDateAndTime.AddMinutes(num);
-                    if (newDate.DayOfYear < Date.DayOfYear)
-                    {
-                        tempStr = $"{SolarTerm[i - 1]}[{newDate:yyyy-MM-dd}]";
-                        break;
-                    }
-                }
-
-                return tempStr;
-            }
-        }
-
-        /// <summary>
-        /// 当前日期后一个最近节气
-        /// </summary>
-        public string ChineseTwentyFourNextDay
-        {
-            get
-            {
-                var baseDateAndTime = new DateTime(1900, 1, 6, 2, 5, 0, DateTimeKind.Local); //#1/6/1900 2:05:00 AM#
-                string tempStr = "";
-                var y = Date.Year;
-                for (int i = 1; i <= 24; i++)
-                {
-                    var num = 525948.76 * (y - 1900) + STermInfo[i - 1];
-                    var newDate = baseDateAndTime.AddMinutes(num);
-                    if (newDate.DayOfYear > Date.DayOfYear)
-                    {
-                        tempStr = $"{SolarTerm[i - 1]}[{newDate:yyyy-MM-dd}]";
-                        break;
-                    }
-                }
-
-                return tempStr;
-            }
-        }
-
-        #endregion 农历日期
-
-        #region 星座
-
-        /// <summary>
-        /// 计算指定日期的星座序号
-        /// </summary>
-        /// <returns></returns>
-        public string Constellation
-        {
-            get
-            {
-                int index;
-                var m = Date.Month;
-                var d = Date.Day;
-                var y = m * 100 + d;
-                if (y is >= 321 and <= 419)
-                {
-                    index = 0;
-                }
-                else if (y is >= 420 and <= 520)
-                {
-                    index = 1;
-                }
-                else if (y is >= 521 and <= 620)
-                {
-                    index = 2;
-                }
-                else if (y is >= 621 and <= 722)
-                {
-                    index = 3;
-                }
-                else if (y is >= 723 and <= 822)
-                {
-                    index = 4;
-                }
-                else if (y is >= 823 and <= 922)
-                {
-                    index = 5;
-                }
-                else if (y is >= 923 and <= 1022)
-                {
-                    index = 6;
-                }
-                else if (y >= 1023 && y <= 1121)
-                {
-                    index = 7;
-                }
-                else if (y is >= 1122 and <= 1221)
-                {
-                    index = 8;
-                }
-                else if (y is >= 1222 or <= 119)
-                {
-                    index = 9;
-                }
-                else if (y is >= 120 and <= 218)
-                {
-                    index = 10;
-                }
-                else if (y is >= 219 and <= 320)
-                {
-                    index = 11;
-                }
-                else
-                {
-                    index = 0;
-                }
-
-                return ConstellationName[index];
-            }
-        }
-
-        #endregion 星座
-
-        #region 生肖
-
-        /// <summary>
-        /// 计算属相的索引,注意虽然属相是以农历年来区别的,但是目前在实际使用中是按公历来计算的
-        /// 鼠年为1,其它类推
-        /// </summary>
-        public int Animal
-        {
-            get
-            {
-                int offset = Date.Year - AnimalStartYear;
-                return offset % 12 + 1;
-            }
-        }
-
-        /// <summary>
-        /// 取属相字符串
-        /// </summary>
-        public string AnimalString
-        {
-            get
-            {
-                int offset = Date.Year - AnimalStartYear; //阳历计算
-                return AnimalStr[offset % 12].ToString();
-            }
-        }
-
-        #endregion 生肖
-
-        #region 天干地支
-
-        /// <summary>
-        /// 取农历年的干支表示法如 乙丑年
-        /// </summary>
-        public string GanZhiYearString
-        {
-            get
-            {
-                int i = (ChineseYear - GanZhiStartYear) % 60; //计算干支
-                var tempStr = TianGan[i % 10] + DiZhi[i % 12].ToString() + "年";
-                return tempStr;
-            }
-        }
-
-        /// <summary>
-        /// 取干支的月表示字符串,注意农历的闰月不记干支
-        /// </summary>
-        public string GanZhiMonthString
-        {
-            get
-            {
-                //每个月的地支总是固定的,而且总是从寅月开始
-                int zhiIndex;
-                if (ChineseMonth > 10)
-                {
-                    zhiIndex = ChineseMonth - 10;
-                }
-                else
-                {
-                    zhiIndex = ChineseMonth + 2;
-                }
-
-                var zhi = DiZhi[zhiIndex - 1].ToString();
-
-                //根据当年的干支年的干来计算月干的第一个
-                int ganIndex = 1;
-                int i = (ChineseYear - GanZhiStartYear) % 60; //计算干支
-                ganIndex = (i % 10) switch
-                {
-                    0 => 3, //甲
-                    1 => 5, //乙
-                    2 => 7, //丙
-                    3 => 9, //丁
-                    4 => 1, //戊
-                    5 => 3, //己
-                    6 => 5, //庚
-                    7 => 7, //辛
-                    8 => 9, //壬
-                    9 => 1, //癸
-                    _ => ganIndex
-                };
-
-                var gan = TianGan[(ganIndex + ChineseMonth - 2) % 10].ToString();
-                return gan + zhi + "月";
-            }
-        }
-
-        /// <summary>
-        /// 取干支日表示法
-        /// </summary>
-        public string GanZhiDayString
-        {
-            get
-            {
-                var ts = Date - GanZhiStartDay;
-                var offset = ts.Days;
-                var i = offset % 60;
-                return TianGan[i % 10].ToString() + DiZhi[i % 12] + "日";
-            }
-        }
-
-        /// <summary>
-        /// 取当前日期的干支表示法如 甲子年乙丑月丙庚日
-        /// </summary>
-        public string GanZhiDateString => GanZhiYearString + GanZhiMonthString + GanZhiDayString;
-
-        #endregion 天干地支
-
-        /// <summary>
-        /// 取下一天
-        /// </summary>
-        /// <returns></returns>
-        public ChineseCalendar NextDay => new(Date.AddDays(1));
-
-        /// <summary>
-        /// 取前一天
-        /// </summary>
-        /// <returns></returns>
-        public ChineseCalendar PervDay => new(Date.AddDays(-1));
-
-        /// <summary>
-        /// 取下n天
-        /// </summary>
-        /// <returns></returns>
-        public ChineseCalendar AddDays(int days)
-        {
-            return new ChineseCalendar(Date.AddDays(days));
-        }
-
-        /// <summary>
-        /// 取下n天
-        /// </summary>
-        /// <returns></returns>
-        public ChineseCalendar AddWorkDays(int days)
-        {
-            var cc = new ChineseCalendar(Date);
-            while (true)
-            {
-                cc = cc.AddDays(1);
-                if (cc.IsWorkDay)
-                {
-                    days--;
-                }
-
-                if (days == 0)
-                {
-                    return cc;
-                }
-            }
-        }
-
-        /// <summary>
-        /// 加n月
-        /// </summary>
-        /// <returns></returns>
-        public ChineseCalendar AddMonths(int months)
-        {
-            return new ChineseCalendar(Date.AddMonths(months));
-        }
-    }
-}
+		private static readonly WeekHolidayStruct[] WHolidayInfo =
+		{
+			new(5, 2, 1, "母亲节"),
+			new(5, 3, 1, "全国助残日"),
+			new(6, 3, 1, "父亲节"),
+			new(9, 3, 3, "国际和平日"),
+			new(9, 4, 1, "国际聋人节"),
+			new(10, 1, 2, "国际住房日"),
+			new(10, 1, 4, "国际减轻自然灾害日"),
+			new(11, 4, 5, "感恩节")
+		};
+
+		#endregion 节日数据
+
+		#endregion 基础数据
+
+		/// <summary>
+		/// 用一个标准的公历日期来初使化
+		/// </summary>
+		/// <param name="dt"></param>
+		public ChineseCalendar(in DateTime dt)
+		{
+			if (dt.Year > MaxYear)
+			{
+				throw new ChineseCalendarException("最大年份支持2100年");
+			}
+
+			int i;
+			Date = dt.Date;
+			_datetime = dt;
+
+			//农历日期计算部分
+			var temp = 0;
+			var ts = Date - MinDay; //计算两天的基本差距
+			var offset = ts.Days;
+			for (i = MinYear; i <= MaxYear; i++)
+			{
+				temp = GetChineseYearDays(i); //求当年农历年天数
+				if (offset - temp < 1)
+				{
+					break;
+				}
+
+				offset = offset - temp;
+			}
+
+			ChineseYear = i;
+			var leap = GetChineseLeapMonth(ChineseYear);
+
+			//设定当年是否有闰月
+			IsChineseLeapYear = leap > 0;
+			IsChineseLeapMonth = false;
+			for (i = 1; i <= 12; i++)
+			{
+				//闰月
+				if (leap > 0 && i == leap + 1 && IsChineseLeapMonth == false)
+				{
+					IsChineseLeapMonth = true;
+					i = i - 1;
+					temp = GetChineseLeapMonthDays(ChineseYear); //计算闰月天数
+				}
+				else
+				{
+					IsChineseLeapMonth = false;
+					temp = GetChineseMonthDays(ChineseYear, i); //计算非闰月天数
+				}
+
+				offset = offset - temp;
+				if (offset <= 0)
+				{
+					break;
+				}
+			}
+
+			offset = offset + temp;
+			ChineseMonth = i;
+			ChineseDay = offset;
+		}
+
+		/// <summary>
+		/// 用农历的日期来初使化
+		/// </summary>
+		/// <param name="cy">农历年</param>
+		/// <param name="cm">农历月</param>
+		/// <param name="cd">农历日</param>
+		/// <param name="leapMonthFlag">闰月标志</param>
+		public ChineseCalendar(int cy, int cm, int cd)
+		{
+			int i, temp;
+			CheckChineseDateLimit(cy, cm, cd);
+			ChineseYear = cy;
+			ChineseMonth = cm;
+			ChineseDay = cd;
+			var offset = 0;
+			for (i = MinYear; i < cy; i++)
+			{
+				temp = GetChineseYearDays(i); //求当年农历年天数
+				offset = offset + temp;
+			}
+
+			var leap = GetChineseLeapMonth(cy);
+			IsChineseLeapYear = leap != 0;
+			IsChineseLeapMonth = cm == leap;
+			if (IsChineseLeapYear == false || cm < leap) //当年没有闰月||计算月份小于闰月
+			{
+				for (i = 1; i < cm; i++)
+				{
+					temp = GetChineseMonthDays(cy, i); //计算非闰月天数
+					offset = offset + temp;
+				}
+
+				//检查日期是否大于最大天
+				if (cd > GetChineseMonthDays(cy, cm))
+				{
+					throw new ChineseCalendarException("不合法的农历日期");
+				}
+
+				offset = offset + cd; //加上当月的天数
+			}
+			else //是闰年,且计算月份大于或等于闰月
+			{
+				for (i = 1; i < cm; i++)
+				{
+					temp = GetChineseMonthDays(cy, i); //计算非闰月天数
+					offset = offset + temp;
+				}
+
+				if (cm > leap) //计算月大于闰月
+				{
+					temp = GetChineseLeapMonthDays(cy); //计算闰月天数
+					offset = offset + temp; //加上闰月天数
+					if (cd > GetChineseMonthDays(cy, cm))
+					{
+						throw new ChineseCalendarException("不合法的农历日期");
+					}
+
+					offset = offset + cd;
+				}
+				else //计算月等于闰月
+				{
+					//如果需要计算的是闰月,则应首先加上与闰月对应的普通月的天数
+					if (IsChineseLeapMonth) //计算月为闰月
+					{
+						temp = GetChineseMonthDays(cy, cm); //计算非闰月天数
+						offset = offset + temp;
+					}
+
+					if (cd > GetChineseLeapMonthDays(cy))
+					{
+						throw new ChineseCalendarException("不合法的农历日期");
+					}
+
+					offset = offset + cd;
+				}
+			}
+
+			Date = MinDay.AddDays(offset);
+		}
+
+		#region 私有函数
+
+		/// <summary>
+		/// 传回农历 y年m月的总天数
+		/// </summary>
+		/// <param name="year"></param>
+		/// <param name="month"></param>
+		/// <returns></returns>
+		internal int GetChineseMonthDays(int year, int month)
+		{
+			return BitTest32(LunarDateArray[year - MinYear] & 0x0000FFFF, 16 - month) ? 30 : 29;
+		}
+
+		internal int GetChineseMonthDays()
+		{
+			return BitTest32(LunarDateArray[ChineseYear - MinYear] & 0x0000FFFF, 16 - ChineseMonth) ? 30 : 29;
+		}
+
+		/// <summary>
+		/// 传回农历 y年闰哪个月 1-12 , 没闰传回 0
+		/// </summary>
+		/// <param name="year"></param>
+		/// <returns></returns>
+		private int GetChineseLeapMonth(int year)
+		{
+			return LunarDateArray[year - MinYear] & 0xF;
+		}
+
+		/// <summary>
+		/// 传回农历 y年闰月的天数
+		/// </summary>
+		/// <param name="year"></param>
+		/// <returns></returns>
+		private int GetChineseLeapMonthDays(int year)
+		{
+			return GetChineseLeapMonth(year) != 0 ? (LunarDateArray[year - MinYear] & 0x10000) != 0 ? 30 : 29 : 0;
+		}
+
+		/// <summary>
+		/// 取农历年一年的天数
+		/// </summary>
+		/// <param name="year"></param>
+		/// <returns></returns>
+		private int GetChineseYearDays(int year)
+		{
+			var sumDay = 348;
+			var i = 0x8000;
+			var info = LunarDateArray[year - MinYear] & 0x0FFFF;
+
+			//计算12个月中有多少天为30天
+			for (int m = 0; m < 12; m++)
+			{
+				var f = info & i;
+				if (f != 0)
+				{
+					sumDay++;
+				}
+
+				i >>= 1;
+			}
+
+			return sumDay + GetChineseLeapMonthDays(year);
+		}
+
+		/// <summary>
+		/// 获得当前时间的时辰
+		/// </summary>
+		/// <param name="dt"></param>
+		/// <returns></returns>
+		///
+		private string GetChineseHour(DateTime dt)
+		{
+			//计算时辰的地支
+			var hour = dt.Hour;
+			var minute = dt.Minute;
+			if (minute != 0)
+			{
+				hour += 1;
+			}
+
+			var offset = hour / 2;
+			if (offset >= 12)
+			{
+				offset = 0;
+			}
+
+			//计算天干
+			var ts = Date - GanZhiStartDay;
+			var i = ts.Days % 60;
+			var indexGan = ((i % 10 + 1) * 2 - 1) % 10 - 1;
+			var tmpGan = TianGan.Substring(indexGan) + TianGan.Substring(0, indexGan + 2);
+			return tmpGan[offset].ToString() + DiZhi[offset];
+		}
+
+		/// <summary>
+		/// 检查农历日期是否合理
+		/// </summary>
+		/// <param name="year"></param>
+		/// <param name="month"></param>
+		/// <param name="day"></param>
+		private void CheckChineseDateLimit(int year, int month, int day)
+		{
+			if (year < MinYear || year > MaxYear)
+			{
+				throw new ChineseCalendarException("非法农历日期");
+			}
+
+			if (month < 1 || month > 12)
+			{
+				throw new ChineseCalendarException("非法农历日期");
+			}
+
+			if (day is < 1 or > 30) //中国的月最多30天
+			{
+				throw new ChineseCalendarException("非法农历日期");
+			}
+		}
+
+		/// <summary>
+		/// 将0-9转成汉字形式
+		/// </summary>
+		/// <param name="n"></param>
+		/// <returns></returns>
+		private string ConvertNumToChineseNum(char n)
+		{
+			if (n < '0' || n > '9')
+			{
+				return default;
+			}
+
+			return n switch
+			{
+				'0' => HzNum[0].ToString(),
+				'1' => HzNum[1].ToString(),
+				'2' => HzNum[2].ToString(),
+				'3' => HzNum[3].ToString(),
+				'4' => HzNum[4].ToString(),
+				'5' => HzNum[5].ToString(),
+				'6' => HzNum[6].ToString(),
+				'7' => HzNum[7].ToString(),
+				'8' => HzNum[8].ToString(),
+				'9' => HzNum[9].ToString(),
+				_ => default
+			};
+		}
+
+		/// <summary>
+		/// 测试某位是否为真
+		/// </summary>
+		/// <param name="num"></param>
+		/// <param name="bitpostion"></param>
+		/// <returns></returns>
+		private bool BitTest32(int num, int bitpostion)
+		{
+			if (bitpostion > 31 || bitpostion < 0)
+			{
+				throw new ArgumentException("参数错误: 取值范围0-31", nameof(bitpostion));
+			}
+
+			int bit = 1 << bitpostion;
+			return (num & bit) != 0;
+		}
+
+		/// <summary>
+		/// 将星期几转成数字表示
+		/// </summary>
+		/// <param name="dayOfWeek"></param>
+		/// <returns></returns>
+		private int ConvertDayOfWeek(DayOfWeek dayOfWeek)
+		{
+			return dayOfWeek switch
+			{
+				DayOfWeek.Sunday => 1,
+				DayOfWeek.Monday => 2,
+				DayOfWeek.Tuesday => 3,
+				DayOfWeek.Wednesday => 4,
+				DayOfWeek.Thursday => 5,
+				DayOfWeek.Friday => 6,
+				DayOfWeek.Saturday => 7,
+				_ => 0
+			};
+		}
+
+		/// <summary>
+		/// 比较当天是不是指定的第周几
+		/// </summary>
+		/// <param name="date"></param>
+		/// <param name="month"></param>
+		/// <param name="week"></param>
+		/// <param name="day"></param>
+		/// <returns></returns>
+		private bool CompareWeekDayHoliday(DateTime date, int month, int week, int day)
+		{
+			bool result = false;
+			if (date.Month == month) //月份相同
+			{
+				if (ConvertDayOfWeek(date.DayOfWeek) == day) //星期几相同
+				{
+					DateTime firstDay = new DateTime(date.Year, date.Month, 1); //生成当月第一天
+					int i = ConvertDayOfWeek(firstDay.DayOfWeek);
+					int firWeekDays = 7 - ConvertDayOfWeek(firstDay.DayOfWeek) + 1; //计算第一周剩余天数
+					if (i > day)
+					{
+						if ((week - 1) * 7 + day + firWeekDays == date.Day)
+						{
+							result = true;
+						}
+					}
+					else
+					{
+						if (day + firWeekDays + (week - 2) * 7 == date.Day)
+						{
+							result = true;
+						}
+					}
+				}
+			}
+
+			return result;
+		}
+
+		#endregion 私有函数
+
+		#region 节日
+
+		/// <summary>
+		/// 计算中国农历节日
+		/// </summary>
+		public string ChineseCalendarHoliday
+		{
+			get
+			{
+				string tempStr = "";
+				if (IsChineseLeapMonth)
+				{
+					return tempStr;
+				}
+
+				foreach (var date in LunarHolidayInfo)
+				{
+					var end = date.Recess > 0 ? date.Day + date.Recess - 1 : date.Day + date.Recess;
+					if (date.Month != ChineseMonth || ChineseDay < date.Day || ChineseDay > end)
+					{
+						continue;
+					}
+
+					tempStr = date.HolidayName;
+					break;
+				}
+
+				//对除夕进行特别处理
+				if (ChineseMonth != 12)
+				{
+					return tempStr;
+				}
+
+				int i = GetChineseMonthDays(ChineseYear, 12); //计算当年农历12月的总天数
+				if (ChineseDay == i) //如果为最后一天
+				{
+					tempStr = "除夕";
+				}
+
+				return tempStr;
+			}
+		}
+
+		/// <summary>
+		/// 按某月第几周第几日计算的节日
+		/// </summary>
+		public string WeekDayHoliday
+		{
+			get
+			{
+				string tempStr = "";
+				foreach (var wh in WHolidayInfo)
+				{
+					if (!CompareWeekDayHoliday(Date, wh.Month, wh.WeekAtMonth, wh.WeekDay))
+					{
+						continue;
+					}
+
+					tempStr = wh.HolidayName;
+					break;
+				}
+
+				return tempStr;
+			}
+		}
+
+		/// <summary>
+		/// 按公历日计算的节日
+		/// </summary>
+		public string DateHoliday
+		{
+			get
+			{
+				string tempStr = "";
+				foreach (DateInfoStruct sh in SolarHolidayInfo)
+				{
+					var end = sh.Recess > 0 ? sh.Day + sh.Recess - 1 : sh.Day + sh.Recess;
+					if ((sh.Month == Date.Month) && Date.Day >= sh.Day && Date.Day <= end)
+					{
+						tempStr = sh.HolidayName;
+						break;
+					}
+
+					if (CustomHolidays.Keys.Any(d => d.Date == Date))
+					{
+						tempStr = CustomHolidays[Date];
+						break;
+					}
+				}
+
+				return tempStr;
+			}
+		}
+
+		/// <summary>
+		/// 今天是否是假期
+		/// </summary>
+		public bool IsHoliday => !IsWorkDay;
+
+		/// <summary>
+		/// 今天是否是工作日
+		/// </summary>
+		public bool IsWorkDay
+		{
+			get
+			{
+				bool isHoliday = false;
+				foreach (var date in SolarHolidayInfo)
+				{
+					var end = date.Recess > 0 ? date.Day + date.Recess - 1 : date.Day + date.Recess;
+					if ((date.Month == Date.Month) && Date.Day >= date.Day && Date.Day <= end && date.Recess > 0)
+					{
+						isHoliday = true;
+						break;
+					}
+
+					if (CustomHolidays.Keys.Any(d => d.Date == Date))
+					{
+						isHoliday = true;
+						break;
+					}
+				}
+
+				if (!isHoliday)
+				{
+					foreach (DateInfoStruct lh in LunarHolidayInfo)
+					{
+						var end = lh.Recess > 0 ? lh.Day + lh.Recess - 1 : lh.Day + lh.Recess;
+						if (lh.Month == ChineseMonth && ChineseDay >= lh.Day && ChineseDay <= end && lh.Recess > 0)
+						{
+							isHoliday = true;
+							break;
+						}
+					}
+
+					//对除夕进行特别处理
+					if (ChineseMonth == 12)
+					{
+						int i = GetChineseMonthDays(ChineseYear, 12); //计算当年农历12月的总天数
+						if (ChineseDay == i) //如果为最后一天
+						{
+							isHoliday = true;
+						}
+					}
+				}
+
+				return !isHoliday && !IsWeekend() || CustomWorkDays.Any(s => s.Date == Date);
+			}
+		}
+
+		/// <summary>
+		/// 是否是周末
+		/// </summary>
+		/// <returns></returns>
+		private bool IsWeekend()
+		{
+			return Date.DayOfWeek == DayOfWeek.Saturday || Date.DayOfWeek == DayOfWeek.Sunday;
+		}
+
+		#endregion 节日
+
+		#region 公历日期
+
+		/// <summary>
+		/// 取对应的公历日期
+		/// </summary>
+		public DateTime Date { get; set; }
+
+		/// <summary>
+		/// 取星期几
+		/// </summary>
+		public DayOfWeek WeekDay => Date.DayOfWeek;
+
+		/// <summary>
+		/// 周几的字符
+		/// </summary>
+		public string WeekDayStr => Date.DayOfWeek switch
+		{
+			DayOfWeek.Sunday => "星期日",
+			DayOfWeek.Monday => "星期一",
+			DayOfWeek.Tuesday => "星期二",
+			DayOfWeek.Wednesday => "星期三",
+			DayOfWeek.Thursday => "星期四",
+			DayOfWeek.Friday => "星期五",
+			_ => "星期六"
+		};
+
+		/// <summary>
+		/// 公历日期中文表示法 如一九九七年七月一日
+		/// </summary>
+		public string DateString => "公元" + Date.ToLongDateString();
+
+		/// <summary>
+		/// 当前是否公历闰年
+		/// </summary>
+		public bool IsLeapYear => DateTime.IsLeapYear(Date.Year);
+
+		/// <summary>
+		/// 28星宿计算
+		/// </summary>
+		public string ChineseConstellation
+		{
+			get
+			{
+				var ts = Date - ChineseConstellationReferDay;
+				var offset = ts.Days;
+				var modStarDay = offset % 28;
+				return (modStarDay >= 0 ? ChineseConstellationName[modStarDay] : ChineseConstellationName[27 + modStarDay]);
+			}
+		}
+
+		/// <summary>
+		/// 时辰
+		/// </summary>
+		public string ChineseHour => GetChineseHour(_datetime);
+
+		#endregion 公历日期
+
+		#region 农历日期
+
+		/// <summary>
+		/// 农历今天
+		/// </summary>
+		public static ChineseCalendar Today => new(DateTime.Today);
+
+		/// <summary>
+		/// 是否闰月
+		/// </summary>
+		public bool IsChineseLeapMonth { get; }
+
+		/// <summary>
+		/// 当年是否有闰月
+		/// </summary>
+		public bool IsChineseLeapYear { get; }
+
+		/// <summary>
+		/// 农历日
+		/// </summary>
+		public int ChineseDay { get; }
+
+		/// <summary>
+		/// 农历日中文表示
+		/// </summary>
+		public string ChineseDayString => ChineseDay switch
+		{
+			0 => "",
+			10 => "初十",
+			20 => "二十",
+			30 => "三十",
+			_ => (NStr2[ChineseDay / 10] + NStr1[ChineseDay % 10].ToString())
+		};
+
+		/// <summary>
+		/// 农历的月份
+		/// </summary>
+		public int ChineseMonth { get; }
+
+		/// <summary>
+		/// 农历月份字符串
+		/// </summary>
+		public string ChineseMonthString => MonthString[ChineseMonth];
+
+		/// <summary>
+		/// 取农历年份
+		/// </summary>
+		public int ChineseYear { get; }
+
+		/// <summary>
+		/// 取农历年字符串如,一九九七年
+		/// </summary>
+		public string ChineseYearString
+		{
+			get
+			{
+				string tempStr = "";
+				string num = ChineseYear.ToString();
+				for (int i = 0; i < 4; i++)
+				{
+					tempStr += ConvertNumToChineseNum(num[i]);
+				}
+
+				return tempStr + "年";
+			}
+		}
+
+		/// <summary>
+		/// 取农历日期表示法:农历一九九七年正月初五
+		/// </summary>
+		public string ChineseDateString
+		{
+			get
+			{
+				if (IsChineseLeapMonth)
+				{
+					return ChineseYearString + "闰" + ChineseMonthString + ChineseDayString;
+				}
+
+				return ChineseYearString + ChineseMonthString + ChineseDayString;
+			}
+		}
+
+		/// <summary>
+		/// 定气法计算二十四节气,二十四节气是按地球公转来计算的,并非是阴历计算的
+		/// </summary>
+		/// <remarks>
+		/// 节气的定法有两种。古代历法采用的称为"恒气",即按时间把一年等分为24份,
+		/// 每一节气平均得15天有余,所以又称"平气"。现代农历采用的称为"定气",即
+		/// 按地球在轨道上的位置为标准,一周360°,两节气之间相隔15°。由于冬至时地
+		/// 球位于近日点附近,运动速度较快,因而太阳在黄道上移动15°的时间不到15天。
+		/// 夏至前后的情况正好相反,太阳在黄道上移动较慢,一个节气达16天之多。采用
+		/// 定气时可以保证春、秋两分必然在昼夜平分的那两天。
+		/// </remarks>
+		public string ChineseTwentyFourDay
+		{
+			get
+			{
+				var baseDateAndTime = new DateTime(1900, 1, 6, 2, 5, 0, DateTimeKind.Local); //#1/6/1900 2:05:00 AM#
+				var y = Date.Year;
+				for (int i = 1; i <= 24; i++)
+				{
+					var num = 525948.76 * (y - 1900) + STermInfo[i - 1];
+					var newDate = baseDateAndTime.AddMinutes(num);
+					if (newDate.DayOfYear != Date.DayOfYear)
+					{
+						continue;
+					}
+
+					return SolarTerm[i - 1];
+				}
+
+				return "";
+			}
+		}
+
+		/// <summary>
+		/// 当前日期前一个最近节气
+		/// </summary>
+		public ChineseCalendar ChineseTwentyFourPrevDay
+		{
+			get
+			{
+				var baseTime = new DateTime(1900, 1, 6, 2, 5, 0, DateTimeKind.Local); //#1/6/1900 2:05:00 AM#
+				var y = Date.Year;
+				for (int i = 24; i >= 1; i--)
+				{
+					var num = 525948.76 * (y - 1900) + STermInfo[i - 1];
+					var newDate = baseTime.AddMinutes(num);
+					if (newDate.DayOfYear < Date.DayOfYear)
+					{
+						return new ChineseCalendar(newDate);
+					}
+				}
+
+				return this;
+			}
+		}
+
+		/// <summary>
+		/// 当前日期后一个最近节气
+		/// </summary>
+		public ChineseCalendar ChineseTwentyFourNextDay
+		{
+			get
+			{
+				var baseDateAndTime = new DateTime(1900, 1, 6, 2, 5, 0, DateTimeKind.Local); //#1/6/1900 2:05:00 AM#
+				var y = Date.Year;
+				for (int i = 1; i <= 24; i++)
+				{
+					var num = 525948.76 * (y - 1900) + STermInfo[i - 1];
+					var newDate = baseDateAndTime.AddMinutes(num);
+					if (newDate.DayOfYear > Date.DayOfYear)
+					{
+						return new ChineseCalendar(newDate);
+					}
+				}
+
+				return this;
+			}
+		}
+
+		#endregion 农历日期
+
+		#region 星座
+
+		/// <summary>
+		/// 计算指定日期的星座序号
+		/// </summary>
+		/// <returns></returns>
+		public string Constellation
+		{
+			get
+			{
+				int index;
+				var m = Date.Month;
+				var d = Date.Day;
+				var y = m * 100 + d;
+				if (y is >= 321 and <= 419)
+				{
+					index = 0;
+				}
+				else if (y is >= 420 and <= 520)
+				{
+					index = 1;
+				}
+				else if (y is >= 521 and <= 620)
+				{
+					index = 2;
+				}
+				else if (y is >= 621 and <= 722)
+				{
+					index = 3;
+				}
+				else if (y is >= 723 and <= 822)
+				{
+					index = 4;
+				}
+				else if (y is >= 823 and <= 922)
+				{
+					index = 5;
+				}
+				else if (y is >= 923 and <= 1022)
+				{
+					index = 6;
+				}
+				else if (y >= 1023 && y <= 1121)
+				{
+					index = 7;
+				}
+				else if (y is >= 1122 and <= 1221)
+				{
+					index = 8;
+				}
+				else if (y is >= 1222 or <= 119)
+				{
+					index = 9;
+				}
+				else if (y is >= 120 and <= 218)
+				{
+					index = 10;
+				}
+				else if (y is >= 219 and <= 320)
+				{
+					index = 11;
+				}
+				else
+				{
+					index = 0;
+				}
+
+				return ConstellationName[index];
+			}
+		}
+
+		#endregion 星座
+
+		#region 生肖
+
+		/// <summary>
+		/// 计算属相的索引,注意虽然属相是以农历年来区别的,但是目前在实际使用中是按公历来计算的
+		/// 鼠年为1,其它类推
+		/// </summary>
+		public int Animal
+		{
+			get
+			{
+				int offset = Date.Year - AnimalStartYear;
+				return offset % 12 + 1;
+			}
+		}
+
+		/// <summary>
+		/// 取属相字符串
+		/// </summary>
+		public string AnimalString
+		{
+			get
+			{
+				int offset = Date.Year - AnimalStartYear; //阳历计算
+				return AnimalStr[offset % 12].ToString();
+			}
+		}
+
+		#endregion 生肖
+
+		#region 天干地支
+
+		/// <summary>
+		/// 取农历年的干支表示法如 乙丑年
+		/// </summary>
+		public string GanZhiYearString
+		{
+			get
+			{
+				int i = (ChineseYear - GanZhiStartYear) % 60; //计算干支
+				var tempStr = TianGan[i % 10] + DiZhi[i % 12].ToString() + "年";
+				return tempStr;
+			}
+		}
+
+		/// <summary>
+		/// 取干支的月表示字符串,注意农历的闰月不记干支
+		/// </summary>
+		public string GanZhiMonthString
+		{
+			get
+			{
+				//每个月的地支总是固定的,而且总是从寅月开始
+				int zhiIndex;
+				if (ChineseMonth > 10)
+				{
+					zhiIndex = ChineseMonth - 10;
+				}
+				else
+				{
+					zhiIndex = ChineseMonth + 2;
+				}
+
+				var zhi = DiZhi[zhiIndex - 1].ToString();
+
+				//根据当年的干支年的干来计算月干的第一个
+				int ganIndex = 1;
+				int i = (ChineseYear - GanZhiStartYear) % 60; //计算干支
+				ganIndex = (i % 10) switch
+				{
+					0 => 3, //甲
+					1 => 5, //乙
+					2 => 7, //丙
+					3 => 9, //丁
+					4 => 1, //戊
+					5 => 3, //己
+					6 => 5, //庚
+					7 => 7, //辛
+					8 => 9, //壬
+					9 => 1, //癸
+					_ => ganIndex
+				};
+
+				var gan = TianGan[(ganIndex + ChineseMonth - 2) % 10].ToString();
+				return gan + zhi + "月";
+			}
+		}
+
+		/// <summary>
+		/// 取干支日表示法
+		/// </summary>
+		public string GanZhiDayString
+		{
+			get
+			{
+				var ts = Date - GanZhiStartDay;
+				var offset = ts.Days;
+				var i = offset % 60;
+				return TianGan[i % 10].ToString() + DiZhi[i % 12] + "日";
+			}
+		}
+
+		/// <summary>
+		/// 取当前日期的干支表示法如 甲子年乙丑月丙庚日
+		/// </summary>
+		public string GanZhiDateString => GanZhiYearString + GanZhiMonthString + GanZhiDayString;
+
+		#endregion 天干地支
+
+		/// <summary>
+		/// 取下一天
+		/// </summary>
+		/// <returns></returns>
+		public ChineseCalendar NextDay => new(Date.AddDays(1));
+
+		/// <summary>
+		/// 取前一天
+		/// </summary>
+		/// <returns></returns>
+		public ChineseCalendar PervDay => new(Date.AddDays(-1));
+
+		/// <summary>
+		/// 取下n天
+		/// </summary>
+		/// <returns></returns>
+		public ChineseCalendar AddDays(int days)
+		{
+			return new ChineseCalendar(Date.AddDays(days));
+		}
+
+		/// <summary>
+		/// 取下n天
+		/// </summary>
+		/// <returns></returns>
+		public ChineseCalendar AddWorkDays(int days)
+		{
+			var cc = new ChineseCalendar(Date);
+			while (true)
+			{
+				cc = cc.AddDays(1);
+				if (cc.IsWorkDay)
+				{
+					days--;
+				}
+
+				if (days == 0)
+				{
+					return cc;
+				}
+			}
+		}
+
+		/// <summary>
+		/// 加n月
+		/// </summary>
+		/// <returns></returns>
+		public ChineseCalendar AddMonths(int months)
+		{
+			return new ChineseCalendar(Date.AddMonths(months));
+		}
+	}
+}

+ 560 - 424
Masuit.Tools.Abstractions/DateTimeExt/DateTimeHelper.cs

@@ -8,428 +8,564 @@ using Masuit.Tools.Models;
 
 namespace Masuit.Tools.DateTimeExt
 {
-    /// <summary>
-    /// 日期操作工具类
-    /// </summary>
-    public static class DateTimeHelper
-    {
-        /// <summary>
-        /// 获取某一年有多少周
-        /// </summary>
-        /// <param name="now"></param>
-        /// <returns>该年周数</returns>
-        public static int GetWeekAmount(this in DateTime now)
-        {
-            var end = new DateTime(now.Year, 12, 31); //该年最后一天
-            var gc = new GregorianCalendar();
-            return gc.GetWeekOfYear(end, CalendarWeekRule.FirstDay, DayOfWeek.Monday); //该年星期数
-        }
-
-        /// <summary>
-        /// 返回年度第几个星期   默认星期日是第一天
-        /// </summary>
-        /// <param name="date">时间</param>
-        /// <returns>第几周</returns>
-        public static int WeekOfYear(this in DateTime date)
-        {
-            var gc = new GregorianCalendar();
-            return gc.GetWeekOfYear(date, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
-        }
-
-        /// <summary>
-        /// 返回年度第几个星期
-        /// </summary>
-        /// <param name="date">时间</param>
-        /// <param name="week">一周的开始日期</param>
-        /// <returns>第几周</returns>
-        public static int WeekOfYear(this in DateTime date, DayOfWeek week)
-        {
-            var gc = new GregorianCalendar();
-            return gc.GetWeekOfYear(date, CalendarWeekRule.FirstDay, week);
-        }
-
-        /// <summary>
-        /// 得到一年中的某周的起始日和截止日
-        /// 周数 nNumWeek
-        /// </summary>
-        /// <param name="now"></param>
-        /// <param name="nNumWeek">第几周</param>
-        public static DateTimeRange GetWeekTime(this DateTime now, int nNumWeek)
-        {
-            var dt = new DateTime(now.Year, 1, 1);
-            dt += new TimeSpan((nNumWeek - 1) * 7, 0, 0, 0);
-            return new DateTimeRange(dt.AddDays(-(int)dt.DayOfWeek + (int)DayOfWeek.Monday), dt.AddDays((int)DayOfWeek.Saturday - (int)dt.DayOfWeek + 1));
-        }
-
-        #region P/Invoke 设置本地时间
-
-        [DllImport("kernel32.dll")]
-        private static extern bool SetLocalTime(ref SystemTime time);
-
-        [StructLayout(LayoutKind.Sequential)]
-        private record struct SystemTime
-        {
-            public short year;
-            public short month;
-            public short dayOfWeek;
-            public short day;
-            public short hour;
-            public short minute;
-            public short second;
-            public short milliseconds;
-        }
-
-        /// <summary>
-        /// 设置本地计算机系统时间,仅支持Windows系统
-        /// </summary>
-        /// <param name="dt">DateTime对象</param>
-        public static void SetLocalTime(this in DateTime dt)
-        {
-            SystemTime st;
-            st.year = (short)dt.Year;
-            st.month = (short)dt.Month;
-            st.dayOfWeek = (short)dt.DayOfWeek;
-            st.day = (short)dt.Day;
-            st.hour = (short)dt.Hour;
-            st.minute = (short)dt.Minute;
-            st.second = (short)dt.Second;
-            st.milliseconds = (short)dt.Millisecond;
-            SetLocalTime(ref st);
-        }
-
-        #endregion P/Invoke 设置本地时间
-
-        /// <summary>
-        /// 返回相对于当前时间的相对天数
-        /// </summary>
-        /// <param name="dt"></param>
-        /// <param name="relativeday">相对天数</param>
-        public static string GetDateTime(this in DateTime dt, int relativeday)
-        {
-            return dt.AddDays(relativeday).ToString("yyyy-MM-dd HH:mm:ss");
-        }
-
-        /// <summary>
-        /// 获取该时间相对于1970-01-01T00:00:00Z的秒数
-        /// </summary>
-        /// <param name="dt"></param>
-        /// <returns></returns>
-        public static long GetTotalSeconds(this in DateTime dt) => new DateTimeOffset(dt).UtcDateTime.Ticks / 10_000_000L - 62135596800L;
-
-        /// <summary>
-        /// 获取该时间相对于1970-01-01T00:00:00Z的毫秒数
-        /// </summary>
-        /// <param name="dt"></param>
-        /// <returns></returns>
-        public static long GetTotalMilliseconds(this in DateTime dt) => new DateTimeOffset(dt).UtcDateTime.Ticks / 10000L - 62135596800000L;
-
-        /// <summary>
-        /// 获取该时间相对于1970-01-01T00:00:00Z的微秒时间戳
-        /// </summary>
-        /// <param name="dt"></param>
-        /// <returns></returns>
-        public static long GetTotalMicroseconds(this in DateTime dt) => (new DateTimeOffset(dt).UtcTicks - 621355968000000000) / 10;
-
-        [DllImport("Kernel32.dll")]
-        private static extern bool QueryPerformanceCounter(out long lpPerformanceCount);
-
-        /// <summary>
-        /// 获取该时间相对于1970-01-01T00:00:00Z的纳秒时间戳
-        /// </summary>
-        /// <param name="dt"></param>
-        /// <returns></returns>
-        public static long GetTotalNanoseconds(this in DateTime dt)
-        {
-            var ticks = (new DateTimeOffset(dt).UtcTicks - 621355968000000000) * 100;
-            if (Environment.OSVersion.Platform == PlatformID.Win32NT)
-            {
-                QueryPerformanceCounter(out var timestamp);
-                return ticks + timestamp % 100;
-            }
-
-            return ticks + Stopwatch.GetTimestamp() % 100;
-        }
-
-        /// <summary>
-        /// 获取该时间相对于1970-01-01T00:00:00Z的分钟数
-        /// </summary>
-        /// <param name="dt"></param>
-        /// <returns></returns>
-        public static double GetTotalMinutes(this in DateTime dt) => new DateTimeOffset(dt).Offset.TotalMinutes;
-
-        /// <summary>
-        /// 获取该时间相对于1970-01-01T00:00:00Z的小时数
-        /// </summary>
-        /// <param name="dt"></param>
-        /// <returns></returns>
-        public static double GetTotalHours(this in DateTime dt) => new DateTimeOffset(dt).Offset.TotalHours;
-
-        /// <summary>
-        /// 获取该时间相对于1970-01-01T00:00:00Z的天数
-        /// </summary>
-        /// <param name="dt"></param>
-        /// <returns></returns>
-        public static double GetTotalDays(this in DateTime dt) => new DateTimeOffset(dt).Offset.TotalDays;
-
-        /// <summary>本年有多少天</summary>
-        /// <param name="dt">日期</param>
-        /// <returns>本天在当年的天数</returns>
-        public static int GetDaysOfYear(this in DateTime dt)
-        {
-            //取得传入参数的年份部分,用来判断是否是闰年
-            int n = dt.Year;
-            return DateTime.IsLeapYear(n) ? 366 : 365;
-        }
-
-        /// <summary>本月有多少天</summary>
-        /// <param name="now"></param>
-        /// <returns>天数</returns>
-        public static int GetDaysOfMonth(this DateTime now)
-        {
-            return now.Month switch
-            {
-                1 => 31,
-                2 => DateTime.IsLeapYear(now.Year) ? 29 : 28,
-                3 => 31,
-                4 => 30,
-                5 => 31,
-                6 => 30,
-                7 => 31,
-                8 => 31,
-                9 => 30,
-                10 => 31,
-                11 => 30,
-                12 => 31,
-                _ => 0
-            };
-        }
-
-        /// <summary>返回当前日期的星期名称</summary>
-        /// <param name="now">日期</param>
-        /// <returns>星期名称</returns>
-        public static string GetWeekNameOfDay(this in DateTime now)
-        {
-            return now.DayOfWeek switch
-            {
-                DayOfWeek.Monday => "星期一",
-                DayOfWeek.Tuesday => "星期二",
-                DayOfWeek.Wednesday => "星期三",
-                DayOfWeek.Thursday => "星期四",
-                DayOfWeek.Friday => "星期五",
-                DayOfWeek.Saturday => "星期六",
-                DayOfWeek.Sunday => "星期日",
-                _ => ""
-            };
-        }
-
-        /// <summary>
-        /// 判断时间是否在区间内
-        /// </summary>
-        /// <param name="this"></param>
-        /// <param name="start">开始</param>
-        /// <param name="end">结束</param>
-        /// <param name="mode">模式</param>
-        /// <returns></returns>
-        public static bool In(this in DateTime @this, DateTime start, DateTime end, RangeMode mode = RangeMode.Close)
-        {
-            return mode switch
-            {
-                RangeMode.Open => start < @this && end > @this,
-                RangeMode.Close => start <= @this && end >= @this,
-                RangeMode.OpenClose => start < @this && end >= @this,
-                RangeMode.CloseOpen => start <= @this && end > @this,
-                _ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null)
-            };
-        }
-
-        /// <summary>
-        /// 返回某年某月最后一天
-        /// </summary>
-        /// <param name="now"></param>
-        /// <returns>日</returns>
-        public static int GetMonthLastDate(this DateTime now)
-        {
-            DateTime lastDay = new DateTime(now.Year, now.Month, new GregorianCalendar().GetDaysInMonth(now.Year, now.Month));
-            return lastDay.Day;
-        }
-
-        /// <summary>
-        /// 获得一段时间内有多少小时
-        /// </summary>
-        /// <param name="start">起始时间</param>
-        /// <param name="end">终止时间</param>
-        /// <returns>小时差</returns>
-        public static string GetTimeDelay(this in DateTime start, DateTime end)
-        {
-            return (end - start).ToString("c");
-        }
-
-        /// <summary>
-        /// 返回时间差
-        /// </summary>
-        /// <param name="dateTime1">时间1</param>
-        /// <param name="dateTime2">时间2</param>
-        /// <returns>时间差</returns>
-        public static string DateDiff(this in DateTime dateTime1, in DateTime dateTime2)
-        {
-            string dateDiff;
-            var ts = dateTime2 - dateTime1;
-            if (ts.TotalDays >= 1)
-            {
-                dateDiff = ts.TotalDays >= 30 ? (ts.TotalDays / 30) + "个月前" : ts.TotalDays + "天前";
-            }
-            else
-            {
-                dateDiff = ts.Hours > 1 ? ts.Hours + "小时前" : ts.Minutes + "分钟前";
-            }
-
-            return dateDiff;
-        }
-
-        /// <summary>
-        /// 计算2个时间差
-        /// </summary>
-        /// <param name="beginTime">开始时间</param>
-        /// <param name="endTime">结束时间</param>
-        /// <returns>时间差</returns>
-        public static string GetDiffTime(this in DateTime beginTime, in DateTime endTime)
-        {
-            string strResout = string.Empty;
-
-            //获得2时间的时间间隔秒计算
-            TimeSpan span = endTime.Subtract(beginTime);
-            if (span.Days >= 365)
-            {
-                strResout += span.Days / 365 + "年";
-            }
-            if (span.Days >= 30)
-            {
-                strResout += span.Days % 365 / 30 + "个月";
-            }
-            if (span.Days >= 1)
-            {
-                strResout += (int)(span.TotalDays % 30.42) + "天";
-            }
-            if (span.Hours >= 1)
-            {
-                strResout += span.Hours + "小时";
-            }
-            if (span.Minutes >= 1)
-            {
-                strResout += span.Minutes + "分钟";
-            }
-            if (span.Seconds >= 1)
-            {
-                strResout += span.Seconds + "秒";
-            }
-            return strResout;
-        }
-
-        /// <summary>
-        /// 根据某个时间段查找在某批时间段中的最大并集
-        /// </summary>
-        /// <param name="destination"></param>
-        /// <param name="sources"></param>
-        /// <typeparam name="T"></typeparam>
-        /// <returns></returns>
-        public static ICollection<T> GetUnionSet<T>(this T destination, List<T> sources) where T : ITimePeriod, new()
-        {
-            var result = true;
-            ICollection<T> frames = new List<T>();
-
-            var timeFrames = sources.Where(frame =>
-                !(destination.Start > frame.End || destination.End < frame.Start)).ToList();
-            if (timeFrames.Any())
-                foreach (var frame in timeFrames)
-                {
-                    frames.Add(frame);
-                    sources.Remove(frame);
-                }
-
-            if (!frames.Any()) return frames;
-            var timePeriod = new T()
-            {
-                End = frames.OrderBy(frame => frame.End).Max(frame => frame.End),
-                Start = frames.OrderBy(frame => frame.Start).Min(frame => frame.Start)
-            };
-
-            while (result)
-            {
-                var maxTimeFrame = GetUnionSet<T>(timePeriod, sources);
-                if (!maxTimeFrame.Any())
-                    result = false;
-                else
-                    foreach (var frame in maxTimeFrame)
-                        frames.Add(frame);
-            }
-
-            return frames;
-        }
-
-        /// <summary>
-        /// 获取一批时间段内存在相互重叠的最大时间段
-        /// </summary>
-        /// <param name="destination">基础时间段</param>
-        /// <param name="sources">一批时间段</param>
-        /// <typeparam name="T"></typeparam>
-        /// <returns></returns>
-        /// <remarks>源数据sources 会受到影响</remarks>
-        public static T GetMaxTimePeriod<T>(this T destination, List<T> sources) where T : ITimePeriod, new()
-        {
-            var list = sources.Select(period => new T()
-            {
-                End = period.End,
-                Start = period.Start,
-            }).ToList();
-
-            var timePeriods = GetUnionSet(destination, list);
-            return new T()
-            {
-                End = timePeriods.OrderBy(period => period.End).Max(period => period.End),
-                Start = timePeriods.OrderBy(period => period.Start).Min(period => period.Start)
-            };
-        }
-    }
-
-    /// <summary>
-    ///
-    /// </summary>
-    public interface ITimePeriod
-    {
-        /// <summary>
-        /// 起始时间
-        /// </summary>
-        public DateTime Start { get; set; }
-
-        /// <summary>
-        /// 终止时间
-        /// </summary>
-        public DateTime End { get; set; }
-    }
-
-    /// <summary>
-    /// 区间模式
-    /// </summary>
-    public enum RangeMode
-    {
-        /// <summary>
-        /// 开区间
-        /// </summary>
-        Open,
-
-        /// <summary>
-        /// 闭区间
-        /// </summary>
-        Close,
-
-        /// <summary>
-        /// 左开右闭区间
-        /// </summary>
-        OpenClose,
-
-        /// <summary>
-        /// 左闭右开区间
-        /// </summary>
-        CloseOpen
-    }
+	/// <summary>
+	/// 日期操作工具类
+	/// </summary>
+	public static class DateTimeHelper
+	{
+		/// <summary>
+		/// 获取某一年有多少周
+		/// </summary>
+		/// <param name="now"></param>
+		/// <returns>该年周数</returns>
+		public static int GetWeekAmount(this in DateTime now)
+		{
+			var end = new DateTime(now.Year, 12, 31); //该年最后一天
+			var gc = new GregorianCalendar();
+			return gc.GetWeekOfYear(end, CalendarWeekRule.FirstDay, DayOfWeek.Monday); //该年星期数
+		}
+
+		/// <summary>
+		/// 返回年度第几个星期   默认星期日是第一天
+		/// </summary>
+		/// <param name="date">时间</param>
+		/// <returns>第几周</returns>
+		public static int WeekOfYear(this in DateTime date)
+		{
+			var gc = new GregorianCalendar();
+			return gc.GetWeekOfYear(date, CalendarWeekRule.FirstDay, DayOfWeek.Sunday);
+		}
+
+		/// <summary>
+		/// 返回年度第几个星期
+		/// </summary>
+		/// <param name="date">时间</param>
+		/// <param name="week">一周的开始日期</param>
+		/// <returns>第几周</returns>
+		public static int WeekOfYear(this in DateTime date, DayOfWeek week)
+		{
+			var gc = new GregorianCalendar();
+			return gc.GetWeekOfYear(date, CalendarWeekRule.FirstDay, week);
+		}
+
+		/// <summary>
+		/// 得到一年中的某周的起始日和截止日
+		/// 周数 nNumWeek
+		/// </summary>
+		/// <param name="now"></param>
+		/// <param name="nNumWeek">第几周</param>
+		public static DateTimeRange GetWeekTime(this DateTime now, int nNumWeek)
+		{
+			var dt = new DateTime(now.Year, 1, 1);
+			dt += new TimeSpan((nNumWeek - 1) * 7, 0, 0, 0);
+			return new DateTimeRange(dt.AddDays(-(int)dt.DayOfWeek + (int)DayOfWeek.Monday), dt.AddDays((int)DayOfWeek.Saturday - (int)dt.DayOfWeek + 1));
+		}
+
+		/// <summary>
+		/// 得到当前周的起始日和截止日
+		/// </summary>
+		/// <param name="dt"></param>
+		public static DateTimeRange GetCurrentWeek(this DateTime dt)
+		{
+			return new DateTimeRange(dt.AddDays(-(int)dt.DayOfWeek + (int)DayOfWeek.Monday).Date, dt.AddDays((int)DayOfWeek.Saturday - (int)dt.DayOfWeek + 1).Date.AddSeconds(86399));
+		}
+
+		/// <summary>
+		/// 得到当前月的起始日和截止日
+		/// </summary>
+		/// <param name="dt"></param>
+		public static DateTimeRange GetCurrentMonth(this DateTime dt)
+		{
+			return new DateTimeRange(new DateTime(dt.Year, dt.Month, 1), new DateTime(dt.Year, dt.Month, GetDaysOfMonth(dt), 23, 59, 59));
+		}
+
+		/// <summary>
+		/// 得到当前农历月的起始日和截止日
+		/// </summary>
+		/// <param name="dt"></param>
+		public static DateTimeRange GetCurrentLunarMonth(this DateTime dt)
+		{
+			var calendar = new ChineseCalendar(dt);
+			return new DateTimeRange(new ChineseCalendar(calendar.ChineseYear, calendar.ChineseMonth, 1).Date, new ChineseCalendar(calendar.ChineseYear, calendar.ChineseMonth, calendar.GetChineseMonthDays()).Date.AddSeconds(86399));
+		}
+
+		/// <summary>
+		/// 得到当前年的起始日和截止日
+		/// </summary>
+		/// <param name="dt"></param>
+		public static DateTimeRange GetCurrentYear(this DateTime dt)
+		{
+			return new DateTimeRange(new DateTime(dt.Year, 1, 1), new DateTime(dt.Year, 12, 31, 23, 59, 59));
+		}
+
+		/// <summary>
+		/// 得到当前农历年的起始日和截止日
+		/// </summary>
+		/// <param name="dt"></param>
+		public static DateTimeRange GetCurrentLunarYear(this DateTime dt)
+		{
+			var calendar = new ChineseCalendar(dt);
+			return new DateTimeRange(new ChineseCalendar(calendar.ChineseYear, 1, 1).Date, new ChineseCalendar(calendar.ChineseYear, 12, calendar.GetChineseMonthDays(calendar.ChineseYear, 12)).Date.AddSeconds(86399));
+		}
+
+		/// <summary>
+		/// 得到当前季度的起始日和截止日
+		/// </summary>
+		/// <param name="dt"></param>
+		public static DateTimeRange GetCurrentQuarter(this DateTime dt)
+		{
+			return dt.Month switch
+			{
+				>= 1 and <= 3 => new DateTimeRange(new DateTime(dt.Year, 1, 1), new DateTime(dt.Year, 3, 31, 23, 59, 59)),
+				>= 4 and <= 6 => new DateTimeRange(new DateTime(dt.Year, 4, 1), new DateTime(dt.Year, 6, 30, 23, 59, 59)),
+				>= 7 and <= 9 => new DateTimeRange(new DateTime(dt.Year, 7, 1), new DateTime(dt.Year, 9, 30, 23, 59, 59)),
+				>= 10 and <= 12 => new DateTimeRange(new DateTime(dt.Year, 10, 1), new DateTime(dt.Year, 12, 31, 23, 59, 59)),
+				_ => throw new ArgumentOutOfRangeException()
+			};
+		}
+
+		/// <summary>
+		/// 得到当前农历季度的起始日和截止日
+		/// </summary>
+		/// <param name="dt"></param>
+		public static DateTimeRange GetCurrentLunarQuarter(this DateTime dt)
+		{
+			var calendar = new ChineseCalendar(dt);
+			return dt.Month switch
+			{
+				>= 1 and <= 3 => new DateTimeRange(new ChineseCalendar(calendar.ChineseYear, 1, 1).Date, new ChineseCalendar(calendar.ChineseYear, 3, calendar.GetChineseMonthDays(calendar.ChineseYear, 3)).Date.AddSeconds(86399)),
+				>= 4 and <= 6 => new DateTimeRange(new ChineseCalendar(calendar.ChineseYear, 4, 1).Date, new ChineseCalendar(calendar.ChineseYear, 6, calendar.GetChineseMonthDays(calendar.ChineseYear, 6)).Date.AddSeconds(86399)),
+				>= 7 and <= 9 => new DateTimeRange(new ChineseCalendar(calendar.ChineseYear, 7, 1).Date, new ChineseCalendar(calendar.ChineseYear, 9, calendar.GetChineseMonthDays(calendar.ChineseYear, 9)).Date.AddSeconds(86399)),
+				>= 10 and <= 12 => new DateTimeRange(new ChineseCalendar(calendar.ChineseYear, 10, 1).Date, new ChineseCalendar(calendar.ChineseYear, 12, calendar.GetChineseMonthDays(calendar.ChineseYear, 12)).Date.AddSeconds(86399)),
+				_ => throw new ArgumentOutOfRangeException()
+			};
+		}
+
+		/// <summary>
+		/// 得到当前农历季度的起始日和截止日
+		/// </summary>
+		/// <param name="dt"></param>
+		public static DateTimeRange GetCurrentSolar(this DateTime dt)
+		{
+			var calendar = new ChineseCalendar(dt);
+			ChineseCalendar[] quarters =
+			[
+				calendar.ChineseTwentyFourPrevDay.ChineseTwentyFourPrevDay.ChineseTwentyFourPrevDay,
+				calendar.ChineseTwentyFourPrevDay.ChineseTwentyFourPrevDay,
+				calendar.ChineseTwentyFourPrevDay,
+				calendar,
+				calendar.ChineseTwentyFourNextDay,
+				calendar.ChineseTwentyFourNextDay.ChineseTwentyFourNextDay,
+				calendar.ChineseTwentyFourNextDay.ChineseTwentyFourNextDay.ChineseTwentyFourNextDay
+			];
+			var solar = quarters.LastOrDefault(c => new[] { "春分", "夏至", "秋分", "冬至" }.Contains(c.ChineseTwentyFourDay));
+			var start = solar.ChineseTwentyFourPrevDay.ChineseTwentyFourPrevDay.ChineseTwentyFourPrevDay;
+			var end = solar.ChineseTwentyFourNextDay.ChineseTwentyFourNextDay.ChineseTwentyFourNextDay;
+			return new DateTimeRange(start.Date, end.Date.AddSeconds(-1));
+		}
+
+		/// <summary>
+		/// 得到当前范围的起始日和截止日
+		/// </summary>
+		/// <param name="dt"></param>
+		/// <param name="type"></param>
+		public static DateTimeRange GetCurrentRange(this DateTime dt, DateRangeType type)
+		{
+			return type switch
+			{
+				DateRangeType.Week => GetCurrentWeek(dt),
+				DateRangeType.Month => GetCurrentMonth(dt),
+				DateRangeType.Quarter => GetCurrentQuarter(dt),
+				DateRangeType.Year => GetCurrentYear(dt),
+				DateRangeType.LunarMonth => GetCurrentLunarMonth(dt),
+				DateRangeType.LunarQuarter => GetCurrentLunarQuarter(dt),
+				DateRangeType.Solar => GetCurrentSolar(dt),
+				DateRangeType.LunarYear => GetCurrentLunarYear(dt),
+				_ => throw new ArgumentOutOfRangeException(nameof(type), type, null)
+			};
+		}
+
+		#region P/Invoke 设置本地时间
+
+		[DllImport("kernel32.dll")]
+		private static extern bool SetLocalTime(ref SystemTime time);
+
+		[StructLayout(LayoutKind.Sequential)]
+		private record struct SystemTime
+		{
+			public short year;
+			public short month;
+			public short dayOfWeek;
+			public short day;
+			public short hour;
+			public short minute;
+			public short second;
+			public short milliseconds;
+		}
+
+		/// <summary>
+		/// 设置本地计算机系统时间,仅支持Windows系统
+		/// </summary>
+		/// <param name="dt">DateTime对象</param>
+		public static void SetLocalTime(this in DateTime dt)
+		{
+			SystemTime st;
+			st.year = (short)dt.Year;
+			st.month = (short)dt.Month;
+			st.dayOfWeek = (short)dt.DayOfWeek;
+			st.day = (short)dt.Day;
+			st.hour = (short)dt.Hour;
+			st.minute = (short)dt.Minute;
+			st.second = (short)dt.Second;
+			st.milliseconds = (short)dt.Millisecond;
+			SetLocalTime(ref st);
+		}
+
+		#endregion P/Invoke 设置本地时间
+
+		/// <summary>
+		/// 返回相对于当前时间的相对天数
+		/// </summary>
+		/// <param name="dt"></param>
+		/// <param name="relativeday">相对天数</param>
+		public static string GetDateTime(this in DateTime dt, int relativeday)
+		{
+			return dt.AddDays(relativeday).ToString("yyyy-MM-dd HH:mm:ss");
+		}
+
+		/// <summary>
+		/// 获取该时间相对于1970-01-01T00:00:00Z的秒数
+		/// </summary>
+		/// <param name="dt"></param>
+		/// <returns></returns>
+		public static long GetTotalSeconds(this in DateTime dt) => new DateTimeOffset(dt).UtcDateTime.Ticks / 10_000_000L - 62135596800L;
+
+		/// <summary>
+		/// 获取该时间相对于1970-01-01T00:00:00Z的毫秒数
+		/// </summary>
+		/// <param name="dt"></param>
+		/// <returns></returns>
+		public static long GetTotalMilliseconds(this in DateTime dt) => new DateTimeOffset(dt).UtcDateTime.Ticks / 10000L - 62135596800000L;
+
+		/// <summary>
+		/// 获取该时间相对于1970-01-01T00:00:00Z的微秒时间戳
+		/// </summary>
+		/// <param name="dt"></param>
+		/// <returns></returns>
+		public static long GetTotalMicroseconds(this in DateTime dt) => (new DateTimeOffset(dt).UtcTicks - 621355968000000000) / 10;
+
+		[DllImport("Kernel32.dll")]
+		private static extern bool QueryPerformanceCounter(out long lpPerformanceCount);
+
+		/// <summary>
+		/// 获取该时间相对于1970-01-01T00:00:00Z的纳秒时间戳
+		/// </summary>
+		/// <param name="dt"></param>
+		/// <returns></returns>
+		public static long GetTotalNanoseconds(this in DateTime dt)
+		{
+			var ticks = (new DateTimeOffset(dt).UtcTicks - 621355968000000000) * 100;
+			if (Environment.OSVersion.Platform == PlatformID.Win32NT)
+			{
+				QueryPerformanceCounter(out var timestamp);
+				return ticks + timestamp % 100;
+			}
+
+			return ticks + Stopwatch.GetTimestamp() % 100;
+		}
+
+		/// <summary>
+		/// 获取该时间相对于1970-01-01T00:00:00Z的分钟数
+		/// </summary>
+		/// <param name="dt"></param>
+		/// <returns></returns>
+		public static double GetTotalMinutes(this in DateTime dt) => new DateTimeOffset(dt).Offset.TotalMinutes;
+
+		/// <summary>
+		/// 获取该时间相对于1970-01-01T00:00:00Z的小时数
+		/// </summary>
+		/// <param name="dt"></param>
+		/// <returns></returns>
+		public static double GetTotalHours(this in DateTime dt) => new DateTimeOffset(dt).Offset.TotalHours;
+
+		/// <summary>
+		/// 获取该时间相对于1970-01-01T00:00:00Z的天数
+		/// </summary>
+		/// <param name="dt"></param>
+		/// <returns></returns>
+		public static double GetTotalDays(this in DateTime dt) => new DateTimeOffset(dt).Offset.TotalDays;
+
+		/// <summary>本年有多少天</summary>
+		/// <param name="dt">日期</param>
+		/// <returns>本天在当年的天数</returns>
+		public static int GetDaysOfYear(this in DateTime dt)
+		{
+			//取得传入参数的年份部分,用来判断是否是闰年
+			int n = dt.Year;
+			return DateTime.IsLeapYear(n) ? 366 : 365;
+		}
+
+		/// <summary>本月有多少天</summary>
+		/// <param name="now"></param>
+		/// <returns>天数</returns>
+		public static int GetDaysOfMonth(this DateTime now)
+		{
+			return now.Month switch
+			{
+				1 => 31,
+				2 => DateTime.IsLeapYear(now.Year) ? 29 : 28,
+				3 => 31,
+				4 => 30,
+				5 => 31,
+				6 => 30,
+				7 => 31,
+				8 => 31,
+				9 => 30,
+				10 => 31,
+				11 => 30,
+				12 => 31,
+				_ => 0
+			};
+		}
+
+		/// <summary>返回当前日期的星期名称</summary>
+		/// <param name="now">日期</param>
+		/// <returns>星期名称</returns>
+		public static string GetWeekNameOfDay(this in DateTime now)
+		{
+			return now.DayOfWeek switch
+			{
+				DayOfWeek.Monday => "星期一",
+				DayOfWeek.Tuesday => "星期二",
+				DayOfWeek.Wednesday => "星期三",
+				DayOfWeek.Thursday => "星期四",
+				DayOfWeek.Friday => "星期五",
+				DayOfWeek.Saturday => "星期六",
+				DayOfWeek.Sunday => "星期日",
+				_ => ""
+			};
+		}
+
+		/// <summary>
+		/// 判断时间是否在区间内
+		/// </summary>
+		/// <param name="this"></param>
+		/// <param name="start">开始</param>
+		/// <param name="end">结束</param>
+		/// <param name="mode">模式</param>
+		/// <returns></returns>
+		public static bool In(this in DateTime @this, DateTime start, DateTime end, RangeMode mode = RangeMode.Close)
+		{
+			return mode switch
+			{
+				RangeMode.Open => start < @this && end > @this,
+				RangeMode.Close => start <= @this && end >= @this,
+				RangeMode.OpenClose => start < @this && end >= @this,
+				RangeMode.CloseOpen => start <= @this && end > @this,
+				_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null)
+			};
+		}
+
+		/// <summary>
+		/// 返回某年某月最后一天
+		/// </summary>
+		/// <param name="now"></param>
+		/// <returns>日</returns>
+		public static int GetMonthLastDate(this DateTime now)
+		{
+			DateTime lastDay = new DateTime(now.Year, now.Month, new GregorianCalendar().GetDaysInMonth(now.Year, now.Month));
+			return lastDay.Day;
+		}
+
+		/// <summary>
+		/// 获得一段时间内有多少小时
+		/// </summary>
+		/// <param name="start">起始时间</param>
+		/// <param name="end">终止时间</param>
+		/// <returns>小时差</returns>
+		public static string GetTimeDelay(this in DateTime start, DateTime end)
+		{
+			return (end - start).ToString("c");
+		}
+
+		/// <summary>
+		/// 返回时间差
+		/// </summary>
+		/// <param name="dateTime1">时间1</param>
+		/// <param name="dateTime2">时间2</param>
+		/// <returns>时间差</returns>
+		public static string DateDiff(this in DateTime dateTime1, in DateTime dateTime2)
+		{
+			string dateDiff;
+			var ts = dateTime2 - dateTime1;
+			if (ts.TotalDays >= 1)
+			{
+				dateDiff = ts.TotalDays >= 30 ? (ts.TotalDays / 30) + "个月前" : ts.TotalDays + "天前";
+			}
+			else
+			{
+				dateDiff = ts.Hours > 1 ? ts.Hours + "小时前" : ts.Minutes + "分钟前";
+			}
+
+			return dateDiff;
+		}
+
+		/// <summary>
+		/// 计算2个时间差
+		/// </summary>
+		/// <param name="beginTime">开始时间</param>
+		/// <param name="endTime">结束时间</param>
+		/// <returns>时间差</returns>
+		public static string GetDiffTime(this in DateTime beginTime, in DateTime endTime)
+		{
+			string strResout = string.Empty;
+
+			//获得2时间的时间间隔秒计算
+			TimeSpan span = endTime.Subtract(beginTime);
+			if (span.Days >= 365)
+			{
+				strResout += span.Days / 365 + "年";
+			}
+			if (span.Days >= 30)
+			{
+				strResout += span.Days % 365 / 30 + "个月";
+			}
+			if (span.Days >= 1)
+			{
+				strResout += (int)(span.TotalDays % 30.42) + "天";
+			}
+			if (span.Hours >= 1)
+			{
+				strResout += span.Hours + "小时";
+			}
+			if (span.Minutes >= 1)
+			{
+				strResout += span.Minutes + "分钟";
+			}
+			if (span.Seconds >= 1)
+			{
+				strResout += span.Seconds + "秒";
+			}
+			return strResout;
+		}
+
+		/// <summary>
+		/// 根据某个时间段查找在某批时间段中的最大并集
+		/// </summary>
+		/// <param name="destination"></param>
+		/// <param name="sources"></param>
+		/// <typeparam name="T"></typeparam>
+		/// <returns></returns>
+		public static ICollection<T> GetUnionSet<T>(this T destination, List<T> sources) where T : ITimePeriod, new()
+		{
+			var result = true;
+			ICollection<T> frames = new List<T>();
+
+			var timeFrames = sources.Where(frame =>
+				!(destination.Start > frame.End || destination.End < frame.Start)).ToList();
+			if (timeFrames.Any())
+				foreach (var frame in timeFrames)
+				{
+					frames.Add(frame);
+					sources.Remove(frame);
+				}
+
+			if (!frames.Any()) return frames;
+			var timePeriod = new T()
+			{
+				End = frames.OrderBy(frame => frame.End).Max(frame => frame.End),
+				Start = frames.OrderBy(frame => frame.Start).Min(frame => frame.Start)
+			};
+
+			while (result)
+			{
+				var maxTimeFrame = GetUnionSet<T>(timePeriod, sources);
+				if (!maxTimeFrame.Any())
+					result = false;
+				else
+					foreach (var frame in maxTimeFrame)
+						frames.Add(frame);
+			}
+
+			return frames;
+		}
+
+		/// <summary>
+		/// 获取一批时间段内存在相互重叠的最大时间段
+		/// </summary>
+		/// <param name="destination">基础时间段</param>
+		/// <param name="sources">一批时间段</param>
+		/// <typeparam name="T"></typeparam>
+		/// <returns></returns>
+		/// <remarks>源数据sources 会受到影响</remarks>
+		public static T GetMaxTimePeriod<T>(this T destination, List<T> sources) where T : ITimePeriod, new()
+		{
+			var list = sources.Select(period => new T()
+			{
+				End = period.End,
+				Start = period.Start,
+			}).ToList();
+
+			var timePeriods = GetUnionSet(destination, list);
+			return new T()
+			{
+				End = timePeriods.OrderBy(period => period.End).Max(period => period.End),
+				Start = timePeriods.OrderBy(period => period.Start).Min(period => period.Start)
+			};
+		}
+	}
+
+	/// <summary>
+	///
+	/// </summary>
+	public interface ITimePeriod
+	{
+		/// <summary>
+		/// 起始时间
+		/// </summary>
+		public DateTime Start { get; set; }
+
+		/// <summary>
+		/// 终止时间
+		/// </summary>
+		public DateTime End { get; set; }
+	}
+
+	/// <summary>
+	/// 区间模式
+	/// </summary>
+	public enum RangeMode
+	{
+		/// <summary>
+		/// 开区间
+		/// </summary>
+		Open,
+
+		/// <summary>
+		/// 闭区间
+		/// </summary>
+		Close,
+
+		/// <summary>
+		/// 左开右闭区间
+		/// </summary>
+		OpenClose,
+
+		/// <summary>
+		/// 左闭右开区间
+		/// </summary>
+		CloseOpen
+	}
+
+	public enum DateRangeType
+	{
+		Week,
+		Month,
+		Quarter,
+		Year,
+		LunarMonth,
+		LunarQuarter,
+		Solar,
+		LunarYear,
+	}
 }

+ 1115 - 1091
Masuit.Tools.Abstractions/Extensions/BaseType/IEnumerableExtensions.cs

@@ -14,1101 +14,1125 @@ namespace Masuit.Tools;
 /// </summary>
 public static class IEnumerableExtensions
 {
-    /// <summary>
-    /// 按字段属性判等取交集
-    /// </summary>
-    /// <typeparam name="TFirst"></typeparam>
-    /// <typeparam name="TSecond"></typeparam>
-    /// <param name="second"></param>
-    /// <param name="condition"></param>
-    /// <param name="first"></param>
-    /// <returns></returns>
-    public static IEnumerable<TFirst> IntersectBy<TFirst, TSecond>(this IEnumerable<TFirst> first, IEnumerable<TSecond> second, Func<TFirst, TSecond, bool> condition)
-    {
-        return first.Where(f => second.Any(s => condition(f, s)));
-    }
-
-    /// <summary>
-    /// 按字段属性判等取交集
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TKey"></typeparam>
-    /// <param name="first"></param>
-    /// <param name="second"></param>
-    /// <param name="keySelector"></param>
-    /// <returns></returns>
-    public static IEnumerable<TSource> IntersectBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TKey> keySelector)
-    {
-        return first.IntersectBy(second, keySelector, null);
-    }
-
-    /// <summary>
-    /// 按字段属性判等取交集
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TKey"></typeparam>
-    /// <param name="first"></param>
-    /// <param name="second"></param>
-    /// <param name="keySelector"></param>
-    /// <param name="comparer"></param>
-    /// <returns></returns>
-    public static IEnumerable<TSource> IntersectBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
-    {
-        if (first == null)
-            throw new ArgumentNullException(nameof(first));
-        if (second == null)
-            throw new ArgumentNullException(nameof(second));
-        if (keySelector == null)
-            throw new ArgumentNullException(nameof(keySelector));
-        return IntersectByIterator(first, second, keySelector, comparer);
-    }
-
-    private static IEnumerable<TSource> IntersectByIterator<TSource, TKey>(IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
-    {
-        var set = new HashSet<TKey>(second.Select(keySelector), comparer);
-        foreach (var item in first.Where(source => set.Remove(keySelector(source))))
-        {
-            yield return item;
-        }
-    }
-
-    /// <summary>
-    /// 多个集合取交集元素
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="source"></param>
-    /// <returns></returns>
-    public static IEnumerable<T> IntersectAll<T>(this IEnumerable<IEnumerable<T>> source)
-    {
-        return source.Aggregate((current, item) => current.Intersect(item));
-    }
-
-    /// <summary>
-    /// 多个集合取交集元素
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TKey"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="keySelector"></param>
-    /// <returns></returns>
-    public static IEnumerable<TSource> IntersectAll<TSource, TKey>(this IEnumerable<IEnumerable<TSource>> source, Func<TSource, TKey> keySelector)
-    {
-        return source.Aggregate((current, item) => current.IntersectBy(item, keySelector));
-    }
-
-    /// <summary>
-    /// 多个集合取交集元素
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TKey"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="keySelector"></param>
-    /// <param name="comparer"></param>
-    /// <returns></returns>
-    public static IEnumerable<TSource> IntersectAll<TSource, TKey>(this IEnumerable<IEnumerable<TSource>> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
-    {
-        return source.Aggregate((current, item) => current.IntersectBy(item, keySelector, comparer));
-    }
-
-    /// <summary>
-    /// 多个集合取交集元素
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="comparer"></param>
-    /// <returns></returns>
-    public static IEnumerable<T> IntersectAll<T>(this IEnumerable<IEnumerable<T>> source, IEqualityComparer<T> comparer)
-    {
-        return source.Aggregate((current, item) => current.Intersect(item, comparer));
-    }
-
-    /// <summary>
-    /// 按字段属性判等取差集
-    /// </summary>
-    /// <typeparam name="TFirst"></typeparam>
-    /// <typeparam name="TSecond"></typeparam>
-    /// <param name="second"></param>
-    /// <param name="condition"></param>
-    /// <param name="first"></param>
-    /// <returns></returns>
-    public static IEnumerable<TFirst> ExceptBy<TFirst, TSecond>(this IEnumerable<TFirst> first, IEnumerable<TSecond> second, Func<TFirst, TSecond, bool> condition)
-    {
-        return first.Where(f => !second.Any(s => condition(f, s)));
-    }
+	/// <summary>
+	/// 按字段属性判等取交集
+	/// </summary>
+	/// <typeparam name="TFirst"></typeparam>
+	/// <typeparam name="TSecond"></typeparam>
+	/// <param name="second"></param>
+	/// <param name="condition"></param>
+	/// <param name="first"></param>
+	/// <returns></returns>
+	public static IEnumerable<TFirst> IntersectBy<TFirst, TSecond>(this IEnumerable<TFirst> first, IEnumerable<TSecond> second, Func<TFirst, TSecond, bool> condition)
+	{
+		return first.Where(f => second.Any(s => condition(f, s)));
+	}
+
+	/// <summary>
+	/// 按字段属性判等取交集
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TKey"></typeparam>
+	/// <param name="first"></param>
+	/// <param name="second"></param>
+	/// <param name="keySelector"></param>
+	/// <returns></returns>
+	public static IEnumerable<TSource> IntersectBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TKey> keySelector)
+	{
+		return first.IntersectBy(second, keySelector, null);
+	}
+
+	/// <summary>
+	/// 按字段属性判等取交集
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TKey"></typeparam>
+	/// <param name="first"></param>
+	/// <param name="second"></param>
+	/// <param name="keySelector"></param>
+	/// <param name="comparer"></param>
+	/// <returns></returns>
+	public static IEnumerable<TSource> IntersectBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
+	{
+		if (first == null)
+			throw new ArgumentNullException(nameof(first));
+		if (second == null)
+			throw new ArgumentNullException(nameof(second));
+		if (keySelector == null)
+			throw new ArgumentNullException(nameof(keySelector));
+		return IntersectByIterator(first, second, keySelector, comparer);
+	}
+
+	private static IEnumerable<TSource> IntersectByIterator<TSource, TKey>(IEnumerable<TSource> first, IEnumerable<TSource> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
+	{
+		var set = new HashSet<TKey>(second.Select(keySelector), comparer);
+		foreach (var item in first.Where(source => set.Remove(keySelector(source))))
+		{
+			yield return item;
+		}
+	}
+
+	/// <summary>
+	/// 多个集合取交集元素
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="source"></param>
+	/// <returns></returns>
+	public static IEnumerable<T> IntersectAll<T>(this IEnumerable<IEnumerable<T>> source)
+	{
+		return source.Aggregate((current, item) => current.Intersect(item));
+	}
+
+	/// <summary>
+	/// 多个集合取交集元素
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TKey"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="keySelector"></param>
+	/// <returns></returns>
+	public static IEnumerable<TSource> IntersectAll<TSource, TKey>(this IEnumerable<IEnumerable<TSource>> source, Func<TSource, TKey> keySelector)
+	{
+		return source.Aggregate((current, item) => current.IntersectBy(item, keySelector));
+	}
+
+	/// <summary>
+	/// 多个集合取交集元素
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TKey"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="keySelector"></param>
+	/// <param name="comparer"></param>
+	/// <returns></returns>
+	public static IEnumerable<TSource> IntersectAll<TSource, TKey>(this IEnumerable<IEnumerable<TSource>> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
+	{
+		return source.Aggregate((current, item) => current.IntersectBy(item, keySelector, comparer));
+	}
+
+	/// <summary>
+	/// 多个集合取交集元素
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="comparer"></param>
+	/// <returns></returns>
+	public static IEnumerable<T> IntersectAll<T>(this IEnumerable<IEnumerable<T>> source, IEqualityComparer<T> comparer)
+	{
+		return source.Aggregate((current, item) => current.Intersect(item, comparer));
+	}
+
+	/// <summary>
+	/// 按字段属性判等取差集
+	/// </summary>
+	/// <typeparam name="TFirst"></typeparam>
+	/// <typeparam name="TSecond"></typeparam>
+	/// <param name="second"></param>
+	/// <param name="condition"></param>
+	/// <param name="first"></param>
+	/// <returns></returns>
+	public static IEnumerable<TFirst> ExceptBy<TFirst, TSecond>(this IEnumerable<TFirst> first, IEnumerable<TSecond> second, Func<TFirst, TSecond, bool> condition)
+	{
+		return first.Where(f => !second.Any(s => condition(f, s)));
+	}
 
 #if NET6_0_OR_GREATER
 #else
 
-    /// <summary>
-    /// 按字段去重
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TKey"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="keySelector"></param>
-    /// <returns></returns>
-    public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
-    {
-        if (source == null) throw new ArgumentNullException(nameof(source));
-        if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
-        var set = new HashSet<TKey>();
-        return source.Where(item => set.Add(keySelector(item)));
-    }
-
-    /// <summary>
-    /// 按字段属性判等取交集
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TKey"></typeparam>
-    /// <param name="first"></param>
-    /// <param name="second"></param>
-    /// <param name="keySelector"></param>
-    /// <returns></returns>
-    public static IEnumerable<TSource> IntersectBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelector)
-    {
-        return first.IntersectBy(second, keySelector, null);
-    }
-
-    /// <summary>
-    /// 按字段属性判等取交集
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TKey"></typeparam>
-    /// <param name="first"></param>
-    /// <param name="second"></param>
-    /// <param name="keySelector"></param>
-    /// <param name="comparer"></param>
-    /// <returns></returns>
-    public static IEnumerable<TSource> IntersectBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
-    {
-        if (first == null) throw new ArgumentNullException(nameof(first));
-        if (second == null) throw new ArgumentNullException(nameof(second));
-        if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
-        return IntersectByIterator(first, second, keySelector, comparer);
-    }
-
-    private static IEnumerable<TSource> IntersectByIterator<TSource, TKey>(IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
-    {
-        var set = new HashSet<TKey>(second, comparer);
-        return first.Where(source => set.Remove(keySelector(source)));
-    }
-
-    /// <summary>
-    /// 按字段属性判等取差集
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TKey"></typeparam>
-    /// <param name="first"></param>
-    /// <param name="second"></param>
-    /// <param name="keySelector"></param>
-    /// <returns></returns>
-    public static IEnumerable<TSource> ExceptBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelector)
-    {
-        return first.ExceptBy(second, keySelector, null);
-    }
-
-    /// <summary>
-    /// 按字段属性判等取差集
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TKey"></typeparam>
-    /// <param name="first"></param>
-    /// <param name="second"></param>
-    /// <param name="keySelector"></param>
-    /// <param name="comparer"></param>
-    /// <returns></returns>
-    /// <exception cref="ArgumentNullException"></exception>
-    public static IEnumerable<TSource> ExceptBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
-    {
-        if (first == null) throw new ArgumentNullException(nameof(first));
-        if (second == null) throw new ArgumentNullException(nameof(second));
-        if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
-        return ExceptByIterator(first, second, keySelector, comparer);
-    }
-
-    private static IEnumerable<TSource> ExceptByIterator<TSource, TKey>(IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
-    {
-        var set = new HashSet<TKey>(second, comparer);
-        return first.Where(source => set.Add(keySelector(source)));
-    }
+	/// <summary>
+	/// 按字段去重
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TKey"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="keySelector"></param>
+	/// <returns></returns>
+	public static IEnumerable<TSource> DistinctBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
+	{
+		if (source == null) throw new ArgumentNullException(nameof(source));
+		if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
+		var set = new HashSet<TKey>();
+		return source.Where(item => set.Add(keySelector(item)));
+	}
+
+	/// <summary>
+	/// 按字段属性判等取交集
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TKey"></typeparam>
+	/// <param name="first"></param>
+	/// <param name="second"></param>
+	/// <param name="keySelector"></param>
+	/// <returns></returns>
+	public static IEnumerable<TSource> IntersectBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelector)
+	{
+		return first.IntersectBy(second, keySelector, null);
+	}
+
+	/// <summary>
+	/// 按字段属性判等取交集
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TKey"></typeparam>
+	/// <param name="first"></param>
+	/// <param name="second"></param>
+	/// <param name="keySelector"></param>
+	/// <param name="comparer"></param>
+	/// <returns></returns>
+	public static IEnumerable<TSource> IntersectBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
+	{
+		if (first == null) throw new ArgumentNullException(nameof(first));
+		if (second == null) throw new ArgumentNullException(nameof(second));
+		if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
+		return IntersectByIterator(first, second, keySelector, comparer);
+	}
+
+	private static IEnumerable<TSource> IntersectByIterator<TSource, TKey>(IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
+	{
+		var set = new HashSet<TKey>(second, comparer);
+		return first.Where(source => set.Remove(keySelector(source)));
+	}
+
+	/// <summary>
+	/// 按字段属性判等取差集
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TKey"></typeparam>
+	/// <param name="first"></param>
+	/// <param name="second"></param>
+	/// <param name="keySelector"></param>
+	/// <returns></returns>
+	public static IEnumerable<TSource> ExceptBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelector)
+	{
+		return first.ExceptBy(second, keySelector, null);
+	}
+
+	/// <summary>
+	/// 按字段属性判等取差集
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TKey"></typeparam>
+	/// <param name="first"></param>
+	/// <param name="second"></param>
+	/// <param name="keySelector"></param>
+	/// <param name="comparer"></param>
+	/// <returns></returns>
+	/// <exception cref="ArgumentNullException"></exception>
+	public static IEnumerable<TSource> ExceptBy<TSource, TKey>(this IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
+	{
+		if (first == null) throw new ArgumentNullException(nameof(first));
+		if (second == null) throw new ArgumentNullException(nameof(second));
+		if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
+		return ExceptByIterator(first, second, keySelector, comparer);
+	}
+
+	private static IEnumerable<TSource> ExceptByIterator<TSource, TKey>(IEnumerable<TSource> first, IEnumerable<TKey> second, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
+	{
+		var set = new HashSet<TKey>(second, comparer);
+		return first.Where(source => set.Add(keySelector(source)));
+	}
 
 #endif
 
-    /// <summary>
-    /// 添加多个元素
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="this"></param>
-    /// <param name="values"></param>
-    public static void AddRange<T>(this ICollection<T> @this, params T[] values)
-    {
-        foreach (var obj in values)
-        {
-            @this.Add(obj);
-        }
-    }
-
-    /// <summary>
-    /// 添加多个元素
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="this"></param>
-    /// <param name="values"></param>
-    public static void AddRange<T>(this ICollection<T> @this, IEnumerable<T> values)
-    {
-        foreach (var obj in values)
-        {
-            @this.Add(obj);
-        }
-    }
-
-    /// <summary>
-    /// 添加多个元素
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="this"></param>
-    /// <param name="values"></param>
-    public static void AddRange<T>(this ConcurrentBag<T> @this, params T[] values)
-    {
-        foreach (var obj in values)
-        {
-            @this.Add(obj);
-        }
-    }
-
-    /// <summary>
-    /// 添加多个元素
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="this"></param>
-    /// <param name="values"></param>
-    public static void AddRange<T>(this ConcurrentQueue<T> @this, params T[] values)
-    {
-        foreach (var obj in values)
-        {
-            @this.Enqueue(obj);
-        }
-    }
-
-    /// <summary>
-    /// 添加符合条件的多个元素
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="this"></param>
-    /// <param name="predicate"></param>
-    /// <param name="values"></param>
-    public static void AddRangeIf<T>(this ICollection<T> @this, Func<T, bool> predicate, params T[] values)
-    {
-        foreach (var obj in values.Where(predicate))
-        {
-            @this.Add(obj);
-        }
-    }
-
-    /// <summary>
-    /// 添加符合条件的多个元素
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="this"></param>
-    /// <param name="predicate"></param>
-    /// <param name="values"></param>
-    public static void AddRangeIf<T>(this ConcurrentBag<T> @this, Func<T, bool> predicate, params T[] values)
-    {
-        foreach (var obj in values.Where(predicate))
-        {
-            @this.Add(obj);
-        }
-    }
-
-    /// <summary>
-    /// 添加符合条件的多个元素
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="this"></param>
-    /// <param name="predicate"></param>
-    /// <param name="values"></param>
-    public static void AddRangeIf<T>(this ConcurrentQueue<T> @this, Func<T, bool> predicate, params T[] values)
-    {
-        foreach (var obj in values.Where(predicate))
-        {
-            @this.Enqueue(obj);
-        }
-    }
-
-    /// <summary>
-    /// 添加不重复的元素
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="this"></param>
-    /// <param name="values"></param>
-    public static void AddRangeIfNotContains<T>(this ICollection<T> @this, params T[] values)
-    {
-        foreach (T obj in values)
-        {
-            if ([email protected](obj))
-            {
-                @this.Add(obj);
-            }
-        }
-    }
-
-    /// <summary>
-    /// 移除符合条件的元素
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="this"></param>
-    /// <param name="where"></param>
-    public static void RemoveWhere<T>(this ICollection<T> @this, Func<T, bool> @where)
-    {
-        foreach (var obj in @this.Where(where).ToList())
-        {
-            @this.Remove(obj);
-        }
-    }
-
-    /// <summary>
-    /// 在元素之后添加元素
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="list"></param>
-    /// <param name="condition">条件</param>
-    /// <param name="value">值</param>
-    public static void InsertAfter<T>(this IList<T> list, Func<T, bool> condition, T value)
-    {
-        foreach (var index in list.Select((item, index) => new
-        {
-            item,
-            index
-        }).Where(p => condition(p.item)).OrderByDescending(p => p.index).Select(t => t.index))
-        {
-            if (index + 1 == list.Count)
-            {
-                list.Add(value);
-            }
-            else
-            {
-                list.Insert(index + 1, value);
-            }
-        }
-    }
-
-    /// <summary>
-    /// 在元素之后添加元素
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="list"></param>
-    /// <param name="index">索引位置</param>
-    /// <param name="value">值</param>
-    public static void InsertAfter<T>(this IList<T> list, int index, T value)
-    {
-        foreach (var i in list.Select((v, i) => new
-        {
-            Value = v,
-            Index = i
-        }).Where(p => p.Index == index).OrderByDescending(p => p.Index).Select(t => t.Index))
-        {
-            if (i + 1 == list.Count)
-            {
-                list.Add(value);
-            }
-            else
-            {
-                list.Insert(i + 1, value);
-            }
-        }
-    }
-
-    /// <summary>
-    /// 转HashSet
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <returns></returns>
-    public static HashSet<TResult> ToHashSet<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector)
-    {
-        return new HashSet<TResult>(source.Select(selector));
-    }
-
-    /// <summary>
-    /// 遍历IEnumerable
-    /// </summary>
-    /// <param name="objs"></param>
-    /// <param name="action">回调方法</param>
-    /// <typeparam name="T"></typeparam>
-    public static void ForEach<T>(this IEnumerable<T> objs, Action<T> action)
-    {
-        foreach (var o in objs)
-        {
-            action(o);
-        }
-    }
-
-    /// <summary>
-    /// 异步foreach
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="maxParallelCount">最大并行数</param>
-    /// <param name="action"></param>
-    /// <param name="cancellationToken"></param>
-    /// <returns></returns>
-    public static async Task ForeachAsync<T>(this IEnumerable<T> source, Func<T, Task> action, int maxParallelCount, CancellationToken cancellationToken = default)
-    {
-        if (Debugger.IsAttached)
-        {
-            foreach (var item in source)
-            {
-                await action(item);
-            }
-
-            return;
-        }
-
-        var list = new List<Task>();
-        foreach (var item in source)
-        {
-            if (cancellationToken.IsCancellationRequested)
-            {
-                return;
-            }
-
-            list.Add(action(item));
-            if (list.Count(t => !t.IsCompleted) >= maxParallelCount)
-            {
-                await Task.WhenAny(list);
-                list.RemoveAll(t => t.IsCompleted);
-            }
-        }
-
-        await Task.WhenAll(list);
-    }
-
-    /// <summary>
-    /// 异步foreach
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="action"></param>
-    /// <param name="cancellationToken"></param>
-    /// <returns></returns>
-    public static Task ForeachAsync<T>(this IEnumerable<T> source, Func<T, Task> action, CancellationToken cancellationToken = default)
-    {
-        if (source is ICollection<T> collection)
-        {
-            return ForeachAsync(collection, action, collection.Count, cancellationToken);
-        }
-
-        var list = source.ToList();
-        return ForeachAsync(list, action, list.Count, cancellationToken);
-    }
-
-    /// <summary>
-    /// 异步Select
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <returns></returns>
-    public static Task<TResult[]> SelectAsync<T, TResult>(this IEnumerable<T> source, Func<T, Task<TResult>> selector)
-    {
-        return Task.WhenAll(source.Select(selector));
-    }
-
-    /// <summary>
-    /// 异步Select
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <returns></returns>
-    public static Task<TResult[]> SelectAsync<T, TResult>(this IEnumerable<T> source, Func<T, int, Task<TResult>> selector)
-    {
-        return Task.WhenAll(source.Select(selector));
-    }
-
-    /// <summary>
-    /// 异步Select
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <param name="maxParallelCount">最大并行数</param>
-    /// <returns></returns>
-    public static async Task<List<TResult>> SelectAsync<T, TResult>(this IEnumerable<T> source, Func<T, Task<TResult>> selector, int maxParallelCount)
-    {
-        var results = new List<TResult>();
-        var tasks = new List<Task<TResult>>();
-        foreach (var item in source)
-        {
-            var task = selector(item);
-            tasks.Add(task);
-            if (tasks.Count >= maxParallelCount)
-            {
-                await Task.WhenAny(tasks);
-                var completedTasks = tasks.Where(t => t.IsCompleted).ToArray();
-                results.AddRange(completedTasks.Select(t => t.Result));
-                tasks.RemoveWhere(t => completedTasks.Contains(t));
-            }
-        }
-
-        results.AddRange(await Task.WhenAll(tasks));
-        return results;
-    }
-
-    /// <summary>
-    /// 异步Select
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <param name="maxParallelCount">最大并行数</param>
-    /// <returns></returns>
-    public static async Task<List<TResult>> SelectAsync<T, TResult>(this IEnumerable<T> source, Func<T, int, Task<TResult>> selector, int maxParallelCount)
-    {
-        var results = new List<TResult>();
-        var tasks = new List<Task<TResult>>();
-        int index = 0;
-        foreach (var item in source)
-        {
-            var task = selector(item, index);
-            tasks.Add(task);
-            Interlocked.Add(ref index, 1);
-            if (tasks.Count >= maxParallelCount)
-            {
-                await Task.WhenAny(tasks);
-                var completedTasks = tasks.Where(t => t.IsCompleted).ToArray();
-                results.AddRange(completedTasks.Select(t => t.Result));
-                tasks.RemoveWhere(t => completedTasks.Contains(t));
-            }
-        }
-
-        results.AddRange(await Task.WhenAll(tasks));
-        return results;
-    }
-
-    /// <summary>
-    /// 异步For
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <param name="maxParallelCount">最大并行数</param>
-    /// <param name="cancellationToken">取消口令</param>
-    /// <returns></returns>
-    public static async Task ForAsync<T>(this IEnumerable<T> source, Func<T, int, Task> selector, int maxParallelCount, CancellationToken cancellationToken = default)
-    {
-        int index = 0;
-        if (Debugger.IsAttached)
-        {
-            foreach (var item in source)
-            {
-                await selector(item, index);
-                index++;
-            }
-
-            return;
-        }
-
-        var list = new List<Task>();
-        foreach (var item in source)
-        {
-            if (cancellationToken.IsCancellationRequested)
-            {
-                return;
-            }
-
-            list.Add(selector(item, index));
-            Interlocked.Add(ref index, 1);
-            if (list.Count >= maxParallelCount)
-            {
-                await Task.WhenAny(list);
-                list.RemoveAll(t => t.IsCompleted);
-            }
-        }
-
-        await Task.WhenAll(list);
-    }
-
-    /// <summary>
-    /// 异步For
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <param name="cancellationToken">取消口令</param>
-    /// <returns></returns>
-    public static Task ForAsync<T>(this IEnumerable<T> source, Func<T, int, Task> selector, CancellationToken cancellationToken = default)
-    {
-        if (source is ICollection<T> collection)
-        {
-            return ForAsync(collection, selector, collection.Count, cancellationToken);
-        }
-
-        var list = source.ToList();
-        return ForAsync(list, selector, list.Count, cancellationToken);
-    }
-
-    /// <summary>
-    /// 取最大值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <returns></returns>
-    public static TResult MaxOrDefault<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector) => source.Select(selector).DefaultIfEmpty().Max();
-
-    /// <summary>
-    /// 取最大值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <param name="defaultValue"></param>
-    /// <returns></returns>
-    public static TResult MaxOrDefault<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector, TResult defaultValue) => source.Select(selector).DefaultIfEmpty(defaultValue).Max();
-
-    /// <summary>
-    /// 取最大值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <param name="source"></param>
-    /// <returns></returns>
-    public static TSource MaxOrDefault<TSource>(this IQueryable<TSource> source) => source.DefaultIfEmpty().Max();
-
-    /// <summary>
-    /// 取最大值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="defaultValue"></param>
-    /// <returns></returns>
-    public static TSource MaxOrDefault<TSource>(this IQueryable<TSource> source, TSource defaultValue) => source.DefaultIfEmpty(defaultValue).Max();
-
-    /// <summary>
-    /// 取最大值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <param name="defaultValue"></param>
-    /// <returns></returns>
-    public static TResult MaxOrDefault<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector, TResult defaultValue) => source.Select(selector).DefaultIfEmpty(defaultValue).Max();
-
-    /// <summary>
-    /// 取最大值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <returns></returns>
-    public static TResult MaxOrDefault<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) => source.Select(selector).DefaultIfEmpty().Max();
-
-    /// <summary>
-    /// 取最大值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <param name="source"></param>
-    /// <returns></returns>
-    public static TSource MaxOrDefault<TSource>(this IEnumerable<TSource> source) => source.DefaultIfEmpty().Max();
-
-    /// <summary>
-    /// 取最大值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="defaultValue"></param>
-    /// <returns></returns>
-    public static TSource MaxOrDefault<TSource>(this IEnumerable<TSource> source, TSource defaultValue) => source.DefaultIfEmpty(defaultValue).Max();
-
-    /// <summary>
-    /// 取最小值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <returns></returns>
-    public static TResult MinOrDefault<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector) => source.Select(selector).DefaultIfEmpty().Min();
-
-    /// <summary>
-    /// 取最小值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <param name="defaultValue"></param>
-    /// <returns></returns>
-    public static TResult MinOrDefault<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector, TResult defaultValue) => source.Select(selector).DefaultIfEmpty(defaultValue).Min();
-
-    /// <summary>
-    /// 取最小值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <param name="source"></param>
-    /// <returns></returns>
-    public static TSource MinOrDefault<TSource>(this IQueryable<TSource> source) => source.DefaultIfEmpty().Min();
-
-    /// <summary>
-    /// 取最小值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="defaultValue"></param>
-    /// <returns></returns>
-    public static TSource MinOrDefault<TSource>(this IQueryable<TSource> source, TSource defaultValue) => source.DefaultIfEmpty(defaultValue).Min();
-
-    /// <summary>
-    /// 取最小值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <returns></returns>
-    public static TResult MinOrDefault<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) => source.Select(selector).DefaultIfEmpty().Min();
-
-    /// <summary>
-    /// 取最小值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <param name="defaultValue"></param>
-    /// <returns></returns>
-    public static TResult MinOrDefault<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector, TResult defaultValue) => source.Select(selector).DefaultIfEmpty(defaultValue).Min();
-
-    /// <summary>
-    /// 取最小值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <param name="source"></param>
-    /// <returns></returns>
-    public static TSource MinOrDefault<TSource>(this IEnumerable<TSource> source) => source.DefaultIfEmpty().Min();
-
-    /// <summary>
-    /// 取最小值
-    /// </summary>
-    /// <typeparam name="TSource"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="defaultValue"></param>
-    /// <returns></returns>
-    public static TSource MinOrDefault<TSource>(this IEnumerable<TSource> source, TSource defaultValue) => source.DefaultIfEmpty(defaultValue).Min();
-
-    /// <summary>
-    /// 标准差
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <typeparam name="TResult"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="selector"></param>
-    /// <returns></returns>
-    public static TResult StandardDeviation<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector) where TResult : IConvertible
-    {
-        return StandardDeviation(source.Select(t => selector(t).ConvertTo<double>())).ConvertTo<TResult>();
-    }
-
-    /// <summary>
-    /// 标准差
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="source"></param>
-    /// <returns></returns>
-    public static T StandardDeviation<T>(this IEnumerable<T> source) where T : IConvertible
-    {
-        return StandardDeviation(source.Select(t => t.ConvertTo<double>())).ConvertTo<T>();
-    }
-
-    /// <summary>
-    /// 标准差
-    /// </summary>
-    /// <param name="source"></param>
-    /// <returns></returns>
-    public static double StandardDeviation(this IEnumerable<double> source)
-    {
-        double result = 0;
-        var list = source as ICollection<double> ?? source.ToList();
-        int count = list.Count;
-        if (count > 1)
-        {
-            var avg = list.Average();
-            var sum = list.Sum(d => (d - avg) * (d - avg));
-            result = Math.Sqrt(sum / count);
-        }
-
-        return result;
-    }
-
-    /// <summary>
-    /// 随机排序
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="source"></param>
-    /// <returns></returns>
-    public static IOrderedEnumerable<T> OrderByRandom<T>(this IEnumerable<T> source)
-    {
-        return source.OrderBy(_ => Guid.NewGuid());
-    }
-
-    /// <summary>
-    /// 序列相等
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="first"></param>
-    /// <param name="second"></param>
-    /// <param name="condition"></param>
-    /// <returns></returns>
-    public static bool SequenceEqual<T>(this IEnumerable<T> first, IEnumerable<T> second, Func<T, T, bool> condition)
-    {
-        if (first is ICollection<T> source1 && second is ICollection<T> source2)
-        {
-            if (source1.Count != source2.Count)
-            {
-                return false;
-            }
-
-            if (source1 is IList<T> list1 && source2 is IList<T> list2)
-            {
-                int count = source1.Count;
-                for (int index = 0; index < count; ++index)
-                {
-                    if (!condition(list1[index], list2[index]))
-                    {
-                        return false;
-                    }
-                }
-
-                return true;
-            }
-        }
-
-        using IEnumerator<T> enumerator1 = first.GetEnumerator();
-        using IEnumerator<T> enumerator2 = second.GetEnumerator();
-        while (enumerator1.MoveNext())
-        {
-            if (!enumerator2.MoveNext() || !condition(enumerator1.Current, enumerator2.Current))
-            {
-                return false;
-            }
-        }
-
-        return !enumerator2.MoveNext();
-    }
-
-    /// <summary>
-    /// 序列相等
-    /// </summary>
-    /// <typeparam name="T1"></typeparam>
-    /// <typeparam name="T2"></typeparam>
-    /// <param name="first"></param>
-    /// <param name="second"></param>
-    /// <param name="condition"></param>
-    /// <returns></returns>
-    public static bool SequenceEqual<T1, T2>(this IEnumerable<T1> first, IEnumerable<T2> second, Func<T1, T2, bool> condition)
-    {
-        if (first is ICollection<T1> source1 && second is ICollection<T2> source2)
-        {
-            if (source1.Count != source2.Count)
-            {
-                return false;
-            }
-
-            if (source1 is IList<T1> list1 && source2 is IList<T2> list2)
-            {
-                int count = source1.Count;
-                for (int index = 0; index < count; ++index)
-                {
-                    if (!condition(list1[index], list2[index]))
-                    {
-                        return false;
-                    }
-                }
-
-                return true;
-            }
-        }
-
-        using IEnumerator<T1> enumerator1 = first.GetEnumerator();
-        using IEnumerator<T2> enumerator2 = second.GetEnumerator();
-        while (enumerator1.MoveNext())
-        {
-            if (!enumerator2.MoveNext() || !condition(enumerator1.Current, enumerator2.Current))
-            {
-                return false;
-            }
-        }
-
-        return !enumerator2.MoveNext();
-    }
-
-    /// <summary>
-    /// 对比两个集合哪些是新增的、删除的、修改的
-    /// </summary>
-    /// <typeparam name="T1"></typeparam>
-    /// <typeparam name="T2"></typeparam>
-    /// <param name="first"></param>
-    /// <param name="second"></param>
-    /// <param name="condition">对比因素条件</param>
-    /// <returns></returns>
-    public static (List<T1> adds, List<T2> remove, List<T1> updates) CompareChanges<T1, T2>(this IEnumerable<T1> first, IEnumerable<T2> second, Func<T1, T2, bool> condition)
-    {
-        first ??= new List<T1>();
-        second ??= new List<T2>();
-        var firstSource = first as ICollection<T1> ?? first.ToList();
-        var secondSource = second as ICollection<T2> ?? second.ToList();
-        var add = firstSource.ExceptBy(secondSource, condition).ToList();
-        var remove = secondSource.ExceptBy(firstSource, (s, f) => condition(f, s)).ToList();
-        var update = firstSource.IntersectBy(secondSource, condition).ToList();
-        return (add, remove, update);
-    }
-
-    /// <summary>
-    /// 对比两个集合哪些是新增的、删除的、修改的
-    /// </summary>
-    /// <typeparam name="T1"></typeparam>
-    /// <typeparam name="T2"></typeparam>
-    /// <param name="first"></param>
-    /// <param name="second"></param>
-    /// <param name="condition">对比因素条件</param>
-    /// <returns></returns>
-    public static (List<T1> adds, List<T2> remove, List<(T1 first, T2 second)> updates) CompareChangesPlus<T1, T2>(this IEnumerable<T1> first, IEnumerable<T2> second, Func<T1, T2, bool> condition)
-    {
-        first ??= new List<T1>();
-        second ??= new List<T2>();
-        var firstSource = first as ICollection<T1> ?? first.ToList();
-        var secondSource = second as ICollection<T2> ?? second.ToList();
-        var add = firstSource.ExceptBy(secondSource, condition).ToList();
-        var remove = secondSource.ExceptBy(firstSource, (s, f) => condition(f, s)).ToList();
-        var updates = firstSource.IntersectBy(secondSource, condition).Select(t1 => (t1, secondSource.FirstOrDefault(t2 => condition(t1, t2)))).ToList();
-        return (add, remove, updates);
-    }
-
-    /// <summary>
-    /// 将集合声明为非null集合
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="list"></param>
-    /// <returns></returns>
-    public static List<T> AsNotNull<T>(this List<T> list)
-    {
-        return list ?? new List<T>();
-    }
-
-    /// <summary>
-    /// 将集合声明为非null集合
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="list"></param>
-    /// <returns></returns>
-    public static IEnumerable<T> AsNotNull<T>(this IEnumerable<T> list)
-    {
-        return list ?? new List<T>();
-    }
-
-    /// <summary>
-    /// 满足条件时执行筛选条件
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="condition"></param>
-    /// <param name="where"></param>
-    /// <returns></returns>
-    public static IEnumerable<T> WhereIf<T>(this IEnumerable<T> source, bool condition, Func<T, bool> where)
-    {
-        return condition ? source.Where(where) : source;
-    }
-
-    /// <summary>
-    /// 满足条件时执行筛选条件
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="condition"></param>
-    /// <param name="where"></param>
-    /// <returns></returns>
-    public static IEnumerable<T> WhereIf<T>(this IEnumerable<T> source, Func<bool> condition, Func<T, bool> where)
-    {
-        return condition() ? source.Where(where) : source;
-    }
-
-    /// <summary>
-    /// 满足条件时执行筛选条件
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="condition"></param>
-    /// <param name="where"></param>
-    /// <returns></returns>
-    public static IQueryable<T> WhereIf<T>(this IQueryable<T> source, bool condition, Expression<Func<T, bool>> where)
-    {
-        return condition ? source.Where(where) : source;
-    }
-
-    /// <summary>
-    /// 满足条件时执行筛选条件
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="source"></param>
-    /// <param name="condition"></param>
-    /// <param name="where"></param>
-    /// <returns></returns>
-    public static IQueryable<T> WhereIf<T>(this IQueryable<T> source, Func<bool> condition, Expression<Func<T, bool>> where)
-    {
-        return condition() ? source.Where(where) : source;
-    }
-
-    /// <summary>
-    /// 改变元素的索引位置
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="list">集合</param>
-    /// <param name="item">元素</param>
-    /// <param name="index">索引值</param>
-    /// <exception cref="ArgumentNullException"></exception>
-    public static IList<T> ChangeIndex<T>(this IList<T> list, T item, int index)
-    {
-        if (item is null)
-        {
-            throw new ArgumentNullException(nameof(item));
-        }
-
-        ChangeIndexInternal(list, item, index);
-        return list;
-    }
-
-    /// <summary>
-    /// 改变元素的索引位置
-    /// </summary>
-    /// <typeparam name="T"></typeparam>
-    /// <param name="list">集合</param>
-    /// <param name="condition">元素定位条件</param>
-    /// <param name="index">索引值</param>
-    public static IList<T> ChangeIndex<T>(this IList<T> list, Func<T, bool> condition, int index)
-    {
-        var item = list.FirstOrDefault(condition);
-        if (item != null)
-        {
-            ChangeIndexInternal(list, item, index);
-        }
-        return list;
-    }
-
-    private static void ChangeIndexInternal<T>(IList<T> list, T item, int index)
-    {
-        index = Math.Max(0, index);
-        index = Math.Min(list.Count - 1, index);
-        list.Remove(item);
-        list.Insert(index, item);
-    }
-}
+	/// <summary>
+	/// 添加多个元素
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="this"></param>
+	/// <param name="values"></param>
+	public static void AddRange<T>(this ICollection<T> @this, params T[] values)
+	{
+		foreach (var obj in values)
+		{
+			@this.Add(obj);
+		}
+	}
+
+	/// <summary>
+	/// 添加多个元素
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="this"></param>
+	/// <param name="values"></param>
+	public static void AddRange<T>(this ICollection<T> @this, IEnumerable<T> values)
+	{
+		foreach (var obj in values)
+		{
+			@this.Add(obj);
+		}
+	}
+
+	/// <summary>
+	/// 添加多个元素
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="this"></param>
+	/// <param name="values"></param>
+	public static void AddRange<T>(this ConcurrentBag<T> @this, params T[] values)
+	{
+		foreach (var obj in values)
+		{
+			@this.Add(obj);
+		}
+	}
+
+	/// <summary>
+	/// 添加多个元素
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="this"></param>
+	/// <param name="values"></param>
+	public static void AddRange<T>(this ConcurrentQueue<T> @this, params T[] values)
+	{
+		foreach (var obj in values)
+		{
+			@this.Enqueue(obj);
+		}
+	}
+
+	/// <summary>
+	/// 添加符合条件的多个元素
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="this"></param>
+	/// <param name="predicate"></param>
+	/// <param name="values"></param>
+	public static void AddRangeIf<T>(this ICollection<T> @this, Func<T, bool> predicate, params T[] values)
+	{
+		foreach (var obj in values.Where(predicate))
+		{
+			@this.Add(obj);
+		}
+	}
+
+	/// <summary>
+	/// 添加符合条件的多个元素
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="this"></param>
+	/// <param name="predicate"></param>
+	/// <param name="values"></param>
+	public static void AddRangeIf<T>(this ConcurrentBag<T> @this, Func<T, bool> predicate, params T[] values)
+	{
+		foreach (var obj in values.Where(predicate))
+		{
+			@this.Add(obj);
+		}
+	}
+
+	/// <summary>
+	/// 添加符合条件的多个元素
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="this"></param>
+	/// <param name="predicate"></param>
+	/// <param name="values"></param>
+	public static void AddRangeIf<T>(this ConcurrentQueue<T> @this, Func<T, bool> predicate, params T[] values)
+	{
+		foreach (var obj in values.Where(predicate))
+		{
+			@this.Enqueue(obj);
+		}
+	}
+
+	/// <summary>
+	/// 添加不重复的元素
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="this"></param>
+	/// <param name="values"></param>
+	public static void AddRangeIfNotContains<T>(this ICollection<T> @this, params T[] values)
+	{
+		foreach (T obj in values)
+		{
+			if ([email protected](obj))
+			{
+				@this.Add(obj);
+			}
+		}
+	}
+
+	/// <summary>
+	/// 移除符合条件的元素
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="this"></param>
+	/// <param name="where"></param>
+	public static void RemoveWhere<T>(this ICollection<T> @this, Func<T, bool> @where)
+	{
+		foreach (var obj in @this.Where(where).ToList())
+		{
+			@this.Remove(obj);
+		}
+	}
+
+	/// <summary>
+	/// 在元素之后添加元素
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="list"></param>
+	/// <param name="condition">条件</param>
+	/// <param name="value">值</param>
+	public static void InsertAfter<T>(this IList<T> list, Func<T, bool> condition, T value)
+	{
+		foreach (var index in list.Select((item, index) => new
+		{
+			item,
+			index
+		}).Where(p => condition(p.item)).OrderByDescending(p => p.index).Select(t => t.index))
+		{
+			if (index + 1 == list.Count)
+			{
+				list.Add(value);
+			}
+			else
+			{
+				list.Insert(index + 1, value);
+			}
+		}
+	}
+
+	/// <summary>
+	/// 在元素之后添加元素
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="list"></param>
+	/// <param name="index">索引位置</param>
+	/// <param name="value">值</param>
+	public static void InsertAfter<T>(this IList<T> list, int index, T value)
+	{
+		foreach (var i in list.Select((v, i) => new
+		{
+			Value = v,
+			Index = i
+		}).Where(p => p.Index == index).OrderByDescending(p => p.Index).Select(t => t.Index))
+		{
+			if (i + 1 == list.Count)
+			{
+				list.Add(value);
+			}
+			else
+			{
+				list.Insert(i + 1, value);
+			}
+		}
+	}
+
+	/// <summary>
+	/// 转HashSet
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <returns></returns>
+	public static HashSet<TResult> ToHashSet<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector)
+	{
+		return new HashSet<TResult>(source.Select(selector));
+	}
+
+	/// <summary>
+	/// 转Queue
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="source"></param>
+	/// <returns></returns>
+	public static Queue<T> ToQueue<T>(this IEnumerable<T> source)
+	{
+		return new Queue<T>(source);
+	}
+
+	/// <summary>
+	/// 转Queue
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <returns></returns>
+	public static Queue<TResult> ToQueue<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector)
+	{
+		return new Queue<TResult>(source.Select(selector));
+	}
+
+	/// <summary>
+	/// 遍历IEnumerable
+	/// </summary>
+	/// <param name="objs"></param>
+	/// <param name="action">回调方法</param>
+	/// <typeparam name="T"></typeparam>
+	public static void ForEach<T>(this IEnumerable<T> objs, Action<T> action)
+	{
+		foreach (var o in objs)
+		{
+			action(o);
+		}
+	}
+
+	/// <summary>
+	/// 异步foreach
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="maxParallelCount">最大并行数</param>
+	/// <param name="action"></param>
+	/// <param name="cancellationToken"></param>
+	/// <returns></returns>
+	public static async Task ForeachAsync<T>(this IEnumerable<T> source, Func<T, Task> action, int maxParallelCount, CancellationToken cancellationToken = default)
+	{
+		if (Debugger.IsAttached)
+		{
+			foreach (var item in source)
+			{
+				await action(item);
+			}
+
+			return;
+		}
+
+		var list = new List<Task>();
+		foreach (var item in source)
+		{
+			if (cancellationToken.IsCancellationRequested)
+			{
+				return;
+			}
+
+			list.Add(action(item));
+			if (list.Count(t => !t.IsCompleted) >= maxParallelCount)
+			{
+				await Task.WhenAny(list);
+				list.RemoveAll(t => t.IsCompleted);
+			}
+		}
+
+		await Task.WhenAll(list);
+	}
+
+	/// <summary>
+	/// 异步foreach
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="action"></param>
+	/// <param name="cancellationToken"></param>
+	/// <returns></returns>
+	public static Task ForeachAsync<T>(this IEnumerable<T> source, Func<T, Task> action, CancellationToken cancellationToken = default)
+	{
+		if (source is ICollection<T> collection)
+		{
+			return ForeachAsync(collection, action, collection.Count, cancellationToken);
+		}
+
+		var list = source.ToList();
+		return ForeachAsync(list, action, list.Count, cancellationToken);
+	}
+
+	/// <summary>
+	/// 异步Select
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <returns></returns>
+	public static Task<TResult[]> SelectAsync<T, TResult>(this IEnumerable<T> source, Func<T, Task<TResult>> selector)
+	{
+		return Task.WhenAll(source.Select(selector));
+	}
+
+	/// <summary>
+	/// 异步Select
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <returns></returns>
+	public static Task<TResult[]> SelectAsync<T, TResult>(this IEnumerable<T> source, Func<T, int, Task<TResult>> selector)
+	{
+		return Task.WhenAll(source.Select(selector));
+	}
+
+	/// <summary>
+	/// 异步Select
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <param name="maxParallelCount">最大并行数</param>
+	/// <returns></returns>
+	public static async Task<List<TResult>> SelectAsync<T, TResult>(this IEnumerable<T> source, Func<T, Task<TResult>> selector, int maxParallelCount)
+	{
+		var results = new List<TResult>();
+		var tasks = new List<Task<TResult>>();
+		foreach (var item in source)
+		{
+			var task = selector(item);
+			tasks.Add(task);
+			if (tasks.Count >= maxParallelCount)
+			{
+				await Task.WhenAny(tasks);
+				var completedTasks = tasks.Where(t => t.IsCompleted).ToArray();
+				results.AddRange(completedTasks.Select(t => t.Result));
+				tasks.RemoveWhere(t => completedTasks.Contains(t));
+			}
+		}
+
+		results.AddRange(await Task.WhenAll(tasks));
+		return results;
+	}
+
+	/// <summary>
+	/// 异步Select
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <param name="maxParallelCount">最大并行数</param>
+	/// <returns></returns>
+	public static async Task<List<TResult>> SelectAsync<T, TResult>(this IEnumerable<T> source, Func<T, int, Task<TResult>> selector, int maxParallelCount)
+	{
+		var results = new List<TResult>();
+		var tasks = new List<Task<TResult>>();
+		int index = 0;
+		foreach (var item in source)
+		{
+			var task = selector(item, index);
+			tasks.Add(task);
+			Interlocked.Add(ref index, 1);
+			if (tasks.Count >= maxParallelCount)
+			{
+				await Task.WhenAny(tasks);
+				var completedTasks = tasks.Where(t => t.IsCompleted).ToArray();
+				results.AddRange(completedTasks.Select(t => t.Result));
+				tasks.RemoveWhere(t => completedTasks.Contains(t));
+			}
+		}
+
+		results.AddRange(await Task.WhenAll(tasks));
+		return results;
+	}
+
+	/// <summary>
+	/// 异步For
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <param name="maxParallelCount">最大并行数</param>
+	/// <param name="cancellationToken">取消口令</param>
+	/// <returns></returns>
+	public static async Task ForAsync<T>(this IEnumerable<T> source, Func<T, int, Task> selector, int maxParallelCount, CancellationToken cancellationToken = default)
+	{
+		int index = 0;
+		if (Debugger.IsAttached)
+		{
+			foreach (var item in source)
+			{
+				await selector(item, index);
+				index++;
+			}
+
+			return;
+		}
+
+		var list = new List<Task>();
+		foreach (var item in source)
+		{
+			if (cancellationToken.IsCancellationRequested)
+			{
+				return;
+			}
+
+			list.Add(selector(item, index));
+			Interlocked.Add(ref index, 1);
+			if (list.Count >= maxParallelCount)
+			{
+				await Task.WhenAny(list);
+				list.RemoveAll(t => t.IsCompleted);
+			}
+		}
+
+		await Task.WhenAll(list);
+	}
+
+	/// <summary>
+	/// 异步For
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <param name="cancellationToken">取消口令</param>
+	/// <returns></returns>
+	public static Task ForAsync<T>(this IEnumerable<T> source, Func<T, int, Task> selector, CancellationToken cancellationToken = default)
+	{
+		if (source is ICollection<T> collection)
+		{
+			return ForAsync(collection, selector, collection.Count, cancellationToken);
+		}
+
+		var list = source.ToList();
+		return ForAsync(list, selector, list.Count, cancellationToken);
+	}
+
+	/// <summary>
+	/// 取最大值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <returns></returns>
+	public static TResult MaxOrDefault<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector) => source.Select(selector).DefaultIfEmpty().Max();
+
+	/// <summary>
+	/// 取最大值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <param name="defaultValue"></param>
+	/// <returns></returns>
+	public static TResult MaxOrDefault<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector, TResult defaultValue) => source.Select(selector).DefaultIfEmpty(defaultValue).Max();
+
+	/// <summary>
+	/// 取最大值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <param name="source"></param>
+	/// <returns></returns>
+	public static TSource MaxOrDefault<TSource>(this IQueryable<TSource> source) => source.DefaultIfEmpty().Max();
+
+	/// <summary>
+	/// 取最大值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="defaultValue"></param>
+	/// <returns></returns>
+	public static TSource MaxOrDefault<TSource>(this IQueryable<TSource> source, TSource defaultValue) => source.DefaultIfEmpty(defaultValue).Max();
+
+	/// <summary>
+	/// 取最大值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <param name="defaultValue"></param>
+	/// <returns></returns>
+	public static TResult MaxOrDefault<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector, TResult defaultValue) => source.Select(selector).DefaultIfEmpty(defaultValue).Max();
+
+	/// <summary>
+	/// 取最大值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <returns></returns>
+	public static TResult MaxOrDefault<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) => source.Select(selector).DefaultIfEmpty().Max();
+
+	/// <summary>
+	/// 取最大值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <param name="source"></param>
+	/// <returns></returns>
+	public static TSource MaxOrDefault<TSource>(this IEnumerable<TSource> source) => source.DefaultIfEmpty().Max();
+
+	/// <summary>
+	/// 取最大值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="defaultValue"></param>
+	/// <returns></returns>
+	public static TSource MaxOrDefault<TSource>(this IEnumerable<TSource> source, TSource defaultValue) => source.DefaultIfEmpty(defaultValue).Max();
+
+	/// <summary>
+	/// 取最小值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <returns></returns>
+	public static TResult MinOrDefault<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector) => source.Select(selector).DefaultIfEmpty().Min();
+
+	/// <summary>
+	/// 取最小值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <param name="defaultValue"></param>
+	/// <returns></returns>
+	public static TResult MinOrDefault<TSource, TResult>(this IQueryable<TSource> source, Expression<Func<TSource, TResult>> selector, TResult defaultValue) => source.Select(selector).DefaultIfEmpty(defaultValue).Min();
+
+	/// <summary>
+	/// 取最小值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <param name="source"></param>
+	/// <returns></returns>
+	public static TSource MinOrDefault<TSource>(this IQueryable<TSource> source) => source.DefaultIfEmpty().Min();
+
+	/// <summary>
+	/// 取最小值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="defaultValue"></param>
+	/// <returns></returns>
+	public static TSource MinOrDefault<TSource>(this IQueryable<TSource> source, TSource defaultValue) => source.DefaultIfEmpty(defaultValue).Min();
+
+	/// <summary>
+	/// 取最小值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <returns></returns>
+	public static TResult MinOrDefault<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector) => source.Select(selector).DefaultIfEmpty().Min();
+
+	/// <summary>
+	/// 取最小值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <param name="defaultValue"></param>
+	/// <returns></returns>
+	public static TResult MinOrDefault<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector, TResult defaultValue) => source.Select(selector).DefaultIfEmpty(defaultValue).Min();
+
+	/// <summary>
+	/// 取最小值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <param name="source"></param>
+	/// <returns></returns>
+	public static TSource MinOrDefault<TSource>(this IEnumerable<TSource> source) => source.DefaultIfEmpty().Min();
+
+	/// <summary>
+	/// 取最小值
+	/// </summary>
+	/// <typeparam name="TSource"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="defaultValue"></param>
+	/// <returns></returns>
+	public static TSource MinOrDefault<TSource>(this IEnumerable<TSource> source, TSource defaultValue) => source.DefaultIfEmpty(defaultValue).Min();
+
+	/// <summary>
+	/// 标准差
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <typeparam name="TResult"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="selector"></param>
+	/// <returns></returns>
+	public static TResult StandardDeviation<T, TResult>(this IEnumerable<T> source, Func<T, TResult> selector) where TResult : IConvertible
+	{
+		return StandardDeviation(source.Select(t => selector(t).ConvertTo<double>())).ConvertTo<TResult>();
+	}
+
+	/// <summary>
+	/// 标准差
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="source"></param>
+	/// <returns></returns>
+	public static T StandardDeviation<T>(this IEnumerable<T> source) where T : IConvertible
+	{
+		return StandardDeviation(source.Select(t => t.ConvertTo<double>())).ConvertTo<T>();
+	}
+
+	/// <summary>
+	/// 标准差
+	/// </summary>
+	/// <param name="source"></param>
+	/// <returns></returns>
+	public static double StandardDeviation(this IEnumerable<double> source)
+	{
+		double result = 0;
+		var list = source as ICollection<double> ?? source.ToList();
+		int count = list.Count;
+		if (count > 1)
+		{
+			var avg = list.Average();
+			var sum = list.Sum(d => (d - avg) * (d - avg));
+			result = Math.Sqrt(sum / count);
+		}
+
+		return result;
+	}
+
+	/// <summary>
+	/// 随机排序
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="source"></param>
+	/// <returns></returns>
+	public static IOrderedEnumerable<T> OrderByRandom<T>(this IEnumerable<T> source)
+	{
+		return source.OrderBy(_ => Guid.NewGuid());
+	}
+
+	/// <summary>
+	/// 序列相等
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="first"></param>
+	/// <param name="second"></param>
+	/// <param name="condition"></param>
+	/// <returns></returns>
+	public static bool SequenceEqual<T>(this IEnumerable<T> first, IEnumerable<T> second, Func<T, T, bool> condition)
+	{
+		if (first is ICollection<T> source1 && second is ICollection<T> source2)
+		{
+			if (source1.Count != source2.Count)
+			{
+				return false;
+			}
+
+			if (source1 is IList<T> list1 && source2 is IList<T> list2)
+			{
+				int count = source1.Count;
+				for (int index = 0; index < count; ++index)
+				{
+					if (!condition(list1[index], list2[index]))
+					{
+						return false;
+					}
+				}
+
+				return true;
+			}
+		}
+
+		using IEnumerator<T> enumerator1 = first.GetEnumerator();
+		using IEnumerator<T> enumerator2 = second.GetEnumerator();
+		while (enumerator1.MoveNext())
+		{
+			if (!enumerator2.MoveNext() || !condition(enumerator1.Current, enumerator2.Current))
+			{
+				return false;
+			}
+		}
+
+		return !enumerator2.MoveNext();
+	}
+
+	/// <summary>
+	/// 序列相等
+	/// </summary>
+	/// <typeparam name="T1"></typeparam>
+	/// <typeparam name="T2"></typeparam>
+	/// <param name="first"></param>
+	/// <param name="second"></param>
+	/// <param name="condition"></param>
+	/// <returns></returns>
+	public static bool SequenceEqual<T1, T2>(this IEnumerable<T1> first, IEnumerable<T2> second, Func<T1, T2, bool> condition)
+	{
+		if (first is ICollection<T1> source1 && second is ICollection<T2> source2)
+		{
+			if (source1.Count != source2.Count)
+			{
+				return false;
+			}
+
+			if (source1 is IList<T1> list1 && source2 is IList<T2> list2)
+			{
+				int count = source1.Count;
+				for (int index = 0; index < count; ++index)
+				{
+					if (!condition(list1[index], list2[index]))
+					{
+						return false;
+					}
+				}
+
+				return true;
+			}
+		}
+
+		using IEnumerator<T1> enumerator1 = first.GetEnumerator();
+		using IEnumerator<T2> enumerator2 = second.GetEnumerator();
+		while (enumerator1.MoveNext())
+		{
+			if (!enumerator2.MoveNext() || !condition(enumerator1.Current, enumerator2.Current))
+			{
+				return false;
+			}
+		}
+
+		return !enumerator2.MoveNext();
+	}
+
+	/// <summary>
+	/// 对比两个集合哪些是新增的、删除的、修改的
+	/// </summary>
+	/// <typeparam name="T1"></typeparam>
+	/// <typeparam name="T2"></typeparam>
+	/// <param name="first"></param>
+	/// <param name="second"></param>
+	/// <param name="condition">对比因素条件</param>
+	/// <returns></returns>
+	public static (List<T1> adds, List<T2> remove, List<T1> updates) CompareChanges<T1, T2>(this IEnumerable<T1> first, IEnumerable<T2> second, Func<T1, T2, bool> condition)
+	{
+		first ??= new List<T1>();
+		second ??= new List<T2>();
+		var firstSource = first as ICollection<T1> ?? first.ToList();
+		var secondSource = second as ICollection<T2> ?? second.ToList();
+		var add = firstSource.ExceptBy(secondSource, condition).ToList();
+		var remove = secondSource.ExceptBy(firstSource, (s, f) => condition(f, s)).ToList();
+		var update = firstSource.IntersectBy(secondSource, condition).ToList();
+		return (add, remove, update);
+	}
+
+	/// <summary>
+	/// 对比两个集合哪些是新增的、删除的、修改的
+	/// </summary>
+	/// <typeparam name="T1"></typeparam>
+	/// <typeparam name="T2"></typeparam>
+	/// <param name="first"></param>
+	/// <param name="second"></param>
+	/// <param name="condition">对比因素条件</param>
+	/// <returns></returns>
+	public static (List<T1> adds, List<T2> remove, List<(T1 first, T2 second)> updates) CompareChangesPlus<T1, T2>(this IEnumerable<T1> first, IEnumerable<T2> second, Func<T1, T2, bool> condition)
+	{
+		first ??= new List<T1>();
+		second ??= new List<T2>();
+		var firstSource = first as ICollection<T1> ?? first.ToList();
+		var secondSource = second as ICollection<T2> ?? second.ToList();
+		var add = firstSource.ExceptBy(secondSource, condition).ToList();
+		var remove = secondSource.ExceptBy(firstSource, (s, f) => condition(f, s)).ToList();
+		var updates = firstSource.IntersectBy(secondSource, condition).Select(t1 => (t1, secondSource.FirstOrDefault(t2 => condition(t1, t2)))).ToList();
+		return (add, remove, updates);
+	}
+
+	/// <summary>
+	/// 将集合声明为非null集合
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="list"></param>
+	/// <returns></returns>
+	public static List<T> AsNotNull<T>(this List<T> list)
+	{
+		return list ?? new List<T>();
+	}
+
+	/// <summary>
+	/// 将集合声明为非null集合
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="list"></param>
+	/// <returns></returns>
+	public static IEnumerable<T> AsNotNull<T>(this IEnumerable<T> list)
+	{
+		return list ?? new List<T>();
+	}
+
+	/// <summary>
+	/// 满足条件时执行筛选条件
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="condition"></param>
+	/// <param name="where"></param>
+	/// <returns></returns>
+	public static IEnumerable<T> WhereIf<T>(this IEnumerable<T> source, bool condition, Func<T, bool> where)
+	{
+		return condition ? source.Where(where) : source;
+	}
+
+	/// <summary>
+	/// 满足条件时执行筛选条件
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="condition"></param>
+	/// <param name="where"></param>
+	/// <returns></returns>
+	public static IEnumerable<T> WhereIf<T>(this IEnumerable<T> source, Func<bool> condition, Func<T, bool> where)
+	{
+		return condition() ? source.Where(where) : source;
+	}
+
+	/// <summary>
+	/// 满足条件时执行筛选条件
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="condition"></param>
+	/// <param name="where"></param>
+	/// <returns></returns>
+	public static IQueryable<T> WhereIf<T>(this IQueryable<T> source, bool condition, Expression<Func<T, bool>> where)
+	{
+		return condition ? source.Where(where) : source;
+	}
+
+	/// <summary>
+	/// 满足条件时执行筛选条件
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="source"></param>
+	/// <param name="condition"></param>
+	/// <param name="where"></param>
+	/// <returns></returns>
+	public static IQueryable<T> WhereIf<T>(this IQueryable<T> source, Func<bool> condition, Expression<Func<T, bool>> where)
+	{
+		return condition() ? source.Where(where) : source;
+	}
+
+	/// <summary>
+	/// 改变元素的索引位置
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="list">集合</param>
+	/// <param name="item">元素</param>
+	/// <param name="index">索引值</param>
+	/// <exception cref="ArgumentNullException"></exception>
+	public static IList<T> ChangeIndex<T>(this IList<T> list, T item, int index)
+	{
+		if (item is null)
+		{
+			throw new ArgumentNullException(nameof(item));
+		}
+
+		ChangeIndexInternal(list, item, index);
+		return list;
+	}
+
+	/// <summary>
+	/// 改变元素的索引位置
+	/// </summary>
+	/// <typeparam name="T"></typeparam>
+	/// <param name="list">集合</param>
+	/// <param name="condition">元素定位条件</param>
+	/// <param name="index">索引值</param>
+	public static IList<T> ChangeIndex<T>(this IList<T> list, Func<T, bool> condition, int index)
+	{
+		var item = list.FirstOrDefault(condition);
+		if (item != null)
+		{
+			ChangeIndexInternal(list, item, index);
+		}
+		return list;
+	}
+
+	private static void ChangeIndexInternal<T>(IList<T> list, T item, int index)
+	{
+		index = Math.Max(0, index);
+		index = Math.Min(list.Count - 1, index);
+		list.Remove(item);
+		list.Insert(index, item);
+	}
+}

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 734 - 734
Masuit.Tools.Abstractions/Extensions/BaseType/StringExtensions.cs


+ 21 - 1
Masuit.Tools.Abstractions/HtmlSanitizer/HtmlSanitizer.cs

@@ -99,6 +99,8 @@ namespace Ganss.Xss
             AllowedClasses = new HashSet<string>(options.AllowedCssClasses, StringComparer.OrdinalIgnoreCase);
             AllowedCssProperties = new HashSet<string>(options.AllowedCssProperties, StringComparer.OrdinalIgnoreCase);
             AllowedAtRules = new HashSet<CssRuleType>(options.AllowedAtRules);
+            AllowCssCustomProperties = options.AllowCssCustomProperties;
+            AllowDataAttributes = options.AllowDataAttributes;
         }
 
         /// <summary>
@@ -204,6 +206,11 @@ namespace Ganss.Xss
         /// </value>
         public ISet<string> AllowedCssProperties { get; private set; }
 
+        /// <summary>
+        /// Allow all custom CSS properties (variables) prefixed with <c>--</c>.
+        /// </summary>
+        public bool AllowCssCustomProperties { get; set; }
+
         /// <summary>
         /// Gets or sets a regex that must not match for legal CSS property values.
         /// </summary>
@@ -726,6 +733,19 @@ namespace Ganss.Xss
             SanitizeStyleDeclaration(element, styles, baseUrl);
         }
 
+        /// <summary>
+        /// Verify if the given CSS property name is allowed. By default this will
+        /// check if the property is in the <see cref="AllowedCssProperties"/> set,
+        /// or if the property is a custom property and <see cref="AllowCssCustomProperties"/> is true.
+        /// </summary>
+        /// <param name="propertyName">The name of the CSS property.</param>
+        /// <returns>True if the property is allowed or not.</returns>
+        protected virtual bool IsAllowedCssProperty(string propertyName)
+        {
+            return AllowedCssProperties.Contains(propertyName)
+                || AllowCssCustomProperties && propertyName != null && propertyName.StartsWith("--");
+        }
+
         private void SanitizeStyleDeclaration(IElement element, ICssStyleDeclaration styles, string baseUrl)
         {
             var removeStyles = new List<Tuple<ICssProperty, RemoveReason>>();
@@ -736,7 +756,7 @@ namespace Ganss.Xss
                 var key = DecodeCss(style.Name);
                 var val = DecodeCss(style.Value);
 
-                if (!AllowedCssProperties.Contains(key))
+                if (!IsAllowedCssProperty(key))
                 {
                     removeStyles.Add(new Tuple<ICssProperty, RemoveReason>(style, RemoveReason.NotAllowedStyle));
                     continue;

+ 10 - 0
Masuit.Tools.Abstractions/HtmlSanitizer/HtmlSanitizerOptions.cs

@@ -43,5 +43,15 @@ namespace Ganss.Xss
         /// Gets or sets the HTML attributes that can contain a URI such as "href".
         /// </summary>
         public ISet<string> UriAttributes { get; set; } = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
+
+        /// <summary>
+        /// Allow all custom CSS properties (variables) prefixed with <c>--</c>.
+        /// </summary>
+        public bool AllowCssCustomProperties { get; set; }
+
+        /// <summary>
+        /// Allow all HTML5 data attributes; the attributes prefixed with <c>data-</c>.
+        /// </summary>
+        public bool AllowDataAttributes { get; set; }
     }
 }

+ 145 - 0
Masuit.Tools.AspNetCore/TextDiff/BitapAlgorithm.cs

@@ -0,0 +1,145 @@
+namespace Masuit.Tools.TextDiff;
+
+/*
+ * https://en.wikipedia.org/wiki/Bitap_algorithm
+ */
+
+/// <summary>
+/// 实现Bitap算法,允许近似字符串匹配的文本搜索算法。提供了在文本字符串中定位给定模式的最佳实例的功能,考虑了潜在的不匹配和错误。该算法通过MatchSettings配置,其中包括匹配阈值和距离,确定搜索的灵敏度和灵活性。
+/// </summary>
+internal class BitapAlgorithm(MatchOption option)
+{
+	// 匹配阈值 (0.0 = 最完美, 1.0 = 非常宽松).
+	private readonly float _matchThreshold = option.MatchThreshold;
+
+	// 搜索匹配的距离(0=精确位置,1000以上=广泛匹配)。与预期位置相差多少字符的匹配将分数增加1.0(0.0是完美匹配)。
+	private readonly int _matchDistance = option.MatchDistance;
+
+	/// <summary>
+	/// 使用Bitap算法在“loc”附近的“text”中找到“pattern”的最佳实例。如果未找到匹配项,则返回-1。
+	/// </summary>
+	/// <param name="text">需要搜索的文本</param>
+	/// <param name="pattern">被搜索片段</param>
+	/// <param name="startIndex">搜索位置</param>
+	/// <returns>最匹配的位置或 -1.</returns>
+	public int Match(string text, string pattern, int startIndex)
+	{
+		double scoreThreshold = _matchThreshold;
+
+		var bestMatchIndex = text.IndexOf(pattern, startIndex, StringComparison.Ordinal);
+		if (bestMatchIndex != -1)
+		{
+			scoreThreshold = Math.Min(MatchBitapScore(0, bestMatchIndex, startIndex, pattern), scoreThreshold);
+			bestMatchIndex = text.LastIndexOf(pattern, Math.Min(startIndex + pattern.Length, text.Length), StringComparison.Ordinal);
+			if (bestMatchIndex != -1)
+			{
+				scoreThreshold = Math.Min(MatchBitapScore(0, bestMatchIndex, startIndex, pattern), scoreThreshold);
+			}
+		}
+
+		var s = InitAlphabet(pattern);
+		var matchmask = 1 << (pattern.Length - 1);
+		bestMatchIndex = -1;
+		var currentMaxRange = pattern.Length + text.Length;
+		var lastComputedRow = Array.Empty<int>();
+		for (var d = 0; d < pattern.Length; d++)
+		{
+			var currentMinRange = 0;
+			var currentMidpoint = currentMaxRange;
+			while (currentMinRange < currentMidpoint)
+			{
+				if (MatchBitapScore(d, startIndex + currentMidpoint, startIndex, pattern) <= scoreThreshold)
+					currentMinRange = currentMidpoint;
+				else
+					currentMaxRange = currentMidpoint;
+				currentMidpoint = (currentMaxRange - currentMinRange) / 2 + currentMinRange;
+			}
+
+			currentMaxRange = currentMidpoint;
+			var start = Math.Max(1, startIndex - currentMidpoint + 1);
+			var finish = Math.Min(startIndex + currentMidpoint, text.Length) + pattern.Length;
+			var rd = new int[finish + 2];
+			rd[finish + 1] = (1 << d) - 1;
+			for (var j = finish; j >= start; j--)
+			{
+				int charMatch;
+				if (text.Length <= j - 1 || !s.ContainsKey(text[j - 1]))
+				{
+					charMatch = 0;
+				}
+				else
+				{
+					charMatch = s[text[j - 1]];
+				}
+
+				if (d == 0)
+				{
+					rd[j] = ((rd[j + 1] << 1) | 1) & charMatch;
+				}
+				else
+				{
+					rd[j] = ((rd[j + 1] << 1) | 1) & charMatch | ((lastComputedRow[j + 1] | lastComputedRow[j]) << 1) | 1 | lastComputedRow[j + 1];
+				}
+
+				if ((rd[j] & matchmask) != 0)
+				{
+					var score = MatchBitapScore(d, j - 1, startIndex, pattern);
+					if (score <= scoreThreshold)
+					{
+						scoreThreshold = score;
+						bestMatchIndex = j - 1;
+						if (bestMatchIndex > startIndex)
+						{
+							start = Math.Max(1, 2 * startIndex - bestMatchIndex);
+						}
+						else
+						{
+							break;
+						}
+					}
+				}
+			}
+
+			if (MatchBitapScore(d + 1, startIndex, startIndex, pattern) > scoreThreshold)
+			{
+				break;
+			}
+
+			lastComputedRow = rd;
+		}
+
+		return bestMatchIndex;
+	}
+
+	/// <summary>
+	/// 初始化Bitap算法的字母表
+	/// </summary>
+	/// <param name="pattern"></param>
+	/// <returns></returns>
+	internal static Dictionary<char, int> InitAlphabet(string pattern) => pattern.Select((c, i) => (c, i)).Aggregate(new Dictionary<char, int>(), (d, x) =>
+	{
+		d.TryGetValue(x.c, out var value);
+		d[x.c] = value | (1 << (pattern.Length - x.i - 1));
+		return d;
+	});
+
+	/// <summary>
+	/// 计算并匹配得分
+	/// </summary>
+	/// <param name="errors">匹配中的错误数</param>
+	/// <param name="location">匹配位置.</param>
+	/// <param name="expectedLocation">预期位置</param>
+	/// <param name="pattern">查找片段</param>
+	/// <returns>匹配得分 (0.0 = 最好, 1.0 = 最差).</returns>
+	private double MatchBitapScore(int errors, int location, int expectedLocation, string pattern)
+	{
+		var accuracy = (float)errors / pattern.Length;
+		var proximity = Math.Abs(expectedLocation - location);
+		return (_matchDistance, proximity) switch
+		{
+			(0, 0) => accuracy,
+			(0, _) => 1.0,
+			_ => accuracy + proximity / (float)_matchDistance
+		};
+	}
+}

+ 341 - 0
Masuit.Tools.AspNetCore/TextDiff/DiffAlgorithm.cs

@@ -0,0 +1,341 @@
+using System.Text;
+using static Masuit.Tools.TextDiff.DiffOperation;
+
+namespace Masuit.Tools.TextDiff;
+
+internal static class DiffAlgorithm
+{
+	/// <summary>
+	/// 找出两段文本之间的差异。通过在区分之前剥离文本中的常见前缀或后缀来简化
+	/// </summary>
+	/// <param name="text1"></param>
+	/// <param name="text2"></param>
+	/// <param name="checklines">加速标记。如果为false,则不要先运行行级差异来识别更改的区域。如果为true,则运行一个速度稍快但不太理想的差异</param>
+	/// <param name="optimized">启用优化</param>
+	/// <param name="token"></param>
+	/// <returns></returns>
+	internal static IEnumerable<TextDiffer> Compute(ReadOnlySpan<char> text1, ReadOnlySpan<char> text2, bool checklines, bool optimized, CancellationToken token)
+	{
+		if (text1.Length == text2.Length && text1.Length == 0)
+		{
+			return [];
+		}
+
+		var commonlength = TextUtil.CommonPrefix(text1, text2);
+		if (commonlength == text1.Length && commonlength == text2.Length)
+		{
+			return new List<TextDiffer>()
+			{
+				TextDiffer.Equal(text1)
+			};
+		}
+
+		var commonprefix = text1.Slice(0, commonlength);
+		text1 = text1[commonlength..];
+		text2 = text2[commonlength..];
+		commonlength = TextUtil.CommonSuffix(text1, text2);
+		var commonsuffix = text1[^commonlength..];
+		text1 = text1.Slice(0, text1.Length - commonlength);
+		text2 = text2.Slice(0, text2.Length - commonlength);
+
+		List<TextDiffer> diffs = new();
+		if (commonprefix.Length != 0)
+		{
+			diffs.Insert(0, TextDiffer.Equal(commonprefix));
+		}
+
+		diffs.AddRange(ComputeCore(text1, text2, checklines, optimized, token));
+		if (commonsuffix.Length != 0)
+		{
+			diffs.Add(TextDiffer.Equal(commonsuffix));
+		}
+
+		return diffs.CleanupMerge();
+	}
+
+	private static IEnumerable<TextDiffer> ComputeCore(ReadOnlySpan<char> text1, ReadOnlySpan<char> text2, bool checklines, bool optimized, CancellationToken token)
+	{
+		if (text1.Length == 0)
+		{
+			return TextDiffer.Insert(text2).ItemAsEnumerable();
+		}
+
+		if (text2.Length == 0)
+		{
+			return TextDiffer.Delete(text1).ItemAsEnumerable();
+		}
+
+		var longtext = text1.Length > text2.Length ? text1 : text2;
+		var shorttext = text1.Length > text2.Length ? text2 : text1;
+		var i = longtext.IndexOf(shorttext, StringComparison.Ordinal);
+		if (i != -1)
+		{
+			if (text1.Length > text2.Length)
+			{
+				return new[]
+				{
+					TextDiffer.Delete(longtext.Slice(0, i)),
+					TextDiffer.Equal(shorttext),
+					TextDiffer.Delete(longtext[(i + shorttext.Length)..])
+				};
+			}
+
+			return new[]
+			{
+				TextDiffer.Insert(longtext.Slice(0, i)),
+				TextDiffer.Equal(shorttext),
+				TextDiffer.Insert(longtext[(i + shorttext.Length)..])
+			};
+		}
+
+		if (shorttext.Length == 1)
+		{
+			return new[]
+			{
+				TextDiffer.Delete(text1),
+				TextDiffer.Insert(text2)
+			};
+		}
+
+		if (optimized)
+		{
+			var result = TextUtil.HalfMatch(text1, text2);
+			if (!result.IsEmpty)
+			{
+				var diffsA = Compute(result.Prefix1, result.Prefix2, checklines, true, token);
+				var diffsB = Compute(result.Suffix1, result.Suffix2, checklines, true, token);
+				return diffsA.Concat(TextDiffer.Equal(result.CommonMiddle)).Concat(diffsB);
+			}
+		}
+
+		if (checklines && text1.Length > 100 && text2.Length > 100)
+		{
+			return DiffLines(text1, text2, optimized, token);
+		}
+
+		return MyersDiffBisect(text1, text2, optimized, token);
+	}
+
+	/// <summary>
+	/// 对两个字符串进行快速的行级差分,然后重新划分部分以获得更高的精度。这种加速会产生非最小的差异。
+	/// </summary>
+	/// <param name="text1"></param>
+	/// <param name="text2"></param>
+	/// <param name="optimized"></param>
+	/// <param name="token"></param>
+	/// <returns></returns>
+	private static List<TextDiffer> DiffLines(ReadOnlySpan<char> text1, ReadOnlySpan<char> text2, bool optimized, CancellationToken token)
+	{
+		var compressor = new StringCompressor();
+		text1 = compressor.Compress(text1, char.MaxValue * 2 / 3);
+		text2 = compressor.Compress(text2);
+		var diffs = Compute(text1, text2, false, optimized, token).Select(diff => diff.Replace(compressor.Decompress(diff.Text))).ToList().CleanupSemantic();
+
+		return RediffAfterDiffLines(diffs, optimized, token).ToList();
+	}
+
+	/// <summary>
+	/// 逐个字符清除所有替换块
+	/// </summary>
+	/// <param name="diffs"></param>
+	/// <param name="optimized"></param>
+	/// <param name="token"></param>
+	/// <returns></returns>
+	private static IEnumerable<TextDiffer> RediffAfterDiffLines(IEnumerable<TextDiffer> diffs, bool optimized, CancellationToken token)
+	{
+		var ins = new StringBuilder();
+		var del = new StringBuilder();
+		foreach (var diff in diffs.Concat(TextDiffer.Empty))
+		{
+			(ins, del) = diff.Operation switch
+			{
+				Insert => (ins.Append(diff.Text), del),
+				Delete => (ins, del.Append(diff.Text)),
+				_ => (ins, del)
+			};
+
+			if (diff.Operation != Equal)
+			{
+				continue;
+			}
+
+			var consolidatedDiffsBeforeEqual = diff.Operation switch
+			{
+				Equal when ins.Length > 0 && del.Length > 0 => Compute(del.ToString(), ins.ToString(), false, optimized, token),
+				Equal when del.Length > 0 => TextDiffer.Delete(del.ToString()).ItemAsEnumerable(),
+				Equal when ins.Length > 0 => TextDiffer.Insert(ins.ToString()).ItemAsEnumerable(),
+				_ => []
+			};
+
+			foreach (var d in consolidatedDiffsBeforeEqual)
+			{
+				yield return d;
+			}
+
+			if (!diff.IsEmpty)
+				yield return diff;
+
+			ins.Clear();
+			del.Clear();
+		}
+	}
+
+	/// <summary>
+	/// 找到diff的“中间值”,将问题一分为二,并返回递归构造的diff。
+	/// </summary>
+	/// <param name="text1"></param>
+	/// <param name="text2"></param>
+	/// <param name="optimizeForSpeed"></param>
+	/// <param name="token"></param>
+	/// <returns></returns>
+	internal static IEnumerable<TextDiffer> MyersDiffBisect(ReadOnlySpan<char> text1, ReadOnlySpan<char> text2, bool optimizeForSpeed, CancellationToken token)
+	{
+		var text1Length = text1.Length;
+		var text2Length = text2.Length;
+		var maxD = (text1Length + text2Length + 1) / 2;
+		var vOffset = maxD;
+		var vLength = 2 * maxD;
+		var v1 = new int[vLength];
+		var v2 = new int[vLength];
+		for (var x = 0; x < vLength; x++)
+		{
+			v1[x] = -1;
+		}
+
+		for (var x = 0; x < vLength; x++)
+		{
+			v2[x] = -1;
+		}
+
+		v1[vOffset + 1] = 0;
+		v2[vOffset + 1] = 0;
+		var delta = text1Length - text2Length;
+		var front = delta % 2 != 0;
+		var k1Start = 0;
+		var k1End = 0;
+		var k2Start = 0;
+		var k2End = 0;
+		for (var d = 0; d < maxD; d++)
+		{
+			if (token.IsCancellationRequested)
+			{
+				break;
+			}
+
+			for (var k1 = -d + k1Start; k1 <= d - k1End; k1 += 2)
+			{
+				var k1Offset = vOffset + k1;
+				int x1;
+				if (k1 == -d || k1 != d && v1[k1Offset - 1] < v1[k1Offset + 1])
+				{
+					x1 = v1[k1Offset + 1];
+				}
+				else
+				{
+					x1 = v1[k1Offset - 1] + 1;
+				}
+
+				var y1 = x1 - k1;
+				while (x1 < text1Length && y1 < text2Length && text1[x1] == text2[y1])
+				{
+					x1++;
+					y1++;
+				}
+
+				v1[k1Offset] = x1;
+				if (x1 > text1Length)
+				{
+					k1End += 2;
+				}
+				else if (y1 > text2Length)
+				{
+					k1Start += 2;
+				}
+				else if (front)
+				{
+					var k2Offset = vOffset + delta - k1;
+					if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] != -1)
+					{
+						var x2 = text1Length - v2[k2Offset];
+						if (x1 >= x2)
+						{
+							return BisectSplit(text1, text2, x1, y1, optimizeForSpeed, token);
+						}
+					}
+				}
+			}
+
+			for (var k2 = -d + k2Start; k2 <= d - k2End; k2 += 2)
+			{
+				var k2Offset = vOffset + k2;
+				int x2;
+				if (k2 == -d || k2 != d && v2[k2Offset - 1] < v2[k2Offset + 1])
+				{
+					x2 = v2[k2Offset + 1];
+				}
+				else
+				{
+					x2 = v2[k2Offset - 1] + 1;
+				}
+
+				var y2 = x2 - k2;
+				while (x2 < text1Length && y2 < text2Length && text1[text1Length - x2 - 1] == text2[text2Length - y2 - 1])
+				{
+					x2++;
+					y2++;
+				}
+
+				v2[k2Offset] = x2;
+				if (x2 > text1Length)
+				{
+					k2End += 2;
+				}
+				else if (y2 > text2Length)
+				{
+					k2Start += 2;
+				}
+				else if (!front)
+				{
+					var k1Offset = vOffset + delta - k2;
+					if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] != -1)
+					{
+						var x1 = v1[k1Offset];
+						var y1 = vOffset + x1 - k1Offset;
+						x2 = text1Length - v2[k2Offset];
+						if (x1 >= x2)
+						{
+							return BisectSplit(text1, text2, x1, y1, optimizeForSpeed, token);
+						}
+					}
+				}
+			}
+		}
+
+		return new[]
+		{
+			TextDiffer.Delete(text1),
+			TextDiffer.Insert(text2)
+		};
+	}
+
+	/// <summary>
+	/// 给定“中值”的位置,将diff分成两部分并递归。
+	/// </summary>
+	/// <param name="text1"></param>
+	/// <param name="text2"></param>
+	/// <param name="x">文本1的中值位置.</param>
+	/// <param name="y">文本2的中值位置.</param>
+	/// <param name="optimized"></param>
+	/// <param name="token"></param>
+	/// <returns></returns>
+	private static IEnumerable<TextDiffer> BisectSplit(ReadOnlySpan<char> text1, ReadOnlySpan<char> text2, int x, int y, bool optimized, CancellationToken token)
+	{
+		var text1A = text1[..x];
+		var text2A = text2[..y];
+		var text1B = text1[x..];
+		var text2B = text2[y..];
+		var diffsa = Compute(text1A, text2A, false, optimized, token);
+		var diffsb = Compute(text1B, text2B, false, optimized, token);
+		return diffsa.Concat(diffsb);
+	}
+}

+ 631 - 0
Masuit.Tools.AspNetCore/TextDiff/DiffExtension.cs

@@ -0,0 +1,631 @@
+using System.Collections.Immutable;
+using System.Text;
+using System.Text.RegularExpressions;
+using static Masuit.Tools.TextDiff.DiffOperation;
+
+namespace Masuit.Tools.TextDiff;
+
+public static class DiffExtension
+{
+	/// <summary>
+	/// 还原文本1
+	/// </summary>
+	/// <param name="diffs"></param>
+	/// <returns></returns>
+	public static string Text1(this IEnumerable<TextDiffer> diffs) => diffs.Where(d => d.Operation != Insert).Aggregate(new StringBuilder(), (sb, diff) => sb.Append(diff.Text)).ToString();
+
+	/// <summary>
+	/// 还原文本2
+	/// </summary>
+	/// <param name="diffs"></param>
+	/// <returns></returns>
+	public static string Text2(this IEnumerable<TextDiffer> diffs) => diffs.Where(d => d.Operation != Delete).Aggregate(new StringBuilder(), (sb, diff) => sb.Append(diff.Text)).ToString();
+
+	private readonly record struct LevenshteinState(int Insertions, int Deletions, int Levenshtein)
+	{
+		public LevenshteinState Consolidate() => new(0, 0, Levenshtein + Math.Max(Insertions, Deletions));
+	}
+
+	/// <summary>
+	/// 计算Levenshtein距离;插入、删除或替换的字符数
+	/// </summary>
+	/// <param name="diffs"></param>
+	/// <returns></returns>
+	internal static int Levenshtein(this IEnumerable<TextDiffer> diffs)
+	{
+		var state = new LevenshteinState(0, 0, 0);
+		state = diffs.Aggregate(state, (current, aDiff) => aDiff.Operation switch
+		{
+			Insert => current with
+			{
+				Insertions = current.Insertions + aDiff.Text.Length
+			},
+			Delete => current with
+			{
+				Deletions = current.Deletions + aDiff.Text.Length
+			},
+			Equal => current.Consolidate(),
+			_ => throw new IndexOutOfRangeException()
+		});
+
+		return state.Consolidate().Levenshtein;
+	}
+
+	private static char ToDelta(this DiffOperation o) => o switch
+	{
+		Delete => '-',
+		Insert => '+',
+		Equal => '=',
+		_ => throw new ArgumentException($"Unknown Operation: {o}")
+	};
+
+	/// <summary>
+	/// 将diff压缩成一个编码字符串,该字符串描述了将text1转换为text2所需的操作。例如,=3\t-2\t+ing->:保留3个字符,删除2个字符,插入“ing”。操作以制表符分隔。插入的文本使用%xx符号转义。
+	/// </summary>
+	/// <param name="diffs"></param>
+	/// <returns></returns>
+	public static string ToDelta(this IEnumerable<TextDiffer> diffs)
+	{
+		var s = from aDiff in diffs
+				let sign = aDiff.Operation.ToDelta()
+				let textToAppend = aDiff.Operation == Insert ? aDiff.Text.UrlEncoded() : aDiff.Text.Length.ToString()
+				select string.Concat(sign, textToAppend);
+		return s.Join("\t");
+	}
+
+	private static DiffOperation FromDelta(char c) => c switch
+	{
+		'-' => Delete,
+		'+' => Insert,
+		'=' => Equal,
+		_ => throw new ArgumentException($"Invalid Delta Token: {c}")
+	};
+
+	/// <summary>
+	/// 从差异字符串中恢复diff对象
+	/// </summary>
+	/// <param name="text1"></param>
+	/// <param name="delta"></param>
+	/// <returns></returns>
+	public static IEnumerable<TextDiffer> FromDelta(this string text1, string delta)
+	{
+		var pointer = 0;
+
+		foreach (var token in delta.SplitBy('\t'))
+		{
+			if (token.Length == 0)
+			{
+				continue;
+			}
+
+			var param = token[1..];
+			var operation = FromDelta(token[0]);
+			int n = 0;
+			if (operation != Insert)
+			{
+				if (!int.TryParse(param, out n))
+				{
+					throw new ArgumentException($"Invalid number in Diff.FromDelta: {param}");
+				}
+
+				if (pointer > text1.Length - n)
+				{
+					throw new ArgumentException($"Delta length ({pointer}) larger than source text length ({text1.Length}).");
+				}
+			}
+
+			(var text, pointer) = operation switch
+			{
+				Insert => (param.Replace("+", "%2b").UrlDecoded(), pointer),
+				Equal => (text1.Substring(pointer, n), pointer + n),
+				Delete => (text1.Substring(pointer, n), pointer + n),
+				_ => throw new ArgumentException($"Unknown Operation: {operation}")
+			};
+			yield return TextDiffer.Create(operation, text);
+		}
+
+		if (pointer != text1.Length)
+		{
+			throw new ArgumentException($"Delta length ({pointer}) smaller than source text length ({text1.Length}).");
+		}
+	}
+
+	internal static IEnumerable<TextDiffer> CleanupMergePass1(this IEnumerable<TextDiffer> diffs)
+	{
+		var sbDelete = new StringBuilder();
+		var sbInsert = new StringBuilder();
+		var lastEquality = TextDiffer.Empty;
+
+		using var enumerator = diffs.Concat(TextDiffer.Empty).GetEnumerator();
+		while (enumerator.MoveNext())
+		{
+			var diff = enumerator.Current;
+
+			(sbInsert, sbDelete) = diff.Operation switch
+			{
+				Insert => (sbInsert.Append(diff.Text), sbDelete),
+				Delete => (sbInsert, sbDelete.Append(diff.Text)),
+				_ => (sbInsert, sbDelete)
+			};
+
+			if (diff.Operation == Equal)
+			{
+				if (sbInsert.Length > 0 || sbDelete.Length > 0)
+				{
+					var prefixLength = TextUtil.CommonPrefix(sbInsert, sbDelete);
+					if (prefixLength > 0)
+					{
+						var commonprefix = sbInsert.ToString(0, prefixLength);
+						sbInsert.Remove(0, prefixLength);
+						sbDelete.Remove(0, prefixLength);
+						lastEquality = lastEquality.Append(commonprefix);
+					}
+
+					var suffixLength = TextUtil.CommonSuffix(sbInsert, sbDelete);
+					if (suffixLength > 0)
+					{
+						var commonsuffix = sbInsert.ToString(sbInsert.Length - suffixLength, suffixLength);
+						sbInsert.Remove(sbInsert.Length - suffixLength, suffixLength);
+						sbDelete.Remove(sbDelete.Length - suffixLength, suffixLength);
+						diff = diff.Prepend(commonsuffix);
+					}
+
+					if (!lastEquality.IsEmpty)
+					{
+						yield return lastEquality;
+					}
+
+					if (sbDelete.Length > 0) yield return TextDiffer.Delete(sbDelete.ToString());
+					if (sbInsert.Length > 0) yield return TextDiffer.Insert(sbInsert.ToString());
+					lastEquality = diff;
+					sbDelete.Clear();
+					sbInsert.Clear();
+				}
+				else
+				{
+					lastEquality = lastEquality.Append(diff.Text);
+				}
+			}
+		}
+
+		if (!lastEquality.IsEmpty)
+		{
+			yield return lastEquality;
+		}
+	}
+
+	internal static IEnumerable<TextDiffer> CleanupMergePass2(this IEnumerable<TextDiffer> input, out bool haschanges)
+	{
+		haschanges = false;
+		var diffs = input.ToList();
+		for (var i = 1; i < diffs.Count - 1; i++)
+		{
+			var previous = diffs[i - 1];
+			var current = diffs[i];
+			var next = diffs[i + 1];
+			if (previous.Operation == Equal && next.Operation == Equal)
+			{
+				var currentSpan = current.Text.AsSpan();
+				var previousSpan = previous.Text.AsSpan();
+				var nextSpan = next.Text.AsSpan();
+				if (currentSpan.Length >= previousSpan.Length && currentSpan[^previousSpan.Length..].SequenceEqual(previousSpan))
+				{
+					var text = previous.Text + current.Text[..^previous.Text.Length];
+					diffs[i] = current.Replace(text);
+					diffs[i + 1] = next.Replace(previous.Text + next.Text);
+					diffs.Splice(i - 1, 1);
+					haschanges = true;
+				}
+				else if (currentSpan.Length >= nextSpan.Length && currentSpan[..nextSpan.Length].SequenceEqual(nextSpan))
+				{
+					diffs[i - 1] = previous.Replace(previous.Text + next.Text);
+					diffs[i] = current.Replace(current.Text[next.Text.Length..] + next.Text);
+					diffs.Splice(i + 1, 1);
+					haschanges = true;
+				}
+			}
+		}
+
+		return diffs;
+	}
+
+	internal static IEnumerable<TextDiffer> CleanupMerge(this IEnumerable<TextDiffer> diffs)
+	{
+		bool changes;
+		do
+		{
+			diffs = diffs.CleanupMergePass1().CleanupMergePass2(out changes).ToList(); // required to detect if anything changed
+		} while (changes);
+
+		return diffs;
+	}
+
+	readonly record struct EditBetweenEqualities(string Equality1, string Edit, string Equality2)
+	{
+		public int Score => DiffCleanupSemanticScore(Equality1, Edit) + DiffCleanupSemanticScore(Edit, Equality2);
+
+		private readonly record struct ScoreHelper(string Str, Index I, Regex Regex)
+		{
+			char C => Str[I];
+			public bool IsEmpty => Str.Length == 0;
+			public bool NonAlphaNumeric => !char.IsLetterOrDigit(C);
+			public bool IsWhitespace => char.IsWhiteSpace(C);
+			public bool IsLineBreak => C == '\n' || C == '\r';
+			public bool IsBlankLine => IsLineBreak && Regex.IsMatch(Str);
+		}
+
+		/// 给定两个字符串,计算一个分数,表示内部边界是否落在逻辑边界上
+		private static int DiffCleanupSemanticScore(string one, string two) => (h1: new ScoreHelper(one, ^1, BlankLineEnd), h2: new ScoreHelper(two, 0, BlankLineStart)) switch
+		{
+			{ h1.IsEmpty: true } or { h2.IsEmpty: true } => 6,
+			{ h1.IsBlankLine: true } or { h2.IsBlankLine: true } => 5,
+			{ h1.IsLineBreak: true } or { h2.IsLineBreak: true } => 4,
+			{ h1.NonAlphaNumeric: true } and { h1.IsWhitespace: false } and { h2.IsWhitespace: true } => 3,
+			{ h1.IsWhitespace: true } or { h2.IsWhitespace: true } => 2,
+			{ h1.NonAlphaNumeric: true } or { h2.NonAlphaNumeric: true } => 1,
+			_ => 0
+		};
+
+		// 将编辑尽可能向左移动
+		public EditBetweenEqualities ShiftLeft()
+		{
+			var commonOffset = TextUtil.CommonSuffix(Equality1, Edit);
+			if (commonOffset > 0)
+			{
+				var commonString = Edit[^commonOffset..];
+				var equality1 = Equality1[..^commonOffset];
+				var edit = commonString + Edit[..^commonOffset];
+				var equality2 = commonString + Equality2;
+				return new EditBetweenEqualities(Equality1: equality1, Edit: edit, Equality2: equality2);
+			}
+
+			return this;
+		}
+
+		private EditBetweenEqualities ShiftRight() => new(Equality1: Equality1 + Edit[0], Edit: Edit[1..] + Equality2[0], Equality2: Equality2[1..]);
+
+		public IEnumerable<EditBetweenEqualities> TraverseRight()
+		{
+			var item = this;
+			while (item.Edit.Length != 0 && item.Equality2.Length != 0 && item.Edit[0] == item.Equality2[0])
+			{
+				yield return item = item.ShiftRight();
+			}
+		}
+
+		public IEnumerable<TextDiffer> ToDiffs(DiffOperation edit)
+		{
+			yield return TextDiffer.Equal(Equality1);
+			yield return TextDiffer.Create(edit, Edit);
+			yield return TextDiffer.Equal(Equality2);
+		}
+	}
+
+	/// <summary>
+	/// 寻找两侧被等式包围的单个编辑,等式可以侧向移动,将编辑与单词边界对齐。
+	/// e.g: The c<ins>at c</ins>ame. -> The <ins>cat </ins>came.
+	/// </summary>
+	/// <param name="diffs"></param>
+	internal static IEnumerable<TextDiffer> CleanupSemanticLossless(this IEnumerable<TextDiffer> diffs)
+	{
+		using var enumerator = diffs.GetEnumerator();
+		if (!enumerator.MoveNext())
+		{
+			yield break;
+		}
+
+		var previous = enumerator.Current;
+		if (!enumerator.MoveNext())
+		{
+			yield return previous;
+			yield break;
+		}
+
+		var current = enumerator.Current;
+		while (true)
+		{
+			if (!enumerator.MoveNext())
+			{
+				yield return previous;
+				yield return current;
+				yield break;
+			}
+
+			var next = enumerator.Current;
+			if (previous.Operation == Equal && next.Operation == Equal)
+			{
+				var item = new EditBetweenEqualities(previous.Text, current.Text, next.Text).ShiftLeft();
+				var best = item.TraverseRight().Aggregate(item, (best, x) => best.Score > x.Score ? best : x);
+				if (previous.Text != best.Equality1)
+				{
+					foreach (var d in best.ToDiffs(current.Operation).Where(d => !d.IsEmpty))
+					{
+						yield return d;
+					}
+
+					if (!enumerator.MoveNext())
+					{
+						yield break;
+					}
+
+					previous = current;
+					current = next;
+					next = enumerator.Current;
+				}
+				else
+				{
+					yield return previous;
+				}
+			}
+			else
+			{
+				yield return previous;
+			}
+
+			previous = current;
+			current = next;
+		}
+	}
+
+	private static readonly Regex BlankLineEnd = new("\\n\\r?\\n\\Z", RegexOptions.Compiled);
+
+	private static readonly Regex BlankLineStart = new("\\A\\r?\\n\\r?\\n", RegexOptions.Compiled);
+
+	/// <summary>
+	/// 从效率上清理一些无用的差异对象
+	/// </summary>
+	/// <param name="input"></param>
+	/// <param name="diffEditCost"></param>
+	internal static IEnumerable<TextDiffer> CleanupEfficiency(this IEnumerable<TextDiffer> input, short diffEditCost = 4)
+	{
+		var diffs = input.ToList();
+		var changes = false;
+		var equalities = new Stack<int>();
+		var lastEquality = string.Empty;
+		var insertionBeforeLastEquality = false;
+		var deletionBeforeLastEquality = false;
+		var insertionAfterLastEquality = false;
+		var deletionAfterLastEquality = false;
+		for (var i = 0; i < diffs.Count; i++)
+		{
+			var diff = diffs[i];
+			if (diff.Operation == Equal)
+			{
+				if (diff.Text.Length < diffEditCost && (insertionAfterLastEquality || deletionAfterLastEquality))
+				{
+					equalities.Push(i);
+					(insertionBeforeLastEquality, deletionBeforeLastEquality) = (insertionAfterLastEquality, deletionAfterLastEquality);
+					lastEquality = diff.Text;
+				}
+				else
+				{
+					equalities.Clear();
+					lastEquality = string.Empty;
+				}
+
+				insertionAfterLastEquality = deletionAfterLastEquality = false;
+			}
+			else
+			{
+				if (diff.Operation == Delete)
+				{
+					deletionAfterLastEquality = true;
+				}
+				else
+				{
+					insertionAfterLastEquality = true;
+				}
+
+				/*
+                 * 分割以下几种情况:
+                 * <ins>A</ins><del>B</del>XY<ins>C</ins><del>D</del>
+                 * <ins>A</ins>X<ins>C</ins><del>D</del>
+                 * <ins>A</ins><del>B</del>X<ins>C</ins>
+                 * <ins>A</del>X<ins>C</ins><del>D</del>
+                 * <ins>A</ins><del>B</del>X<del>C</del>
+                 */
+				if ((lastEquality.Length != 0) && ((insertionBeforeLastEquality && deletionBeforeLastEquality && insertionAfterLastEquality && deletionAfterLastEquality) || ((lastEquality.Length < diffEditCost / 2) && (insertionBeforeLastEquality ? 1 : 0) + (deletionBeforeLastEquality ? 1 : 0) + (insertionAfterLastEquality ? 1 : 0) + (deletionAfterLastEquality ? 1 : 0) == 3)))
+				{
+					diffs.Splice(equalities.Peek(), 1, TextDiffer.Delete(lastEquality), TextDiffer.Insert(lastEquality));
+					equalities.Pop();
+					lastEquality = string.Empty;
+					if (insertionBeforeLastEquality && deletionBeforeLastEquality)
+					{
+						insertionAfterLastEquality = deletionAfterLastEquality = true;
+						equalities.Clear();
+					}
+					else
+					{
+						if (equalities.Count > 0)
+						{
+							equalities.Pop();
+						}
+
+						i = equalities.Count > 0 ? equalities.Peek() : -1;
+						insertionAfterLastEquality = deletionAfterLastEquality = false;
+					}
+
+					changes = true;
+				}
+			}
+		}
+
+		if (changes)
+		{
+			return diffs.CleanupMerge();
+		}
+
+		return input;
+	}
+
+	/// <summary>
+	/// 两个不相关文本的差异可以用巧合的匹配来填充。例如,“mouse”和“sofas”的区别是
+	/// `[(-1, "m"), (1, "s"), (0, "o"), (-1, "u"), (1, "fa"), (0, "s"), (-1, "e")]`.
+	/// 虽然这是最佳差异,但人类很难理解。语义清理重写了差异,将其扩展为更易于理解的格式。上述示例将变为: `[(-1, "mouse"), (1, "sofas")]`.
+	/// </summary>
+	/// <param name="input"></param>
+	/// <returns></returns>
+	public static IImmutableList<TextDiffer> MakeHumanReadable(this IEnumerable<TextDiffer> input) => input.CleanupSemantic().ToImmutableList();
+
+	/// <summary>
+	/// 此函数类似于“OptimizeForReadability”,除了它不是将差异优化为人类可读,而是优化差异以提高机器处理的效率。两种清理类型的结果通常是相同的。CleanupEfficiency就是基于这样的,即由大量小差异编辑组成的差异可能需要更长的时间来处理(在下游应用程序中),或者需要比少量较大差异更多的存储或传输容量。
+	/// </summary>
+	/// <param name="input"></param>
+	/// <param name="diffEditCost">处理新编辑的成本,即处理现有编辑中的额外字符。默认值为4,这意味着如果将差异的长度扩展三个字符可以消除一次编辑,那么这种优化将降低总成本</param>
+	/// <returns></returns>
+	public static IImmutableList<TextDiffer> OptimizeForMachineProcessing(this IEnumerable<TextDiffer> input, short diffEditCost = 4) => input.CleanupEfficiency(diffEditCost).ToImmutableList();
+
+	/// <summary>
+	/// 从语法上清理一些无用的差异对象
+	/// </summary>
+	/// <param name="input"></param>
+	internal static List<TextDiffer> CleanupSemantic(this IEnumerable<TextDiffer> input)
+	{
+		var diffs = input.ToList();
+		var equalities = new Stack<int>();
+		string lastEquality = null;
+		var pointer = 0;
+		var lengthInsertions1 = 0;
+		var lengthDeletions1 = 0;
+		var lengthInsertions2 = 0;
+		var lengthDeletions2 = 0;
+		while (pointer < diffs.Count)
+		{
+			if (diffs[pointer].Operation == Equal)
+			{
+				equalities.Push(pointer);
+				lengthInsertions1 = lengthInsertions2;
+				lengthDeletions1 = lengthDeletions2;
+				lengthInsertions2 = 0;
+				lengthDeletions2 = 0;
+				lastEquality = diffs[pointer].Text;
+			}
+			else
+			{
+				if (diffs[pointer].Operation == Insert)
+				{
+					lengthInsertions2 += diffs[pointer].Text.Length;
+				}
+				else
+				{
+					lengthDeletions2 += diffs[pointer].Text.Length;
+				}
+
+				if (lastEquality != null && (lastEquality.Length <= Math.Max(lengthInsertions1, lengthDeletions1)) && (lastEquality.Length <= Math.Max(lengthInsertions2, lengthDeletions2)))
+				{
+					diffs.Splice(equalities.Peek(), 1, TextDiffer.Delete(lastEquality), TextDiffer.Insert(lastEquality));
+					equalities.Pop();
+					if (equalities.Count > 0)
+					{
+						equalities.Pop();
+					}
+
+					pointer = equalities.Count > 0 ? equalities.Peek() : -1;
+					lengthInsertions1 = 0; // Reset the counters.
+					lengthDeletions1 = 0;
+					lengthInsertions2 = 0;
+					lengthDeletions2 = 0;
+					lastEquality = null;
+				}
+			}
+
+			pointer++;
+		}
+
+		diffs = diffs.CleanupMerge().CleanupSemanticLossless().ToList();
+
+		// 查找删除和插入之间的重叠.
+		// e.g: <del>abcxxx</del><ins>xxxdef</ins>
+		//   -> <del>abc</del>xxx<ins>def</ins>
+		// e.g: <del>xxxabc</del><ins>defxxx</ins>
+		//   -> <ins>def</ins>xxx<del>abc</del>
+		// 只有当重叠与前面或后面的编辑一样大时,才提取重叠
+		pointer = 1;
+		while (pointer < diffs.Count)
+		{
+			if (diffs[pointer - 1].Operation == Delete && diffs[pointer].Operation == Insert)
+			{
+				var deletion = diffs[pointer - 1].Text.AsSpan();
+				var insertion = diffs[pointer].Text.AsSpan();
+				var overlapLength1 = TextUtil.CommonOverlap(deletion, insertion);
+				var overlapLength2 = TextUtil.CommonOverlap(insertion, deletion);
+				var minLength = Math.Min(deletion.Length, insertion.Length);
+
+				TextDiffer[] newdiffs = null;
+				if ((overlapLength1 >= overlapLength2) && (overlapLength1 >= minLength / 2.0))
+				{
+					newdiffs = new[]
+					{
+						TextDiffer.Delete(deletion.Slice(0, deletion.Length - overlapLength1).ToArray()),
+						TextDiffer.Equal(insertion.Slice(0, overlapLength1).ToArray()),
+						TextDiffer.Insert(insertion[overlapLength1..].ToArray())
+					};
+				}
+				else if ((overlapLength2 >= overlapLength1) && overlapLength2 >= minLength / 2.0)
+				{
+					newdiffs = new[]
+					{
+						TextDiffer.Insert(insertion.Slice(0, insertion.Length - overlapLength2)),
+						TextDiffer.Equal(deletion.Slice(0, overlapLength2)),
+						TextDiffer.Delete(deletion[overlapLength2..])
+					};
+				}
+
+				if (newdiffs != null)
+				{
+					diffs.Splice(pointer - 1, 2, newdiffs);
+					pointer++;
+				}
+
+				pointer++;
+			}
+
+			pointer++;
+		}
+
+		return diffs;
+	}
+
+	/// <summary>
+	/// 计算并返回目标文本中的等效位置
+	/// </summary>
+	/// <param name="diffs"></param>
+	/// <param name="location1"></param>
+	/// <returns></returns>
+	internal static int FindEquivalentLocation2(this IEnumerable<TextDiffer> diffs, int location1)
+	{
+		var chars1 = 0;
+		var chars2 = 0;
+		var lastChars1 = 0;
+		var lastChars2 = 0;
+		var lastDiff = TextDiffer.Empty;
+		foreach (var aDiff in diffs)
+		{
+			if (aDiff.Operation != Insert)
+			{
+				chars1 += aDiff.Text.Length;
+			}
+
+			if (aDiff.Operation != Delete)
+			{
+				chars2 += aDiff.Text.Length;
+			}
+
+			if (chars1 > location1)
+			{
+				lastDiff = aDiff;
+				break;
+			}
+
+			lastChars1 = chars1;
+			lastChars2 = chars2;
+		}
+
+		if (lastDiff.Operation == Delete)
+		{
+			return lastChars2;
+		}
+
+		return lastChars2 + (location1 - lastChars1);
+	}
+}

+ 8 - 0
Masuit.Tools.AspNetCore/TextDiff/DiffOperation.cs

@@ -0,0 +1,8 @@
+namespace Masuit.Tools.TextDiff;
+
+public enum DiffOperation
+{
+	Delete = '-',
+	Insert = '+',
+	Equal = ' '
+}

+ 220 - 0
Masuit.Tools.AspNetCore/TextDiff/DiffPatch.cs

@@ -0,0 +1,220 @@
+using System.Collections.Immutable;
+using System.Text;
+using static Masuit.Tools.TextDiff.DiffOperation;
+
+namespace Masuit.Tools.TextDiff;
+
+public record DiffPatch(int Start1, int Length1, int Start2, int Length2, SemanticsImmutableList<TextDiffer> Diffs)
+{
+	public bool IsEmpty => Diffs.IsEmpty;
+
+	public DiffPatch Bump(int length) => this with
+	{
+		Start1 = Start1 + length,
+		Start2 = Start2 + length
+	};
+
+	public bool StartsWith(DiffOperation operation) => Diffs[0].Operation == operation;
+	public bool EndsWith(DiffOperation operation) => Diffs[^1].Operation == operation;
+
+	internal DiffPatch AddPadding(string padding)
+	{
+		var (s1, l1, s2, l2, diffs) = this;
+		var builder = diffs.ToBuilder();
+		(s1, l1, s2, l2) = AddPaddingBegin(builder, s1, l1, s2, l2, padding);
+		(s1, l1, s2, l2) = AddPaddingEnd(builder, s1, l1, s2, l2, padding);
+
+		return new DiffPatch(s1, l1, s2, l2, builder.ToImmutable());
+	}
+
+	internal DiffPatch AddPaddingBegin(string padding)
+	{
+		var (s1, l1, s2, l2, diffs) = this;
+		var builder = diffs.ToBuilder();
+		(s1, l1, s2, l2) = AddPaddingBegin(builder, s1, l1, s2, l2, padding);
+		return new DiffPatch(s1, l1, s2, l2, builder.ToImmutable());
+	}
+
+	private (int s1, int l1, int s2, int l2) AddPaddingBegin(ImmutableList<TextDiffer>.Builder builder, int s1, int l1, int s2, int l2, string padding)
+	{
+		if (!StartsWith(Equal))
+		{
+			builder.Insert(0, TextDiffer.Equal(padding));
+			return (s1 - padding.Length, l1 + padding.Length, s2 - padding.Length, l2 + padding.Length);
+		}
+
+		if (padding.Length <= Diffs[0].Text.Length)
+		{
+			return (s1, l1, s2, l2);
+		}
+
+		var firstDiff = Diffs[0];
+		var extraLength = padding.Length - firstDiff.Text.Length;
+		var text = padding[firstDiff.Text.Length..] + firstDiff.Text;
+		builder.RemoveAt(0);
+		builder.Insert(0, firstDiff.Replace(text));
+		return (s1 - extraLength, l1 + extraLength, s2 - extraLength, l2 + extraLength);
+	}
+
+	internal DiffPatch AddPaddingEnd(string padding)
+	{
+		var (s1, l1, s2, l2, diffs) = this;
+		var builder = diffs.ToBuilder();
+		(s1, l1, s2, l2) = AddPaddingEnd(builder, s1, l1, s2, l2, padding);
+		return new DiffPatch(s1, l1, s2, l2, builder.ToImmutable());
+	}
+
+	private (int s1, int l1, int s2, int l2) AddPaddingEnd(ImmutableList<TextDiffer>.Builder builder, int s1, int l1, int s2, int l2, string padding)
+	{
+		if (!EndsWith(Equal))
+		{
+			builder.Add(TextDiffer.Equal(padding));
+			return (s1, l1 + padding.Length, s2, l2 + padding.Length);
+		}
+
+		if (padding.Length <= Diffs[^1].Text.Length)
+		{
+			return (s1, l1, s2, l2);
+		}
+
+		var lastDiff = Diffs[^1];
+		var extraLength = padding.Length - lastDiff.Text.Length;
+		var text = lastDiff.Text + padding[..extraLength];
+		builder.RemoveAt(builder.Count - 1);
+		builder.Add(lastDiff.Replace(text));
+		return (s1, l1 + extraLength, s2, l2 + extraLength);
+	}
+
+	/// <summary>
+	/// 计算一个补丁列表,将text1转换为text2。将计算一组差异
+	/// </summary>
+	/// <param name="text1"></param>
+	/// <param name="text2"></param>
+	/// <param name="diffTimeout">超时限制</param>
+	/// <param name="diffEditCost"></param>
+	/// <returns></returns>
+	public static SemanticsImmutableList<DiffPatch> Compute(string text1, string text2, float diffTimeout = 0, short diffEditCost = 4)
+	{
+		using var cts = diffTimeout <= 0 ? new CancellationTokenSource() : new CancellationTokenSource(TimeSpan.FromSeconds(diffTimeout));
+		return Compute(text1, DiffAlgorithm.Compute(text1, text2, true, true, cts.Token).CleanupSemantic().CleanupEfficiency(diffEditCost)).ToImmutableList().WithValueSemantics();
+	}
+
+	/// <summary>
+	/// 计算一个patch列表,将text1转换为text2。不提供text2,Diffs是text1和text2之间的差值
+	/// </summary>
+	/// <param name="text1"></param>
+	/// <param name="diffs"></param>
+	/// <param name="patchMargin"></param>
+	/// <returns></returns>
+	public static IEnumerable<DiffPatch> Compute(string text1, IEnumerable<TextDiffer> diffs, short patchMargin = 4)
+	{
+		if (!diffs.Any())
+		{
+			yield break;
+		}
+
+		var charCount1 = 0;
+		var charCount2 = 0;
+		var prepatchText = text1;
+		var postpatchText = text1;
+		var newdiffs = ImmutableList.CreateBuilder<TextDiffer>();
+		int start1 = 0, length1 = 0, start2 = 0, length2 = 0;
+		foreach (var aDiff in diffs)
+		{
+			if (!newdiffs.Any() && aDiff.Operation != Equal)
+			{
+				start1 = charCount1;
+				start2 = charCount2;
+			}
+
+			switch (aDiff.Operation)
+			{
+				case Insert:
+					newdiffs.Add(aDiff);
+					length2 += aDiff.Text.Length;
+					postpatchText = postpatchText.Insert(charCount2, aDiff.Text);
+					break;
+
+				case Delete:
+					length1 += aDiff.Text.Length;
+					newdiffs.Add(aDiff);
+					postpatchText = postpatchText.Remove(charCount2, aDiff.Text.Length);
+					break;
+
+				case Equal:
+					if (aDiff.Text.Length <= 2 * patchMargin && newdiffs.Any() && aDiff != diffs.Last())
+					{
+						newdiffs.Add(aDiff);
+						length1 += aDiff.Text.Length;
+						length2 += aDiff.Text.Length;
+					}
+
+					if (aDiff.Text.Length >= 2 * patchMargin)
+					{
+						if (newdiffs.Any())
+						{
+							(start1, length1, start2, length2) = newdiffs.AddContext(prepatchText, start1, length1, start2, length2);
+							yield return new DiffPatch(start1, length1, start2, length2, newdiffs.ToImmutable());
+							start1 = start2 = length1 = length2 = 0;
+							newdiffs.Clear();
+
+							// http://code.google.com/p/google-diff-match-patch/wiki/Unidiff
+							prepatchText = postpatchText;
+							charCount1 = charCount2;
+						}
+					}
+
+					break;
+			}
+
+			if (aDiff.Operation != Insert)
+			{
+				charCount1 += aDiff.Text.Length;
+			}
+
+			if (aDiff.Operation != Delete)
+			{
+				charCount2 += aDiff.Text.Length;
+			}
+		}
+
+		if (newdiffs.Any())
+		{
+			(start1, length1, start2, length2) = newdiffs.AddContext(prepatchText, start1, length1, start2, length2);
+			yield return new DiffPatch(start1, length1, start2, length2, newdiffs.ToImmutable());
+		}
+	}
+
+	/// <summary>
+	/// 计算一个patch列表,将text1转换为text2。text1将从提供的Diff中导出。
+	/// </summary>
+	/// <param name="diffs"></param>
+	/// <returns></returns>
+	public static SemanticsImmutableList<DiffPatch> FromDiffs(IEnumerable<TextDiffer> diffs) => Compute(diffs.Text1(), diffs).ToImmutableList().WithValueSemantics();
+
+	public override string ToString()
+	{
+		var coords1 = Length1 switch
+		{
+			0 => Start1 + ",0",
+			1 => Convert.ToString(Start1 + 1),
+			_ => Start1 + 1 + "," + Length1
+		};
+
+		var coords2 = Length2 switch
+		{
+			0 => Start2 + ",0",
+			1 => Convert.ToString(Start2 + 1),
+			_ => Start2 + 1 + "," + Length2
+		};
+
+		var text = new StringBuilder().Append("@@ -").Append(coords1).Append(" +").Append(coords2).Append(" @@\n");
+		foreach (var aDiff in Diffs)
+		{
+			text.Append((char)aDiff.Operation);
+			text.Append(aDiff.Text.UrlEncoded()).Append("\n");
+		}
+
+		return text.ToString();
+	}
+}

+ 55 - 0
Masuit.Tools.AspNetCore/TextDiff/DifferBuilder.cs

@@ -0,0 +1,55 @@
+using System.Collections.Immutable;
+
+namespace Masuit.Tools.TextDiff;
+
+internal static class DifferBuilder
+{
+	/// <summary>
+	/// 添加上下文,直到它是唯一的,不要让模式扩展到Match_MaxBits之外
+	/// </summary>
+	/// <param name="diffListBuilder"></param>
+	/// <param name="text"></param>
+	/// <param name="length2"></param>
+	/// <param name="patchMargin"></param>
+	/// <param name="start1"></param>
+	/// <param name="length1"></param>
+	/// <param name="start2"></param>
+	internal static (int start1, int length1, int start2, int length2) AddContext(this ImmutableList<TextDiffer>.Builder diffListBuilder, string text, int start1, int length1, int start2, int length2, short patchMargin = 4)
+	{
+		if (text.Length == 0)
+		{
+			return (start1, length1, start2, length2);
+		}
+
+		var pattern = text.Substring(start2, length1);
+		var padding = 0;
+		while (text.IndexOf(pattern, StringComparison.Ordinal) != text.LastIndexOf(pattern, StringComparison.Ordinal) && pattern.Length < TextDiffConstants.MatchMaxBits - patchMargin - patchMargin)
+		{
+			padding += patchMargin;
+			var begin = Math.Max(0, start2 - padding);
+			pattern = text[begin..Math.Min(text.Length, start2 + length1 + padding)];
+		}
+
+		padding += patchMargin;
+		var begin1 = Math.Max(0, start2 - padding);
+		var prefix = text[begin1..start2];
+		if (prefix.Length != 0)
+		{
+			diffListBuilder.Insert(0, TextDiffer.Equal(prefix));
+		}
+
+		var begin2 = start2 + length1;
+		var length = Math.Min(text.Length, start2 + length1 + padding) - begin2;
+		var suffix = text.Substring(begin2, length);
+		if (suffix.Length != 0)
+		{
+			diffListBuilder.Add(TextDiffer.Equal(suffix));
+		}
+
+		start1 -= prefix.Length;
+		start2 -= prefix.Length;
+		length1 = length1 + prefix.Length + suffix.Length;
+		length2 = length2 + prefix.Length + suffix.Length;
+		return (start1, length1, start2, length2);
+	}
+}

+ 139 - 0
Masuit.Tools.AspNetCore/TextDiff/Extensions.cs

@@ -0,0 +1,139 @@
+using System.Text;
+using System.Text.RegularExpressions;
+using AngleSharp.Text;
+using SixLabors.ImageSharp.Drawing;
+
+namespace Masuit.Tools.TextDiff;
+
+public static partial class Extensions
+{
+	internal static IEnumerable<T> Concat<T>(this IEnumerable<T> items, T item)
+	{
+		foreach (var i in items)
+		{
+			yield return i;
+		}
+
+		yield return item;
+	}
+
+	internal static IEnumerable<T> ItemAsEnumerable<T>(this T item)
+	{
+		yield return item;
+	}
+
+	internal static void Splice<T>(this List<T> input, int start, int count, params T[] objects) => input.Splice(start, count, (IEnumerable<T>)objects);
+
+	internal static void Splice<T>(this List<T> input, int start, int count, IEnumerable<T> objects)
+	{
+		input.RemoveRange(start, count);
+		input.InsertRange(start, objects);
+	}
+
+	internal static IEnumerable<string> SplitBy(this string s, char separator)
+	{
+		StringBuilder sb = new();
+		foreach (var c in s)
+		{
+			if (c == separator)
+			{
+				yield return sb.ToString();
+				sb.Clear();
+			}
+			else
+			{
+				sb.Append(c);
+			}
+		}
+
+		if (sb.Length > 0)
+		{
+			yield return sb.ToString();
+		}
+	}
+
+	public static (string html1, string html2) HtmlDiff(this string text1, string text2)
+	{
+		if (string.IsNullOrWhiteSpace(text1) || string.IsNullOrWhiteSpace(text2))
+		{
+			return (text1, text2);
+		}
+
+		var regex = new Regex(@"<pre[\s\S]*?</pre>|<[^>]+>");
+		const string sep = "\f";
+		var tags1 = regex.Matches(text1).Select(m => m.Value).Append("").ToArray();
+		var tags2 = regex.Matches(text2).Select(m => m.Value).Append("").ToArray();
+		var html1 = regex.Replace(text1, sep);
+		var html2 = regex.Replace(text2, sep);
+		var diffs = TextDiffer.Compute(html1, html2);
+		var s1 = diffs.Where(d => d.Operation != DiffOperation.Insert).Select(diff => diff.Operation == DiffOperation.Equal || string.IsNullOrWhiteSpace(diff.Text) ? diff.Text : diff.Text.Split(sep).Select(s => string.IsNullOrWhiteSpace(s) ? s : $"<del>{s}</del>").Join(sep)).Join("");
+		var s2 = diffs.Where(d => d.Operation != DiffOperation.Delete).Select(diff => diff.Operation == DiffOperation.Equal || string.IsNullOrWhiteSpace(diff.Text) ? diff.Text : diff.Text.Split(sep).Select(s => string.IsNullOrWhiteSpace(s) ? s : $"<ins>{s}</ins>").Join(sep)).Join("");
+		return (s1.Split(sep).Select((s, i) => s + tags1[i]).Join(""), s2.Split(sep).Select((s, i) => s + tags2[i]).Join(""));
+	}
+
+	public static string HtmlDiffMerge(this string text1, string text2)
+	{
+		if (string.IsNullOrWhiteSpace(text1))
+		{
+			return text2;
+		}
+
+		if (string.IsNullOrWhiteSpace(text2))
+		{
+			return text1;
+		}
+
+		var regex = new Regex(@"<pre[\s\S]*?</pre>|<[^>]+>");
+		const string sep = "\f";
+		var tags1 = regex.Matches(text1).Select(m => m.Value).Append("").ToQueue();
+		var tags2 = regex.Matches(text2).Select(m => m.Value).Append("").ToQueue();
+		var html1 = regex.Replace(text1, sep);
+		var html2 = regex.Replace(text2, sep);
+		var diffs = TextDiffer.Compute(html1, html2);
+		return diffs.Select(diff =>
+		{
+			switch (diff.Operation)
+			{
+				case DiffOperation.Equal:
+					{
+						var str = diff.Text;
+						foreach (Match m in Regex.Matches(str, sep))
+						{
+							tags1.Dequeue();
+							var tag = tags2.Dequeue();
+							str = str.ReplaceFirst(m.Value, tag);
+						}
+
+						return str;
+					}
+
+				case DiffOperation.Delete:
+					{
+						var str = diff.Text.Split(sep).Select(s => string.IsNullOrWhiteSpace(s) ? s : $"<del>{s}</del>").Join(sep);
+						foreach (Match m in Regex.Matches(str, sep))
+						{
+							var tag = tags1.Dequeue();
+							str = str.ReplaceFirst(m.Value, tag);
+						}
+
+						return str;
+					}
+
+				case DiffOperation.Insert:
+					{
+						var str = diff.Text.Split(sep).Select(s => string.IsNullOrWhiteSpace(s) ? s : $"<ins>{s}</ins>").Join(sep);
+						foreach (Match m in Regex.Matches(str, sep))
+						{
+							var tag = tags2.Dequeue();
+							str = str.ReplaceFirst(m.Value, tag);
+						}
+
+						return str;
+					}
+
+				default:
+					throw new ArgumentOutOfRangeException();
+			}
+		}).Join("");
+	}
+}

+ 14 - 0
Masuit.Tools.AspNetCore/TextDiff/HalfMatchResult.cs

@@ -0,0 +1,14 @@
+namespace Masuit.Tools.TextDiff;
+
+internal readonly record struct HalfMatchResult(string Prefix1, string Suffix1, string Prefix2, string Suffix2, string CommonMiddle)
+{
+    public bool IsEmpty => string.IsNullOrEmpty(CommonMiddle);
+
+    public static readonly HalfMatchResult Empty = new(string.Empty, string.Empty, string.Empty, string.Empty, string.Empty);
+
+    public static bool operator >(HalfMatchResult left, HalfMatchResult right) => left.CommonMiddle.Length > right.CommonMiddle.Length;
+
+    public static bool operator <(HalfMatchResult left, HalfMatchResult right) => left.CommonMiddle.Length < right.CommonMiddle.Length;
+    public static HalfMatchResult operator -(HalfMatchResult item) => new(item.Prefix2, item.Suffix2, item.Prefix1, item.Suffix1, item.CommonMiddle);
+    public override string ToString() => $"[{Prefix1}/{Prefix2}] - {CommonMiddle} - [{Suffix1}/{Suffix2}]";
+}

+ 5 - 0
Masuit.Tools.AspNetCore/TextDiff/IsExternalInit.cs

@@ -0,0 +1,5 @@
+namespace System.Runtime.CompilerServices;
+
+public class IsExternalInit
+{
+}

+ 12 - 0
Masuit.Tools.AspNetCore/TextDiff/MatchOption.cs

@@ -0,0 +1,12 @@
+namespace Masuit.Tools.TextDiff;
+
+/// <summary>
+///
+/// </summary>
+/// <param name="MatchThreshold">匹配阈值 (0.0 = 最好, 1.0 = 最差).</param>
+/// <param name="MatchDistance"> 搜索匹配的距离(0=精确位置,1000以上=广泛匹配) (0.0 为完美匹配).
+/// </param>
+public readonly record struct MatchOption(float MatchThreshold, int MatchDistance)
+{
+	public static MatchOption Default { get; } = new(0.5f, 1000);
+}

+ 341 - 0
Masuit.Tools.AspNetCore/TextDiff/PatchExtension.cs

@@ -0,0 +1,341 @@
+using System.Collections.Immutable;
+using System.Text;
+using System.Text.RegularExpressions;
+using static Masuit.Tools.TextDiff.DiffOperation;
+
+namespace Masuit.Tools.TextDiff;
+
+public static class PatchExtension
+{
+	internal static readonly string NullPadding = new(Enumerable.Range(1, 4).Select(i => (char)i).ToArray());
+
+	private static readonly Regex PatchHeader = new("^@@ -(\\d+),?(\\d*) \\+(\\d+),?(\\d*) @@$");
+
+	/// <summary>
+	/// 在文本的开始和结束处添加一些填充,以便边缘可以匹配某些内容。patch_apply内部调用
+	/// </summary>
+	/// <param name="patches"></param>
+	/// <param name="padding"></param>
+	/// <returns></returns>
+	internal static IEnumerable<DiffPatch> AddPadding(this IEnumerable<DiffPatch> patches, string padding)
+	{
+		var paddingLength = padding.Length;
+		using var enumerator = patches.GetEnumerator();
+		if (!enumerator.MoveNext())
+		{
+			yield break;
+		}
+
+		var current = enumerator.Current.Bump(paddingLength);
+		var next = current;
+		var isfirst = true;
+		while (true)
+		{
+			var hasnext = enumerator.MoveNext();
+			if (hasnext)
+			{
+				next = enumerator.Current.Bump(paddingLength);
+			}
+
+			yield return (isfirst, hasnext) switch
+			{
+				(true, false) => current.AddPadding(padding), // list has only one patch
+				(true, true) => current.AddPaddingBegin(padding),
+				(false, true) => current,
+				(false, false) => current.AddPaddingEnd(padding)
+			};
+
+			isfirst = false;
+			if (!hasnext) yield break;
+			current = next;
+		}
+	}
+
+	/// <summary>
+	/// 获取补丁列表并重建文本
+	/// </summary>
+	/// <param name="patches"></param>
+	/// <returns></returns>
+	public static string ToText(this IEnumerable<DiffPatch> patches) => patches.Aggregate(new StringBuilder(), (sb, patch) => sb.Append(patch)).ToString();
+
+	/// <summary>
+	/// 解析补丁的文本表示,并返回补丁对象列表
+	/// </summary>
+	/// <param name="text"></param>
+	/// <returns></returns>
+	public static ImmutableList<DiffPatch> ParsePatches(this string text) => ParseCore(text).ToImmutableList();
+
+	private static IEnumerable<DiffPatch> ParseCore(string text)
+	{
+		if (text.Length == 0)
+		{
+			yield break;
+		}
+
+		var lines = text.SplitBy('\n').ToArray();
+		var index = 0;
+		while (index < lines.Length)
+		{
+			var line = lines[index];
+			var m = PatchHeader.Match(line);
+			if (!m.Success)
+			{
+				throw new ArgumentException("Invalid patch string: " + line);
+			}
+
+			var (start1, length1) = m.GetStartAndLength(1, 2);
+			var (start2, length2) = m.GetStartAndLength(3, 4);
+			index++;
+			IEnumerable<TextDiffer> CreateDiffs()
+			{
+				while (index < lines.Length)
+				{
+					line = lines[index];
+					if (!string.IsNullOrEmpty(line))
+					{
+						var sign = line[0];
+						if (sign == '@')
+						{
+							break;
+						}
+
+						yield return sign switch
+						{
+							'+' => TextDiffer.Insert(line[1..].Replace("+", "%2b").UrlDecoded()),
+							'-' => TextDiffer.Delete(line[1..].Replace("+", "%2b").UrlDecoded()),
+							_ => TextDiffer.Equal(line[1..].Replace("+", "%2b").UrlDecoded())
+						};
+					}
+
+					index++;
+				}
+			}
+
+			yield return new DiffPatch(start1, length1, start2, length2, CreateDiffs().ToImmutableList());
+		}
+	}
+
+	private static (int start, int length) GetStartAndLength(this Match m, int startIndex, int lengthIndex)
+	{
+		var lengthStr = m.Groups[lengthIndex].Value;
+		var value = Convert.ToInt32(m.Groups[startIndex].Value);
+		return lengthStr switch
+		{
+			"0" => (value, 0),
+			"" => (value - 1, 1),
+			_ => (value - 1, Convert.ToInt32(lengthStr))
+		};
+	}
+
+	/// <summary>
+	/// 将一组补丁合并到文本上。返回一个补丁文本,以及一个指示应用了哪些补丁应用成功
+	/// </summary>
+	/// <param name="patches"></param>
+	/// <param name="text"></param>
+	/// <returns></returns>
+	public static (string newText, bool[] results) Apply(this IEnumerable<DiffPatch> patches, string text) => Apply(patches, text, MatchOption.Default, PatchOption.Default);
+
+	public static (string newText, bool[] results) Apply(this IEnumerable<DiffPatch> patches, string text, MatchOption matchOption) => Apply(patches, text, matchOption, PatchOption.Default);
+
+	/// <summary>
+	/// 将一组补丁合并到文本上。返回一个补丁文本,以及一个指示应用了哪些补丁应用成功
+	/// </summary>
+	/// <param name="input"></param>
+	/// <param name="text"></param>
+	/// <param name="matchOption"></param>
+	/// <param name="option"></param>
+	/// <returns></returns>
+	public static (string newText, bool[] results) Apply(this IEnumerable<DiffPatch> input, string text, MatchOption matchOption, PatchOption option)
+	{
+		if (!input.Any())
+		{
+			return (text, []);
+		}
+
+		var nullPadding = NullPadding;
+		text = nullPadding + text + nullPadding;
+		var patches = input.AddPadding(nullPadding).SplitMax().ToList();
+		var x = 0;
+		var delta = 0;
+		var results = new bool[patches.Count];
+		foreach (var aPatch in patches)
+		{
+			var expectedLoc = aPatch.Start2 + delta;
+			var text1 = aPatch.Diffs.Text1();
+			int startLoc;
+			var endLoc = -1;
+			if (text1.Length > TextDiffConstants.MatchMaxBits)
+			{
+				startLoc = text.FindBestMatchIndex(text1[..TextDiffConstants.MatchMaxBits], expectedLoc, matchOption);
+
+				if (startLoc != -1)
+				{
+					endLoc = text.FindBestMatchIndex(text1[^TextDiffConstants.MatchMaxBits..], expectedLoc + text1.Length - TextDiffConstants.MatchMaxBits, matchOption);
+					if (endLoc == -1 || startLoc >= endLoc)
+					{
+						startLoc = -1;
+					}
+				}
+			}
+			else
+			{
+				startLoc = text.FindBestMatchIndex(text1, expectedLoc, matchOption);
+			}
+
+			if (startLoc == -1)
+			{
+				results[x] = false;
+				delta -= aPatch.Length2 - aPatch.Length1;
+			}
+			else
+			{
+				results[x] = true;
+				delta = startLoc - expectedLoc;
+				var actualEndLoc = endLoc == -1 ? Math.Min(startLoc + text1.Length, text.Length) : Math.Min(endLoc + TextDiffConstants.MatchMaxBits, text.Length);
+				var text2 = text[startLoc..actualEndLoc];
+				if (text1 == text2)
+				{
+					text = text[..startLoc] + aPatch.Diffs.Text2() + text[(startLoc + text1.Length)..];
+				}
+				else
+				{
+					var diffs = TextDiffer.Compute(text1, text2, 0f, false);
+					if (text1.Length > TextDiffConstants.MatchMaxBits && diffs.Levenshtein() / (float)text1.Length > option.PatchDeleteThreshold)
+					{
+						results[x] = false;
+					}
+					else
+					{
+						diffs = diffs.CleanupSemanticLossless().ToImmutableList();
+						var index1 = 0;
+						foreach (var aDiff in aPatch.Diffs)
+						{
+							if (aDiff.Operation != Equal)
+							{
+								var index2 = diffs.FindEquivalentLocation2(index1);
+								if (aDiff.Operation == Insert)
+								{
+									text = text.Insert(startLoc + index2, aDiff.Text);
+								}
+								else if (aDiff.Operation == Delete)
+								{
+									text = text.Remove(startLoc + index2, diffs.FindEquivalentLocation2(index1 + aDiff.Text.Length) - index2);
+								}
+							}
+
+							if (aDiff.Operation != Delete)
+							{
+								index1 += aDiff.Text.Length;
+							}
+						}
+					}
+				}
+			}
+
+			x++;
+		}
+
+		text = text.Substring(nullPadding.Length, text.Length - 2 * nullPadding.Length);
+		return (text, results);
+	}
+
+	internal static IEnumerable<DiffPatch> SplitMax(this IEnumerable<DiffPatch> patches, short patchMargin = 4)
+	{
+		const short patchSize = TextDiffConstants.MatchMaxBits;
+		foreach (var patch in patches)
+		{
+			if (patch.Length1 <= patchSize)
+			{
+				yield return patch;
+				continue;
+			}
+
+			var (start1, _, start2, _, diffs) = patch;
+			var precontext = string.Empty;
+			while (diffs.Any())
+			{
+				var (s1, l1, s2, l2, thediffs) = (start1 - precontext.Length, precontext.Length, start2 - precontext.Length, precontext.Length, new List<TextDiffer>());
+				var empty = true;
+				if (precontext.Length != 0)
+				{
+					thediffs.Add(TextDiffer.Equal(precontext));
+				}
+
+				while (diffs.Any() && l1 < patchSize - patchMargin)
+				{
+					var first = diffs[0];
+					var diffType = diffs[0].Operation;
+					var diffText = diffs[0].Text;
+					if (first.Operation == Insert)
+					{
+						l2 += diffText.Length;
+						start2 += diffText.Length;
+						thediffs.Add(TextDiffer.Insert(diffText));
+						diffs = diffs.RemoveAt(0);
+						empty = false;
+					}
+					else if (first.IsLargeDelete(2 * patchSize) && thediffs.Count == 1 && thediffs[0].Operation == Equal)
+					{
+						l1 += diffText.Length;
+						start1 += diffText.Length;
+						thediffs.Add(TextDiffer.Delete(diffText));
+						diffs = diffs.RemoveAt(0);
+						empty = false;
+					}
+					else
+					{
+						var cutoff = diffText[..Math.Min(diffText.Length, patchSize - l1 - patchMargin)];
+						l1 += cutoff.Length;
+						start1 += cutoff.Length;
+						if (diffType == Equal)
+						{
+							l2 += cutoff.Length;
+							start2 += cutoff.Length;
+						}
+						else
+						{
+							empty = false;
+						}
+
+						thediffs.Add(TextDiffer.Create(diffType, cutoff));
+						if (cutoff == first.Text)
+						{
+							diffs = diffs.RemoveAt(0);
+						}
+						else
+						{
+							diffs = diffs.RemoveAt(0).Insert(0, first with
+							{
+								Text = first.Text[cutoff.Length..]
+							});
+						}
+					}
+				}
+
+				precontext = thediffs.Text2();
+				precontext = precontext[Math.Max(0, precontext.Length - patchMargin)..];
+				var text1 = diffs.Text1();
+				var postcontext = text1.Length > patchMargin ? text1[..patchMargin] : text1;
+				if (postcontext.Length != 0)
+				{
+					l1 += postcontext.Length;
+					l2 += postcontext.Length;
+					var lastDiff = thediffs.Last();
+					if (thediffs.Count > 0 && lastDiff.Operation == Equal)
+					{
+						thediffs[^1] = lastDiff.Append(postcontext);
+					}
+					else
+					{
+						thediffs.Add(TextDiffer.Equal(postcontext));
+					}
+				}
+
+				if (!empty)
+				{
+					yield return new DiffPatch(s1, l1, s2, l2, thediffs.ToImmutableList());
+				}
+			}
+		}
+	}
+}

+ 6 - 0
Masuit.Tools.AspNetCore/TextDiff/PatchOption.cs

@@ -0,0 +1,6 @@
+namespace Masuit.Tools.TextDiff;
+
+public readonly record struct PatchOption(float PatchDeleteThreshold, short PatchMargin)
+{
+	public static PatchOption Default { get; } = new(0.5f, 4);
+}

+ 78 - 0
Masuit.Tools.AspNetCore/TextDiff/SemanticsImmutableList.cs

@@ -0,0 +1,78 @@
+using System.Collections;
+using System.Collections.Immutable;
+
+namespace Masuit.Tools.TextDiff;
+
+public class SemanticsImmutableList<T>(ImmutableList<T> list) : IImmutableList<T>, IEquatable<IImmutableList<T>>
+{
+	#region IImutableList implementation
+
+	public T this[int index] => list[index];
+
+	public int Count => list.Count;
+
+	public IImmutableList<T> Add(T value) => list.Add(value).WithValueSemantics();
+
+	public IImmutableList<T> AddRange(IEnumerable<T> items) => list.AddRange(items).WithValueSemantics();
+
+	public IImmutableList<T> Clear() => list.Clear().WithValueSemantics();
+
+	public IEnumerator<T> GetEnumerator() => list.GetEnumerator();
+
+	public int IndexOf(T item, int index, int count, IEqualityComparer<T>? equalityComparer) => list.IndexOf(item, index, count, equalityComparer);
+
+	IImmutableList<T> IImmutableList<T>.Insert(int index, T element) => list.Insert(index, element).WithValueSemantics();
+
+	public SemanticsImmutableList<T> Insert(int index, T element) => list.Insert(index, element).WithValueSemantics();
+
+	public IImmutableList<T> InsertRange(int index, IEnumerable<T> items) => list.InsertRange(index, items).WithValueSemantics();
+
+	public int LastIndexOf(T item, int index, int count, IEqualityComparer<T>? equalityComparer) => list.LastIndexOf(item, index, count, equalityComparer);
+
+	public IImmutableList<T> Remove(T value, IEqualityComparer<T>? equalityComparer) => list.Remove(value, equalityComparer).WithValueSemantics();
+
+	public IImmutableList<T> RemoveAll(Predicate<T> match) => list.RemoveAll(match).WithValueSemantics();
+
+	IImmutableList<T> IImmutableList<T>.RemoveAt(int index) => list.RemoveAt(index).WithValueSemantics();
+
+	public SemanticsImmutableList<T> RemoveAt(int index) => list.RemoveAt(index).WithValueSemantics();
+
+	public IImmutableList<T> RemoveRange(IEnumerable<T> items, IEqualityComparer<T>? equalityComparer) => list.RemoveRange(items, equalityComparer).WithValueSemantics();
+
+	public IImmutableList<T> RemoveRange(int index, int count) => list.RemoveRange(index, count).WithValueSemantics();
+
+	public IImmutableList<T> Replace(T oldValue, T newValue, IEqualityComparer<T>? equalityComparer) => list.Replace(oldValue, newValue, equalityComparer).WithValueSemantics();
+
+	public IImmutableList<T> SetItem(int index, T value) => list.SetItem(index, value);
+
+	IEnumerator IEnumerable.GetEnumerator() => list.GetEnumerator();
+
+	#endregion IImutableList implementation
+
+	public ImmutableList<T>.Builder ToBuilder() => list.ToBuilder();
+
+	public bool IsEmpty => list.IsEmpty;
+
+	public override bool Equals(object obj) => Equals(obj as IImmutableList<T>);
+
+	public bool Equals(IImmutableList<T>? other) => this.SequenceEqual(other ?? ImmutableList<T>.Empty);
+
+	public override int GetHashCode()
+	{
+		unchecked
+		{
+			return this.Aggregate(19, (h, i) => h * 19 + i?.GetHashCode() ?? 0);
+		}
+	}
+
+	public static implicit operator SemanticsImmutableList<T>(ImmutableList<T> list) => list.WithValueSemantics();
+
+	public static bool operator ==(SemanticsImmutableList<T> left, SemanticsImmutableList<T> right) => left.Equals(right);
+
+	public static bool operator !=(SemanticsImmutableList<T> left, SemanticsImmutableList<T> right) => !left.Equals(right);
+}
+
+internal static class Ex
+{
+	public static SemanticsImmutableList<T> WithValueSemantics<T>(this ImmutableList<T> list) => new(list);
+}

+ 51 - 0
Masuit.Tools.AspNetCore/TextDiff/StringCompressor.cs

@@ -0,0 +1,51 @@
+using System.Text;
+
+namespace Masuit.Tools.TextDiff;
+
+internal class StringCompressor
+{
+	private readonly List<string> _lineArray = [];
+	private readonly Dictionary<string, char> _lineHash = new();
+
+	private char this[string line] => _lineHash[line];
+
+	private string this[int c] => _lineArray[c];
+
+	/// <summary>
+	/// 将文本的所有行压缩为一系列索引 (starting at \u0001, ending at (char)text.Length)
+	/// </summary>
+	/// <param name="text"></param>
+	/// <param name="maxLines"></param>
+	/// <returns></returns>
+	public string Compress(ReadOnlySpan<char> text, int maxLines = char.MaxValue) => Encode(text, maxLines);
+
+	public string Decompress(string text) => text.Aggregate(new StringBuilder(), (sb, c) => sb.Append(this[c])).Append(text.Length == char.MaxValue ? this[char.MaxValue] : "").ToString();
+
+	private string Encode(ReadOnlySpan<char> text, int maxLines)
+	{
+		var sb = new StringBuilder();
+		var start = 0;
+		var end = -1;
+		while (end < text.Length - 1)
+		{
+			var i = text[start..].IndexOf('\n');
+			end = _lineArray.Count == maxLines || i == -1 ? text.Length - 1 : i + start;
+			var line = text[start..(end + 1)].ToString();
+			EnsureHashed(line);
+			sb.Append(this[line]);
+			start = end + 1;
+		}
+
+		return sb.ToString();
+	}
+
+	// e.g. _lineArray[4] == "Hello\n"
+	// e.g. _lineHash["Hello\n"] == 4
+
+	private void EnsureHashed(string line)
+	{
+		if (_lineHash.ContainsKey(line)) return;
+		_lineArray.Add(line);
+		_lineHash.Add(line, (char)(_lineArray.Count - 1));
+	}
+}

+ 6 - 0
Masuit.Tools.AspNetCore/TextDiff/TextDiffConstants.cs

@@ -0,0 +1,6 @@
+namespace Masuit.Tools.TextDiff;
+
+internal static class TextDiffConstants
+{
+	public const short MatchMaxBits = 32;
+}

+ 44 - 0
Masuit.Tools.AspNetCore/TextDiff/TextDiffer.cs

@@ -0,0 +1,44 @@
+using System.Collections.Immutable;
+
+namespace Masuit.Tools.TextDiff;
+
+public readonly record struct TextDiffer(DiffOperation Operation, string Text)
+{
+	public static TextDiffer Empty => new(DiffOperation.Equal, string.Empty);
+	public bool IsEmpty => Text.Length == 0;
+	public static TextDiffer Equal(ReadOnlySpan<char> text) => Create(DiffOperation.Equal, text.ToString());
+	public static TextDiffer Insert(ReadOnlySpan<char> text) => Create(DiffOperation.Insert, text.ToString());
+	public static TextDiffer Delete(ReadOnlySpan<char> text) => Create(DiffOperation.Delete, text.ToString());
+	internal static TextDiffer Create(DiffOperation operation, string text) => new(operation, text);
+
+	internal TextDiffer Replace(string text) => this with { Text = text };
+	internal TextDiffer Append(string text) => this with { Text = Text + text };
+	internal TextDiffer Prepend(string text) => this with { Text = text + Text };
+
+	/// <summary>
+	/// 比较两段文本并生成差异信息
+	/// </summary>
+	/// <param name="text1"></param>
+	/// <param name="text2"></param>
+	/// <param name="timeoutInSeconds">比较超时时间,单位:秒,0表示不超时</param>
+	/// <param name="checklines">如果为false,则不要先运行行级差异来识别更改的区域。如果为true,则运行一个速度稍快但不太理想的差异</param>
+	/// <returns></returns>
+	public static ImmutableList<TextDiffer> Compute(string text1, string text2, float timeoutInSeconds = 0f, bool checklines = true)
+	{
+		using var cts = timeoutInSeconds <= 0
+			? new CancellationTokenSource()
+			: new CancellationTokenSource(TimeSpan.FromSeconds(timeoutInSeconds));
+		return Compute(text1, text2, checklines, timeoutInSeconds > 0, cts.Token);
+	}
+
+	public static ImmutableList<TextDiffer> Compute(string text1, string text2, bool checkLines, bool optimizeForSpeed, CancellationToken token)
+		=> DiffAlgorithm.Compute(text1, text2, checkLines, optimizeForSpeed, token).ToImmutableList();
+
+	public bool IsLargeDelete(int size) => Operation == DiffOperation.Delete && Text.Length > size;
+
+	public override string ToString()
+	{
+		var prettyText = Text.Replace('\n', '\u00b6');
+		return "Diff(" + Operation + ",\"" + prettyText + "\")";
+	}
+}

+ 238 - 0
Masuit.Tools.AspNetCore/TextDiff/TextUtil.cs

@@ -0,0 +1,238 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace Masuit.Tools.TextDiff;
+
+internal static class TextUtil
+{
+	private static readonly Regex HexCode = new("%[0-9A-F][0-9A-F]");
+
+	/// <summary>
+	/// 求两个字符串的最长公共前子串长度
+	/// </summary>
+	/// <param name="text1"></param>
+	/// <param name="text2"></param>
+	/// <param name="i1">text1子字符串的起始索引</param>
+	/// <param name="i2">text2子字符串的起始索引</param>
+	/// <returns>每个字符串开头共有的字符数</returns>
+	internal static int CommonPrefix(ReadOnlySpan<char> text1, ReadOnlySpan<char> text2, int i1 = 0, int i2 = 0)
+	{
+		var l1 = text1.Length - i1;
+		var l2 = text2.Length - i2;
+		var n = Math.Min(l1, l2);
+		for (var i = 0; i < n; i++)
+		{
+			if (text1[i + i1] != text2[i + i2])
+			{
+				return i;
+			}
+		}
+
+		return n;
+	}
+
+	internal static int CommonPrefix(StringBuilder text1, StringBuilder text2)
+	{
+		var n = Math.Min(text1.Length, text2.Length);
+		for (var i = 0; i < n; i++)
+		{
+			if (text1[i] != text2[i])
+			{
+				return i;
+			}
+		}
+
+		return n;
+	}
+
+	/// <summary>
+	/// 求两个字符串的最长公共后子串长度
+	/// </summary>
+	/// <param name="text1"></param>
+	/// <param name="text2"></param>
+	/// <param name="l1">text1的最大长度</param>
+	/// <param name="l2">text2的最大长度</param>
+	/// <returns>每个字符串末尾共有的字符数</returns>
+	internal static int CommonSuffix(ReadOnlySpan<char> text1, ReadOnlySpan<char> text2, int? l1 = null, int? l2 = null)
+	{
+		var text1Length = l1 ?? text1.Length;
+		var text2Length = l2 ?? text2.Length;
+		var n = Math.Min(text1Length, text2Length);
+		for (var i = 1; i <= n; i++)
+		{
+			if (text1[text1Length - i] != text2[text2Length - i])
+			{
+				return i - 1;
+			}
+		}
+
+		return n;
+	}
+
+	internal static int CommonSuffix(StringBuilder text1, StringBuilder text2)
+	{
+		var text1Length = text1.Length;
+		var text2Length = text2.Length;
+		var n = Math.Min(text1Length, text2Length);
+		for (var i = 1; i <= n; i++)
+		{
+			if (text1[text1Length - i] != text2[text2Length - i])
+			{
+				return i - 1;
+			}
+		}
+
+		return n;
+	}
+
+	/// <summary>
+	/// 确定一个字符串的后缀是否是另一个字符串。返回第一个字符串末尾和第二个字符串开头共有的字符数。
+	/// </summary>
+	/// <param name="text1"></param>
+	/// <param name="text2"></param>
+	/// <returns></returns>
+	internal static int CommonOverlap(ReadOnlySpan<char> text1, ReadOnlySpan<char> text2)
+	{
+		var text1Length = text1.Length;
+		var text2Length = text2.Length;
+		if (text1Length == 0 || text2Length == 0)
+		{
+			return 0;
+		}
+
+		if (text1Length > text2Length)
+		{
+			text1 = text1[(text1Length - text2Length)..];
+		}
+		else if (text1Length < text2Length)
+		{
+			text2 = text2[..text1Length];
+		}
+
+		var last = text1[^1];
+		for (var length = text2.Length; length > 0; length--)
+		{
+			if (text2[length - 1] == last && text1.EndsWith(text2[..length]))
+			{
+				return length;
+			}
+		}
+
+		return 0;
+	}
+
+	/// <summary>
+	/// 长文本中是否存在短文本的子字符串,使得子字符串至少是长文本长度的一半
+	/// </summary>
+	/// <param name="longtext"></param>
+	/// <param name="shorttext"></param>
+	/// <param name="i">在长文本内开始四分之一长度的子字符串索引位置</param>
+	/// <returns></returns>
+	private static HalfMatchResult HalfMatchI(ReadOnlySpan<char> longtext, ReadOnlySpan<char> shorttext, int i)
+	{
+		var seed = longtext.Slice(i, longtext.Length / 4);
+		var j = -1;
+
+		var bestCommon = string.Empty;
+		string bestLongtextA = string.Empty, bestLongtextB = string.Empty;
+		string bestShorttextA = string.Empty, bestShorttextB = string.Empty;
+
+		int n = j;
+		while (n < shorttext.Length && (j = shorttext[(j + 1)..].IndexOf(seed, StringComparison.Ordinal)) != -1)
+		{
+			j = n = j + n + 1;
+			var prefixLength = CommonPrefix(longtext, shorttext, i, j);
+			var suffixLength = CommonSuffix(longtext, shorttext, i, j);
+			if (bestCommon.Length < suffixLength + prefixLength)
+			{
+				bestCommon = shorttext.Slice(j - suffixLength, suffixLength).ToString() + shorttext.Slice(j, prefixLength).ToString();
+				bestLongtextA = longtext[..(i - suffixLength)].ToString();
+				bestLongtextB = longtext[(i + prefixLength)..].ToString();
+				bestShorttextA = shorttext[..(j - suffixLength)].ToString();
+				bestShorttextB = shorttext[(j + prefixLength)..].ToString();
+			}
+		}
+
+		return bestCommon.Length * 2 >= longtext.Length ? new(bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB, bestCommon) : HalfMatchResult.Empty;
+	}
+
+	/// <summary>
+	/// 这两个文本是否共享一个子字符串,子字符串的长度至少是较长文本的一半?
+	/// 这种加速会产生非最小的差异。
+	/// </summary>
+	/// <param name="text1"></param>
+	/// <param name="text2"></param>
+	/// <returns></returns>
+	internal static HalfMatchResult HalfMatch(ReadOnlySpan<char> text1, ReadOnlySpan<char> text2)
+	{
+		var longtext = text1.Length > text2.Length ? text1 : text2;
+		var shorttext = text1.Length > text2.Length ? text2 : text1;
+		if (longtext.Length < 4 || shorttext.Length * 2 < longtext.Length)
+		{
+			return HalfMatchResult.Empty;
+		}
+
+		var hm1 = HalfMatchI(longtext, shorttext, (longtext.Length + 3) / 4);
+		var hm2 = HalfMatchI(longtext, shorttext, (longtext.Length + 1) / 2);
+		var hm = (hm1, hm2) switch
+		{
+			{ hm1.IsEmpty: true } and { hm2.IsEmpty: true } => hm1,
+			{ hm2.IsEmpty: true } => hm1,
+			{ hm1.IsEmpty: true } => hm2,
+			_ when hm1 > hm2 => hm1,
+			_ => hm2
+		};
+
+		return text1.Length > text2.Length ? hm : -hm;
+	}
+
+	internal static string UrlEncoded(this string str)
+	{
+		const int maxLength = 0xFFEF;
+		StringBuilder sb = new();
+		var index = 0;
+		while (index + maxLength < str.Length)
+		{
+			sb.Append(Uri.EscapeDataString(str.Substring(index, maxLength)));
+			index += maxLength;
+		}
+
+		sb.Append(Uri.EscapeDataString(str[index..]));
+		sb = sb.Replace('+', ' ').Replace("%20", " ").Replace("%21", "!").Replace("%2A", "*").Replace("%27", "'").Replace("%28", "(").Replace("%29", ")").Replace("%3B", ";").Replace("%2F", "/").Replace("%3F", "?").Replace("%3A", ":").Replace("%40", "@").Replace("%26", "&").Replace("%3D", "=").Replace("%2B", "+").Replace("%24", "$").Replace("%2C", ",").Replace("%23", "#");
+		return HexCode.Replace(sb.ToString(), s => s.Value.ToLower());
+	}
+
+	internal static string UrlDecoded(this string str) => Uri.UnescapeDataString(str);
+
+	/// <summary>
+	/// 查找最匹配的索引位置
+	/// 返回 -1 则未匹配到
+	/// </summary>
+	/// <param name="text"></param>
+	/// <param name="pattern"></param>
+	/// <param name="loc"></param>
+	/// <param name="option"></param>
+	/// <returns></returns>
+	internal static int FindBestMatchIndex(this string text, string pattern, int loc, MatchOption option)
+	{
+		loc = Math.Max(0, Math.Min(loc, text.Length));
+
+		if (text == pattern)
+		{
+			return 0;
+		}
+
+		if (text.Length == 0)
+		{
+			return -1;
+		}
+
+		if (loc + pattern.Length <= text.Length && text.AsSpan(loc, pattern.Length).SequenceEqual(pattern))
+		{
+			return loc;
+		}
+
+		var bitap = new BitapAlgorithm(option);
+		return bitap.Match(text, pattern, loc);
+	}
+}

+ 1 - 1
Masuit.Tools/Masuit.Tools.csproj

@@ -209,7 +209,7 @@
       <Version>1.8.0</Version>
     </PackageReference>
     <PackageReference Include="HtmlSanitizer">
-      <Version>8.0.865</Version>
+      <Version>8.1.870</Version>
     </PackageReference>
     <PackageReference Include="Microsoft.AspNet.Mvc">
       <Version>5.3.0</Version>

+ 1 - 1
NetCoreTest/NetCoreTest.csproj

@@ -6,7 +6,7 @@
     <ConcurrentGarbageCollection>false</ConcurrentGarbageCollection>
   </PropertyGroup>
   <ItemGroup>
-    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.0" />
   </ItemGroup>
   <ItemGroup>
     <Folder Include="Controllers\" />

+ 41 - 1
README.md

@@ -945,6 +945,9 @@ var type=DefaultMimeItems.Items.FirstOrDefault(t=>t.Extension=="jpg"); // image/
 ### 32.日期时间扩展
 
 ```csharp
+var weeks=DateTime.Now.GetWeekAmount(); // 获取当前所在年一共有多少周
+var week = DateTime.Now.WeekOfYear(); // 获取当前所在年的第几周
+var week = DateTime.Now.WeekOfYear(DayOfWeek.Monday); // 获取当前所在年的第几周,并指定星期几是每周第一天
 
 double milliseconds = DateTime.Now.GetTotalMilliseconds();// 获取毫秒级时间戳
 double microseconds = DateTime.Now.GetTotalMicroseconds();// 获取微秒级时间戳
@@ -964,6 +967,24 @@ range.Contains(DateTime.Parse("2020-8-3"), DateTime.Parse("2020-8-4"));//判断
 
 range.GetUnionSet(List<DateTimeRange>); // 根据某个时间段查找在某批时间段中的最大并集
 range.GetMaxTimePeriod(List<DateTimeRange>); // 获取一批时间段内存在相互重叠的最大时间段
+
+var range = DateTime.Now.GetCurrentWeek(); // 获取当前时间所在周的时间区间:2024-08-05 00:00:00~2024-08-11 23:59:59
+var range = DateTime.Now.GetCurrentMonth(); // 获取当前时间所在月的时间区间:2024-08-01 00:00:00~2024-08-31 23:59:59
+var range = DateTime.Now.GetCurrentYear(); // 获取当前时间所在年的时间区间:2024-01-01 00:00:00~2024-12-31 23:59:59
+var range = DateTime.Now.GetCurrentQuarter(); // 获取当前时间所在季度的时间区间:2024-07-01 00:00:00~2024-09-30 23:59:59
+var range = DateTime.Now.GetCurrentLunarMonth(); // 获取当前时间所在农历月的时间区间:2024-08-04 00:00:00~2024-09-02 23:59:59
+var range = DateTime.Now.GetCurrentLunarQuarter(); // 获取当前时间所在农历季度的时间区间:2024-08-04 00:00:00~2024-10-31 23:59:59
+var range = DateTime.Now.GetCurrentLunarYaer(); // 获取当前时间所在农历年的时间区间:2024-02-10 00:00:00~2025-01-28 23:59:59
+var range = DateTime.Now.GetCurrentSolar(); // 获取当前时间所在季节的时间区间:2024-08-07 00:00:00~2024-11-06 23:59:59
+var range = DateTime.Now.GetCurrentRange(DateRangeType.Week); // 获取当前时间所在周的时间区间:2024-08-05 00:00:00~2024-08-11 23:59:59
+var range = DateTime.Now.GetCurrentRange(DateRangeType.Month); // 获取当前时间所在月的时间区间:2024-08-01 00:00:00~2024-08-31 23:59:59
+var range = DateTime.Now.GetCurrentRange(DateRangeType.Quarter); // 获取当前时间所在季度的时间区间:2024-07-01 00:00:00~2024-09-30 23:59:59
+var range = DateTime.Now.GetCurrentRange(DateRangeType.Year); // 获取当前时间所在年的时间区间:2024-01-01 00:00:00~2024-12-31 23:59:59
+var range = DateTime.Now.GetCurrentRange(DateRangeType.LunarMonth); // 获取当前时间所在农历月的时间区间:2024-08-04 00:00:00~2024-09-02 23:59:59
+var range = DateTime.Now.GetCurrentRange(DateRangeType.LunarQuarter); // 获取当前时间所在农历季度的时间区间:2024-08-04 00:00:00~2024-10-31 23:59:59
+var range = DateTime.Now.GetCurrentRange(DateRangeType.LunarYear); // 获取当前时间所在农历年的时间区间:2024-02-10 00:00:00~2025-01-28 23:59:59
+var range = DateTime.Now.GetCurrentRange(DateRangeType.Solar); // 获取当前时间所在季节的时间区间:2024-08-07 00:00:00~2024-11-06 23:59:59
+
 ...
 ```
 
@@ -1493,6 +1514,7 @@ var item=CurrentContext.GetData<T>();//获取值
 ```
 
 ### 51. ASP.NET Core自动扫描注册服务
+包:Masuit.Tools.AspNetCore  
 
 ```csharp
 // 自动扫描注册服务
@@ -1506,7 +1528,25 @@ public class MyClass:MyInterface{...}
 public class MyService{...}
 ```
 
-### 52. 房贷试算模型
+### 52. 文本对比(支持html和纯文本)
+包:Masuit.Tools.AspNetCore  
+集成案例:https://masuit.org/1889/history
+```csharp
+var text1 = "<h1>你好 UEditorPlus</h1><p>UEditorPlus 是基于 UEditor 二次开发的富文本编辑器,让 UEditor <span style=\"color: #E36C09;\">焕<span style=\"color: #0070C0;\">然</span><span style=\"color: #31859B;\"><span style=\"color: #00B050;\">一</span><span style=\"color: #FF0000;\">新</span></span></span></p><table data-sort=\"sortDisabled\"><tbody><tr class=\"firstRow\"><td valign=\"top\" style=\"word-break: break-all;\" rowspan=\"1\" colspan=\"3\">我是表格</td></tr><tr><td width=\"273\" valign=\"top\" style=\"word-break: break-all;\">如果</td><td width=\"273\" valign=\"top\" style=\"word-break: break-all;\">有一天</td><td width=\"273\" valign=\"top\" style=\"word-break: break-all;\">我离开了</td></tr><tr><td valign=\"top\" colspan=\"1\" rowspan=\"1\" style=\"word-break: break-all;\">怎么才能</td><td valign=\"top\" colspan=\"1\" rowspan=\"1\" style=\"word-break: break-all;\">证明我</td><td valign=\"top\" colspan=\"1\" rowspan=\"1\" style=\"word-break: break-all;\">曾经来过</td></tr></tbody></table><h2>公式支持</h2><p><img src=\"https://r.latexeasy.com/image.svg?%5Cint%20%5Cfrac%7B1%7D%7Bx%7D%20dx%20%3D%20%5Cln%20%5Cleft%7C%20x%20%5Cright%7C%20%2B%20C\" data-formula-image=\"%5Cint%20%5Cfrac%7B1%7D%7Bx%7D%20dx%20%3D%20%5Cln%20%5Cleft%7C%20x%20%5Cright%7C%20%2B%20C\"/></p><p><br/></p>";
+var text2 = "<p>UEditorPlus 是基于 UEditor 二次开发的富文本编辑器,让 UEditor <span style=\"color: #E36C09;\">焕<p style=\"color: #0070C0;\">然</p><span style=\"color: #31859B;\"><span style=\"color: #00B050;\">一</span><span style=\"color: #FF0000;\">新</span></span></span></p><table data-sort=\"sortDisabled\"><tbody><tr class=\"firstRow\"><td valign=\"top\" style=\"word-break: break-all;\" rowspan=\"1\" colspan=\"3\">我是表格</td></tr><tr><td width=\"273\" valign=\"top\" style=\"word-break: break-all;\">如果</td><td width=\"273\" valign=\"top\" style=\"word-break: break-all;\">有一天</td><td width=\"273\" valign=\"top\" style=\"word-break: break-all;\">我离开了</td></tr><tr><td valign=\"top\" colspan=\"1\" rowspan=\"1\" style=\"word-break: break-all;\">怎么才能</td><td valign=\"top\" colspan=\"1\" rowspan=\"1\" style=\"word-break: break-all;\">证明我</td><td valign=\"top\" colspan=\"1\" rowspan=\"1\" style=\"word-break: break-all;\">曾经来过</td></tr></tbody></table><pre class=\"brush:html;toolbar:false\">&lt;div&gt;\r\n&nbsp;&nbsp;&lt;span&gt;这里是HTML标签&lt;/span&gt;\r\n&lt;/div&gt;</pre><h2>公式支持23333333</h2><p><img src=\"https://r.latexeasy.com/image.svg?%5Cint%20%5Cfrac%7B1%7D%7Bx%7D%20dx%20%3D%20%5Cln%20%5Cleft%7C%20x%20%5Cright%7C%20%2B%20C\" data-formula-image=\"%5Cint%20%5Cfrac%7B1%7D%7Bx%7D%20dx%20%3D%20%5Cln%20%5Cleft%7C%20x%20%5Cright%7C%20%2B%20C\"/></p><p><br/></p>";
+
+var (html1, html2) = text1.HtmlDiff(text2); // 对比两段文本并分别生成差异
+var diffs = TextDiffer.Compute(text1, text2); // 对比两段文本并分别生成差异详细记录
+var patches = DiffPatch.FromDiffs(diffs); // 根据差异信息生成补丁
+patches.ToText(); // 根据补丁记录重建文本
+(string newText, bool[] results) = patches.Apply(text1); // 将变更补丁应用到原始文本1,并返回是否应用成功
+var text1 = diffs.Text1(); // 根据差异信息还原文本1
+var text2 = diffs.Text2(); // 根据差异信息还原文本2
+var delta = diffs.ToDelta(); // 根据差异信息生成类似于git差异的差分记录
+var diffs = text1.FromDelta(delta); // 根据差分信息生成差异记录
+```
+
+### 53. 房贷试算模型
 
 集成案例:https://masuit.org/tools/loan
 

+ 1 - 1
Test/Masuit.Tools.AspNetCore.ResumeFileResults.WebTest/Masuit.Tools.AspNetCore.ResumeFileResults.WebTest.csproj

@@ -23,7 +23,7 @@
   </ItemGroup>
 
   <ItemGroup>
-    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.6.2" />
+    <PackageReference Include="Swashbuckle.AspNetCore" Version="6.7.0" />
   </ItemGroup>
 
   <ItemGroup>

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä