OSDN Git Service

varを使用する (IDE0007)
[opentween/open-tween.git] / OpenTween / MyCommon.cs
index 8612189..32eda4c 100644 (file)
@@ -47,6 +47,8 @@ using System.Net.Http;
 using System.Net.NetworkInformation;
 using System.Runtime.InteropServices;
 using OpenTween.Api;
+using OpenTween.Models;
+using OpenTween.Setting;
 
 namespace OpenTween
 {
@@ -54,7 +56,6 @@ namespace OpenTween
     {
         private static readonly object LockObj = new object();
         public static bool _endingFlag;        //終了フラグ
-        public static string cultureStr = null;
         public static string settingPath;
 
         public enum IconSizes
@@ -103,7 +104,6 @@ namespace OpenTween
         {
             TinyUrl,
             Isgd,
-            Twurl,
             Bitly,
             Jmp,
             Uxnu,
@@ -111,6 +111,7 @@ namespace OpenTween
             Nicoms,
             //廃止
             Unu = -1,
+            Twurl = -1,
         }
 
         public enum HITRESULT
@@ -125,7 +126,7 @@ namespace OpenTween
         public enum HttpTimeOut
         {
             MinValue = 10,
-            MaxValue = 120,
+            MaxValue = 1000,
             DefaultValue = 20,
         }
 
@@ -160,6 +161,7 @@ namespace OpenTween
             public const string REPLY = "Reply";
             public const string DM = "Direct";
             public const string FAV = "Favorites";
+            public static readonly string MUTE = Properties.Resources.MuteTabName;
 
             //private string dummy;
 
@@ -195,7 +197,7 @@ namespace OpenTween
             BlinkIcon,
         }
 
-        [FlagsAttribute()]
+        [Flags]
         public enum EVENTTYPE
         {
             None = 0,
@@ -216,11 +218,13 @@ namespace OpenTween
             ListDestroyed = 16384,
             Mute = 32768,
             Unmute = 65536,
+            QuotedTweet = 131072,
+            Retweet = 262144,
 
             All = (None | Favorite | Unfavorite | Follow | ListMemberAdded | ListMemberRemoved |
                    Block | Unblock | UserUpdate | Deleted | ListCreated | ListUpdated | Unfollow |
                    ListUserSubscribed | ListUserUnsubscribed | ListDestroyed |
-                   Mute | Unmute),
+                   Mute | Unmute | QuotedTweet | Retweet),
         }
 
         public static _Assembly EntryAssembly { get; internal set; }
@@ -237,9 +241,7 @@ namespace OpenTween
         }
 
         public static string GetErrorLogPath()
-        {
-            return Path.Combine(Path.GetDirectoryName(MyCommon.EntryAssembly.Location), "ErrorLogs");
-        }
+            => Path.Combine(Path.GetDirectoryName(MyCommon.EntryAssembly.Location), "ErrorLogs");
 
         public static void TraceOut(WebApiException ex)
         {
@@ -258,9 +260,7 @@ namespace OpenTween
         }
 
         public static void TraceOut(string Message)
