2
0

StringUtils.cs 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. using System.Text.RegularExpressions;
  2. namespace MeloongCore;
  3. public static class StringUtils {
  4. #region 转义
  5. /// <summary>
  6. /// WPF XML 转义。
  7. /// </summary>
  8. public static string XmlEscape(string str) {
  9. if (str.StartsWithF("{")) str = "{}" + str; // #4187
  10. return str
  11. .Replace("&", "&amp;").Replace("<", "&lt;").Replace(">", "&gt;").Replace("'", "&apos;")
  12. .Replace("\"", "&quot;").ReplaceLineEndings("&#xa;");
  13. }
  14. /// <summary>
  15. /// VB.NET 的 Like 关键字转义。
  16. /// </summary>
  17. public static string LikePatternEscape(string input) {
  18. var builder = new StringBuilder();
  19. foreach (char c in input) {
  20. if (c is '[' or ']' or '*' or '?' or '#') {
  21. builder.Append('[').Append(c).Append(']');
  22. } else {
  23. builder.Append(c);
  24. }
  25. }
  26. return builder.ToString();
  27. }
  28. /// <summary>
  29. /// 正则表达式转义。
  30. /// 等同于 <see cref="Regex.Escape"/>。
  31. /// </summary>
  32. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  33. public static string RegexEscape(string value)
  34. => Regex.Escape(value);
  35. /// <summary>
  36. /// 正则表达式去转义。
  37. /// 等同于 <see cref="Regex.Unescape"/>。
  38. /// </summary>
  39. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  40. public static string RegexUnescape(string value)
  41. => Regex.Unescape(value);
  42. /// <summary>
  43. /// URL 转义。
  44. /// 等同于 <see cref="Uri.EscapeDataString"/>。
  45. /// </summary>
  46. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  47. public static string UrlEscape(string value)
  48. => Uri.EscapeDataString(value);
  49. /// <summary>
  50. /// URL 去转义。
  51. /// 等同于 <see cref="Uri.UnescapeDataString"/>。
  52. /// </summary>
  53. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  54. public static string UrlUnescape(string value)
  55. => Uri.UnescapeDataString(value);
  56. /// <summary>
  57. /// 表单转义。
  58. /// 等同于 <see cref="WebUtility.UrlEncode"/>。
  59. /// </summary>
  60. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  61. public static string FormUrlEscape(string value)
  62. => WebUtility.UrlEncode(value);
  63. /// <summary>
  64. /// 表单去转义。
  65. /// 等同于 <see cref="WebUtility.UrlDecode"/>。
  66. /// </summary>
  67. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  68. public static string FormUrlUnescape(string value)
  69. => WebUtility.UrlDecode(value);
  70. /// <summary>
  71. /// HTML 转义。
  72. /// 等同于 <see cref="WebUtility.HtmlEncode"/>。
  73. /// </summary>
  74. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  75. public static string HtmlEscape(string value)
  76. => WebUtility.HtmlEncode(value);
  77. /// <summary>
  78. /// HTML 去转义。
  79. /// 等同于 <see cref="WebUtility.HtmlDecode"/>。
  80. /// </summary>
  81. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  82. public static string HtmlUnescape(string value)
  83. => WebUtility.HtmlDecode(value);
  84. #endregion
  85. #region 格式化
  86. /// <summary>
  87. /// 将文件大小的字节数格式化为适合阅读的文本,如 <c>"1.28 M"</c>、<c>"128 G"</c>。
  88. /// </summary>
  89. public static string FormatByteSize(long byteSize) {
  90. var sign = byteSize < 0 ? "-" : "";
  91. if (byteSize < 0) byteSize *= -1;
  92. if (byteSize < 1000) { // B 级
  93. return $"{sign}{byteSize} B";
  94. } else if (byteSize < 1024 * 1000) { // K 级
  95. var roundResult = Math.Round(byteSize / 1024D).ToString();
  96. return $"{sign}{Math.Round(byteSize / 1024D, (int) (3 - roundResult.Length).Clamp(0, 2))} K";
  97. } else if (byteSize < 1024 * 1024 * 1000) { // M 级
  98. var roundResult = Math.Round(byteSize / 1024D / 1024D).ToString();
  99. return $"{sign}{Math.Round(byteSize / 1024D / 1024D, (int) (3 - roundResult.Length).Clamp(0, 2))} M";
  100. } else { // G 级
  101. var roundResult = Math.Round(byteSize / 1024D / 1024D / 1024D).ToString();
  102. return $"{sign}{Math.Round(byteSize / 1024D / 1024D / 1024D, (int) (3 - roundResult.Length).Clamp(0, 2))} G";
  103. }
  104. }
  105. /// <summary>
  106. /// 将时间间隔格式化为适合阅读的文本。
  107. /// <para/>若指定了 <paramref name="isShort"/>,则只使用一个单位,如 <c>"5 分钟前"</c>,否则有可能会使用两个单位,如 <c>"5 分 10 秒前"</c>。
  108. /// </summary>
  109. public static string FormatTimeSpan(TimeSpan span, bool isShort) {
  110. var suffix = span.TotalMilliseconds > 0 ? "后" : "前";
  111. if (span.TotalMilliseconds < 0) span = -span;
  112. var totalMonths = (int) Math.Floor(span.Days / 30D);
  113. if (isShort) {
  114. if (totalMonths >= 12) {
  115. // 1+ 年,“3 年”
  116. return $"{Math.Floor(totalMonths / 12D)} 年{suffix}";
  117. } else if (totalMonths >= 2) {
  118. // 2~11 月,“5 个月”
  119. return $"{totalMonths} 个月{suffix}";
  120. } else if (span.TotalDays >= 2) {
  121. // 2 天 ~ 2 月,“23 天”
  122. return $"{span.Days} 天{suffix}";
  123. } else if (span.TotalHours >= 1) {
  124. // 1 小时 ~ 2 天,“15 小时”
  125. return $"{span.Hours} 小时{suffix}";
  126. } else if (span.TotalMinutes >= 1) {
  127. // 1 分钟 ~ 1 小时,“49 分钟”
  128. return $"{span.Minutes} 分钟{suffix}";
  129. } else if (span.TotalSeconds >= 1) {
  130. // 1 秒 ~ 1 分钟,“23 秒”
  131. return $"{span.Seconds} 秒{suffix}";
  132. } else {
  133. // 不到 1 秒
  134. return $"1 秒{suffix}";
  135. }
  136. } else {
  137. if (totalMonths >= 61) {
  138. // 5+ 年,“5 年”
  139. return $"{Math.Floor(totalMonths / 12D)} 年{suffix}";
  140. } else if (totalMonths >= 12) {
  141. // 12~60 月,“1 年 2 个月”
  142. return $"{Math.Floor(totalMonths / 12D)} 年{(totalMonths % 12 > 0 ? $" {totalMonths % 12} 个月" : "")}{suffix}";
  143. } else if (totalMonths >= 4) {
  144. // 4~11 月,“5 个月”
  145. return $"{totalMonths} 个月{suffix}";
  146. } else if (totalMonths >= 1) {
  147. // 1~4 月,“2 个月 13 天”
  148. return $"{totalMonths} 月{(span.Days % 30 > 0 ? $" {span.Days % 30} 天" : "")}{suffix}";
  149. } else if (span.TotalDays >= 4) {
  150. // 4~30 天,“23 天”
  151. return $"{span.Days} 天{suffix}";
  152. } else if (span.TotalDays >= 1) {
  153. // 1~3 天,“2 天 20 小时”
  154. return $"{span.Days} 天{(span.Hours > 0 ? $" {span.Hours} 小时" : "")}{suffix}";
  155. } else if (span.TotalHours >= 10) {
  156. // 10 小时 ~ 1 天,“15 小时”
  157. return $"{span.Hours} 小时{suffix}";
  158. } else if (span.TotalHours >= 1) {
  159. // 1~10 小时,“1 小时 20 分钟”
  160. return $"{span.Hours} 小时{(span.Minutes > 0 ? $" {span.Minutes} 分钟" : "")}{suffix}";
  161. } else if (span.TotalMinutes >= 10) {
  162. // 10 分钟 ~ 1 小时,“49 分钟”
  163. return $"{span.Minutes} 分钟{suffix}";
  164. } else if (span.TotalMinutes >= 1) {
  165. // 1~10 分钟,“9 分 23 秒”
  166. return $"{span.Minutes} 分{(span.Seconds > 0 ? $" {span.Seconds} 秒" : "")}{suffix}";
  167. } else if (span.TotalSeconds >= 1) {
  168. // 1 秒 ~ 1 分钟,“23 秒”
  169. return $"{span.Seconds} 秒{suffix}";
  170. } else {
  171. // 不到 1 秒
  172. return $"1 秒{suffix}";
  173. }
  174. }
  175. }
  176. #endregion
  177. /// <summary>
  178. /// 将字符串解析为版本号。
  179. /// 允许缺少次要版本(如 "2" 代表 "2.0")。
  180. /// </summary>
  181. public static Version ToVersion(string value) {
  182. if (Version.TryParse(value.Contains('.') ? value : value + ".0", out var version)) {
  183. return version;
  184. } else {
  185. throw new FormatException($"无法将字符串 \"{value}\" 解析为版本号");
  186. }
  187. }
  188. }