-        {
-            TraceOut(TraceFlag, Message);
-        }
+            => TraceOut(TraceFlag, Message);
 
         public static void TraceOut(bool OutputFlag, string Message)
         {
@@ -272,20 +272,20 @@ namespace OpenTween
                 if (!Directory.Exists(logPath))
                     Directory.CreateDirectory(logPath);
 
-                var now = DateTime.Now;
-                var fileName = string.Format("{0}Trace-{1:0000}{2:00}{3:00}-{4:00}{5:00}{6:00}.log", GetAssemblyName(), now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second);
+                var now = DateTimeUtc.Now;
+                var fileName = $"{ApplicationSettings.AssemblyName}Trace-{now.ToLocalTime():yyyyMMdd-HHmmss}.log";
                 fileName = Path.Combine(logPath, fileName);
 
                 using (var writer = new StreamWriter(fileName))
                 {
-                    writer.WriteLine("**** TraceOut: {0} ****", DateTime.Now.ToString());
+                    writer.WriteLine("**** TraceOut: {0} ****", now.ToLocalTimeString());
                     writer.WriteLine(Properties.Resources.TraceOutText1, ApplicationSettings.FeedbackEmailAddress);
                     writer.WriteLine(Properties.Resources.TraceOutText2, ApplicationSettings.FeedbackTwitterName);
                     writer.WriteLine();
                     writer.WriteLine(Properties.Resources.TraceOutText3);
                     writer.WriteLine(Properties.Resources.TraceOutText4, Environment.OSVersion.VersionString);
-                    writer.WriteLine(Properties.Resources.TraceOutText5, Environment.Version.ToString());
-                    writer.WriteLine(Properties.Resources.TraceOutText6, MyCommon.GetAssemblyName(), FileVersion);
+                    writer.WriteLine(Properties.Resources.TraceOutText5, Environment.Version);
+                    writer.WriteLine(Properties.Resources.TraceOutText6, ApplicationSettings.AssemblyName, FileVersion);
                     writer.WriteLine(Message);
                     writer.WriteLine();
                 }
@@ -299,7 +299,7 @@ namespace OpenTween
 
         public static string ExceptionOutMessage(Exception ex)
         {
-            bool IsTerminatePermission = true;
+            var IsTerminatePermission = true;
             return ExceptionOutMessage(ex, ref IsTerminatePermission);
         }
 
@@ -383,118 +383,73 @@ namespace OpenTween
             {
                 var IsTerminatePermission = true;
 
-                var logPath = MyCommon.GetErrorLogPath();
-                if (!Directory.Exists(logPath))
-                    Directory.CreateDirectory(logPath);
-
-                var now = DateTime.Now;
-                var fileName = string.Format("{0}-{1:0000}{2:00}{3:00}-{4:00}{5:00}{6:00}.log", GetAssemblyName(), now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second);
-                fileName = Path.Combine(logPath, fileName);
+                var ident = WindowsIdentity.GetCurrent();
+                var princ = new WindowsPrincipal(ident);
+                var now = DateTimeUtc.Now;
 
-                using (var writer = new StreamWriter(fileName))
-                {
-                    var ident = WindowsIdentity.GetCurrent();
-                    var princ = new WindowsPrincipal(ident);
+                var errorReport = string.Join(Environment.NewLine,
+                    string.Format(Properties.Resources.UnhandledExceptionText1, now.ToLocalTimeString()),
 
-                    writer.WriteLine(Properties.Resources.UnhandledExceptionText1, DateTime.Now.ToString());
-                    writer.WriteLine(Properties.Resources.UnhandledExceptionText2, ApplicationSettings.FeedbackEmailAddress);
-                    writer.WriteLine(Properties.Resources.UnhandledExceptionText3, ApplicationSettings.FeedbackTwitterName);
                     // 権限書き出し
-                    writer.WriteLine(Properties.Resources.UnhandledExceptionText11 + princ.IsInRole(WindowsBuiltInRole.Administrator).ToString());
-                    writer.WriteLine(Properties.Resources.UnhandledExceptionText12 + princ.IsInRole(WindowsBuiltInRole.User).ToString());
-                    writer.WriteLine();
+                    string.Format(Properties.Resources.UnhandledExceptionText11 + princ.IsInRole(WindowsBuiltInRole.Administrator)),
+                    string.Format(Properties.Resources.UnhandledExceptionText12 + princ.IsInRole(WindowsBuiltInRole.User)),
+                    "",
+
                     // OSVersion,AppVersion書き出し
-                    writer.WriteLine(Properties.Resources.UnhandledExceptionText4);
-                    writer.WriteLine(Properties.Resources.UnhandledExceptionText5, Environment.OSVersion.VersionString);
-                    writer.WriteLine(Properties.Resources.UnhandledExceptionText6, Environment.Version.ToString());
-                    writer.WriteLine(Properties.Resources.UnhandledExceptionText7, MyCommon.GetAssemblyName(), FileVersion);
+                    string.Format(Properties.Resources.UnhandledExceptionText4),
+                    string.Format(Properties.Resources.UnhandledExceptionText5, Environment.OSVersion.VersionString),
+                    string.Format(Properties.Resources.UnhandledExceptionText6, Environment.Version),
+                    string.Format(Properties.Resources.UnhandledExceptionText7, ApplicationSettings.AssemblyName, FileVersion),
 
-                    writer.Write(ExceptionOutMessage(ex, ref IsTerminatePermission));
-                    writer.Flush();
-                }
+                    ExceptionOutMessage(ex, ref IsTerminatePermission));
+
+                var logPath = MyCommon.GetErrorLogPath();
+                if (!Directory.Exists(logPath))
+                    Directory.CreateDirectory(logPath);
 
-                switch (MessageBox.Show(MyCommon.ReplaceAppName(string.Format(Properties.Resources.UnhandledExceptionText9, fileName, ApplicationSettings.FeedbackEmailAddress, ApplicationSettings.FeedbackTwitterName, Environment.NewLine)),
-                                   Properties.Resources.UnhandledExceptionText10, MessageBoxButtons.YesNoCancel, MessageBoxIcon.Error))
+                var fileName = $"{ApplicationSettings.AssemblyName}-{now.ToLocalTime():yyyyMMdd-HHmmss}.log";
+                using (var writer = new StreamWriter(Path.Combine(logPath, fileName)))
                 {
-                    case DialogResult.Yes:
-                        Process.Start(fileName);
-                        return false;
-                    case DialogResult.No:
-                        return false;
-                    case DialogResult.Cancel:
-                    default:
-                        return IsTerminatePermission;
+                    writer.Write(errorReport);
                 }
-            }
-        }
-
-        /// <summary>
-        /// URLに含まれているマルチバイト文字列を%xx形式でエンコードします。
-        /// <newpara>
-        /// マルチバイト文字のコードはUTF-8またはUnicodeで自動的に判断します。
-        /// </newpara>
-        /// </summary>
-        /// <param name="_input">エンコード対象のURL</param>
-        /// <returns>マルチバイト文字の部分をUTF-8/%xx形式でエンコードした文字列を返します。</returns>
 
-        public static string urlEncodeMultibyteChar(string _input)
-        {
-            Uri uri = null;
-            var sb = new StringBuilder(256);
-            var result = "";
-            var c_ = 'd';
-            foreach (var c in _input)
-            {
-                c_ = c;
-                if (Convert.ToInt32(c) > 127 || c == '%') break;
-            }
-            if (Convert.ToInt32(c_) <= 127 && c_ != '%') return _input;
+                var settings = SettingManager.Common;
+                var mainForm = Application.OpenForms.OfType<TweenMain>().FirstOrDefault();
 
-            var input = Uri.UnescapeDataString(_input);
-        retry:
-            foreach (char c in input)
-            {
-                if (Convert.ToInt32(c) > 255)
-                {
-                    // Unicodeの場合(1charが複数のバイトで構成されている)
-                    // Uriクラスをnewして再構成し、入力をPathAndQueryのみとしてやり直す
-                    foreach (var b in Encoding.UTF8.GetBytes(c.ToString()))
-                    {
-                        sb.AppendFormat("%{0:X2}", b);
-                    }
-                }
-                else if (Convert.ToInt32(c) > 127 || c == '%')
-                {
-                    // UTF-8の場合
-                    // Uriクラスをnewして再構成し、入力をinputからAuthority部分を除去してやり直す
-                    if (uri == null)
-                    {
-                        uri = new Uri(input);
-                        input = input.Remove(0, uri.GetLeftPart(UriPartial.Authority).Length);
-                        sb.Length = 0;
-                        goto retry;
-                    }
-                    else
-                    {
-                        sb.Append("%" + Convert.ToInt16(c).ToString("X2").ToUpper());
-                    }
-                }
+                ErrorReport report;
+                if (mainForm != null && !mainForm.IsDisposed)
+                    report = new ErrorReport(mainForm.TwitterInstance, errorReport);
                 else
+                    report = new ErrorReport(errorReport);
+
+                report.AnonymousReport = settings.ErrorReportAnonymous;
+
+                OpenErrorReportDialog(mainForm, report);
+
+                // ダイアログ内で設定が変更されていれば保存する
+                if (settings.ErrorReportAnonymous != report.AnonymousReport)
                 {
-                    sb.Append(c);
+                    settings.ErrorReportAnonymous = report.AnonymousReport;
+                    settings.Save();
                 }
+
+                return false;
             }
+        }
 
-            if (uri == null)
+        private static void OpenErrorReportDialog(Form owner, ErrorReport report)
+        {
+            if (owner != null && owner.InvokeRequired)
             {
-                result = sb.ToString();
+                owner.Invoke((Action)(() => OpenErrorReportDialog(owner, report)));
+                return;
             }
-            else
+
+            using (var dialog = new SendErrorReportForm())
             {
-                result = uri.GetLeftPart(UriPartial.Authority) + sb.ToString();
+                dialog.ErrorReport = report;
+                dialog.ShowDialog(owner);
             }
-
-            return result;
         }
 
         /// <summary>
@@ -504,7 +459,7 @@ namespace OpenTween
         /// ドメインラベルの区切り文字はFULLSTOP(.、U002E)に置き換えられます。
         /// </para>
         /// </summary>
-        /// <param name="input">展開対象のURL</param>
+        /// <param name="inputUrl">展開対象のURL</param>
         /// <returns>IDNが含まれていた場合はPunycodeに展開したURLをを返します。Punycode展開時にエラーが発生した場合はnullを返します。</returns>
         public static string IDNEncode(string inputUrl)
         {
@@ -515,7 +470,7 @@ namespace OpenTween
                 var idnConverter = new IdnMapping();
                 uriBuilder.Host = idnConverter.GetAscii(uriBuilder.Host);
 
-                return uriBuilder.Uri.ToString();
+                return uriBuilder.Uri.AbsoluteUri;
             }
             catch (Exception)
             {
@@ -535,11 +490,11 @@ namespace OpenTween
                     uriBuilder.Host = idnConverter.GetUnicode(uriBuilder.Host);
                 }
 
-                return uriBuilder.Uri.ToString();
+                return uriBuilder.Uri.AbsoluteUri;
             }
             catch (Exception)
             {
-                return null;
+                return inputUrl;
             }
         }
 
@@ -554,8 +509,6 @@ namespace OpenTween
 
                 // Punycodeをデコードする
                 outputUrl = MyCommon.IDNDecode(outputUrl);
-                if (outputUrl == null)
-                    return inputUrl;
 
                 // URL内で特殊な意味を持つ記号は元の文字に変換されることを避けるために二重エスケープする
                 // 参考: Firefoxの losslessDecodeURI() 関数
@@ -621,10 +574,10 @@ namespace OpenTween
                     desdecrypt = des.CreateEncryptor();
 
                     //書き込むためのCryptoStreamの作成
-                    using (CryptoStream cryptStream = new CryptoStream(msOut, desdecrypt, CryptoStreamMode.Write))
+                    using (var cryptStream = new CryptoStream(msOut, desdecrypt, CryptoStreamMode.Write))
                     {
                         //Disposeが重複して呼ばれないようにする
-                        MemoryStream msTmp = msOut;
+                        var msTmp = msOut;
                         msOut = null;
                         desdecrypt = null;
 
@@ -640,8 +593,8 @@ namespace OpenTween
                 }
                 finally
                 {
-                    if (msOut != null) msOut.Dispose();
-                    if (desdecrypt != null) desdecrypt.Dispose();
+                    msOut?.Dispose();
+                    desdecrypt?.Dispose();
                 }
             }
         }
@@ -681,7 +634,7 @@ namespace OpenTween
                     desdecrypt = null;
 
                     //復号化されたデータを取得するためのStreamReader
-                    using (StreamReader srOut = new StreamReader(cryptStreem, Encoding.UTF8))
+                    using (var srOut = new StreamReader(cryptStreem, Encoding.UTF8))
                     {
                         //Disposeが重複して呼ばれないようにする
                         cryptStreem = null;
@@ -694,9 +647,9 @@ namespace OpenTween
                 }
                 finally
                 {
-                    if (msIn != null) msIn.Dispose();
-                    if (desdecrypt != null) desdecrypt.Dispose();
-                    if (cryptStreem != null) cryptStreem.Dispose();
+                    msIn?.Dispose();
+                    desdecrypt?.Dispose();
+                    cryptStreem?.Dispose();
                 }
             }
         }
@@ -728,13 +681,7 @@ namespace OpenTween
             return newBytes;
         }
 
-        public static bool IsNT6()
-        {
-            //NT6 kernelかどうか検査
-            return Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version.Major == 6;
-        }
-
-        [FlagsAttribute()]
+        [Flags]
         public enum TabUsageType
         {
             Undefined = 0,
@@ -749,6 +696,8 @@ namespace OpenTween
             Lists = 256,
             Related = 512,
             UserTimeline = 1024,
+            Mute = 2048,
+            SearchResults = 4096,
             //RTMyTweet
             //RTByOthers
             //RTByMe
@@ -784,34 +733,23 @@ namespace OpenTween
             }
             finally
             {
-                if (img != null) img.Dispose();
+                img?.Dispose();
             }
         }
 
-        public static DateTime DateTimeParse(string input)
+        public static DateTimeUtc DateTimeParse(string input)
         {
-            DateTime rslt;
-            string[] format = {
+            var formats = new[] {
                 "ddd MMM dd HH:mm:ss zzzz yyyy",
                 "ddd, d MMM yyyy HH:mm:ss zzzz",
             };
-            foreach (var fmt in format)
-            {
-                if (DateTime.TryParseExact(input,
-                                          fmt,
-                                          DateTimeFormatInfo.InvariantInfo,
-                                          DateTimeStyles.None,
-                                          out rslt))
-                {
-                    return rslt;
-                }
-                else
-                {
-                    continue;
-                }
-            }
+
+            if (DateTimeUtc.TryParseExact(input, formats, DateTimeFormatInfo.InvariantInfo, out var result))
+                return result;
+
             TraceOut("Parse Error(DateTimeFormat) : " + input);
-            return new DateTime();
+
+            return DateTimeUtc.Now;
         }
 
         public static T CreateDataFromJson<T>(string content)
@@ -820,7 +758,11 @@ namespace OpenTween
             var buf = Encoding.Unicode.GetBytes(content);
             using (var stream = new MemoryStream(buf))
             {
-                data = (T)((new DataContractJsonSerializer(typeof(T))).ReadObject(stream));
+                var settings = new DataContractJsonSerializerSettings
+                {
+                    UseSimpleDictionaryFormat = true,
+                };
+                data = (T)((new DataContractJsonSerializer(typeof(T), settings)).ReadObject(stream));
             }
             return data;
         }
@@ -851,13 +793,11 @@ namespace OpenTween
         /// <param name="keys">状態を調べるキー</param>
         /// <returns><paramref name="keys"/> で指定された修飾キーがすべて押されている状態であれば true。それ以外であれば false。</returns>
         public static bool IsKeyDown(params Keys[] keys)
-        {
-            return MyCommon._IsKeyDown(Control.ModifierKeys, keys);
-        }
+            => MyCommon._IsKeyDown(Control.ModifierKeys, keys);
 
         internal static bool _IsKeyDown(Keys modifierKeys, Keys[] targetKeys)
         {
-            foreach (Keys key in targetKeys)
+            foreach (var key in targetKeys)
             {
                 if ((modifierKeys & key) != key)
                 {
@@ -875,9 +815,7 @@ namespace OpenTween
         /// </remarks>
         /// <returns>アプリケーションのアセンブリ名</returns>
         public static string GetAssemblyName()
-        {
-            return MyCommon.EntryAssembly.GetName().Name;
-        }
+            => MyCommon.EntryAssembly.GetName().Name;
 
         /// <summary>
         /// 文字列中に含まれる %AppName% をアプリケーション名に置換する
@@ -885,9 +823,7 @@ namespace OpenTween
         /// <param name="orig">対象となる文字列</param>
         /// <returns>置換後の文字列</returns>
         public static string ReplaceAppName(string orig)
-        {
-            return MyCommon.ReplaceAppName(orig, Application.ProductName);
-        }
+            => MyCommon.ReplaceAppName(orig, ApplicationSettings.ApplicationName);
 
         /// <summary>
         /// 文字列中に含まれる %AppName% をアプリケーション名に置換する
@@ -896,9 +832,7 @@ namespace OpenTween
         /// <param name="appname">アプリケーション名</param>
         /// <returns>置換後の文字列</returns>
         public static string ReplaceAppName(string orig, string appname)
-        {
-            return orig.Replace("%AppName%", appname);
-        }
+            => orig.Replace("%AppName%", appname);
 
         /// <summary>
         /// 表示用のバージョン番号の文字列を生成する
@@ -920,7 +854,7 @@ namespace OpenTween
         /// 表示用のバージョン番号の文字列を生成する
         /// </summary>
         /// <remarks>
-        /// バージョン1.0.0.1のように末尾が0でない(=開発版)の場合は「1.0.1-beta1」が出力される
+        /// バージョン1.0.0.1のように末尾が0でない(=開発版)の場合は「1.0.1-dev」のように出力される
         /// </remarks>
         /// <returns>
         /// 生成されたバージョン番号の文字列
@@ -937,20 +871,10 @@ namespace OpenTween
             {
                 versionNum[2] = versionNum[2] + 1;
 
-                // 10を越えたら桁上げ
-                if (versionNum[2] >= 10)
-                {
-                    versionNum[1] += versionNum[2] / 10;
-                    versionNum[2] %= 10;
-
-                    if (versionNum[1] >= 10)
-                    {
-                        versionNum[0] += versionNum[1] / 10;
-                        versionNum[1] %= 10;
-                    }
-                }
-
-                return string.Format("{0}.{1}.{2}-beta{3}", versionNum[0], versionNum[1], versionNum[2], versionNum[3]);
+                if (versionNum[3] == 1)
+                    return string.Format("{0}.{1}.{2}-dev", versionNum[0], versionNum[1], versionNum[2]);
+                else
+                    return string.Format("{0}.{1}.{2}-dev+build.{3}", versionNum[0], versionNum[1], versionNum[2], versionNum[3]);
             }
         }
 
@@ -965,17 +889,15 @@ namespace OpenTween
         }
 
         public static string GetStatusUrl(string screenName, long statusId)
-        {
-            return TwitterUrl + screenName + "/status/" + statusId.ToString();
-        }
+            => TwitterUrl + screenName + "/status/" + statusId;
 
         /// <summary>
         /// 指定された IDictionary を元にクエリ文字列を生成します
         /// </summary>
         /// <param name="param">生成するクエリの key-value コレクション</param>
-        public static string BuildQueryString(IDictionary<string, string> param)
+        public static string BuildQueryString(IEnumerable<KeyValuePair<string, string>> param)
         {
-            if (param == null || param.Count == 0)
+            if (param == null)
                 return string.Empty;
 
             var query = param
@@ -1008,5 +930,92 @@ namespace OpenTween
 
             return sb.ToString();
         }
+
+        /// <summary>
+        /// 指定された範囲の整数を昇順に列挙します
+        /// </summary>
+        /// <remarks>
+        /// start, start + 1, start + 2, ..., end の範囲の数列を生成します
+        /// </remarks>
+        /// <param name="from">数列の先頭の値 (最小値)</param>
+        /// <param name="to">数列の末尾の値 (最大値)</param>
+        /// <returns>整数を列挙する IEnumerable インスタンス</returns>
+        public static IEnumerable<int> CountUp(int from, int to)
+        {
+            if (from > to)
+                return Enumerable.Empty<int>();
+
+            return Enumerable.Range(from, to - from + 1);
+        }
+
+        /// <summary>
+        /// 指定された範囲の整数を降順に列挙します
+        /// </summary>
+        /// <remarks>
+        /// start, start - 1, start - 2, ..., end の範囲の数列を生成します
+        /// </remarks>
+        /// <param name="from">数列の先頭の値 (最大値)</param>
+        /// <param name="to">数列の末尾の値 (最小値)</param>
+        /// <returns>整数を列挙する IEnumerable インスタンス</returns>
+        public static IEnumerable<int> CountDown(int from, int to)
+        {
+            for (var i = from; i >= to; i--)
+                yield return i;
+        }
+
+        public static IEnumerable<int> CircularCountUp(int length, int startIndex)
+        {
+            if (length < 1)
+                throw new ArgumentOutOfRangeException(nameof(length));
+            if (startIndex < 0 || startIndex >= length)
+                throw new ArgumentOutOfRangeException(nameof(startIndex));
+
+            // startindex ... 末尾
+            var indices = MyCommon.CountUp(startIndex, length - 1);
+
+            // 先頭 ... (startIndex - 1)
+            if (startIndex != 0)
+                indices = indices.Concat(MyCommon.CountUp(0, startIndex - 1));
+
+            return indices;
+        }
+
+        public static IEnumerable<int> CircularCountDown(int length, int startIndex)
+        {
+            if (length < 1)
+                throw new ArgumentOutOfRangeException(nameof(length));
+            if (startIndex < 0 || startIndex >= length)
+                throw new ArgumentOutOfRangeException(nameof(startIndex));
+
+            // startIndex ... 先頭
+            var indices = MyCommon.CountDown(startIndex, 0);
+
+            // 末尾 ... (startIndex + 1)
+            if (startIndex != length - 1)
+                indices = indices.Concat(MyCommon.CountDown(length - 1, startIndex + 1));
+
+            return indices;
+        }
+
+        /// <summary>
+        /// 2バイト文字も考慮したUrlエンコード
+        /// </summary>
+        /// <param name="stringToEncode">エンコードする文字列</param>
+        /// <returns>エンコード結果文字列</returns>
+        public static string UrlEncode(string stringToEncode)
+        {
+            const string UnreservedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_.~";
+            var sb = new StringBuilder();
+            var bytes = Encoding.UTF8.GetBytes(stringToEncode);
+
+            foreach (var b in bytes)
+            {
+                if (UnreservedChars.IndexOf((char)b) != -1)
+                    sb.Append((char)b);
+                else
+                    sb.AppendFormat("%{0:X2}", b);
+            }
+            return sb.ToString();
+        }
     }
 }