OSDN Git Service

ツイート文字数の280文字への上限緩和 (weightedLength) に対応
authorKimura Youichi <kim.upsilon@bucyou.net>
Wed, 8 Nov 2017 19:05:40 +0000 (04:05 +0900)
committerKimura Youichi <kim.upsilon@bucyou.net>
Wed, 8 Nov 2017 19:09:10 +0000 (04:09 +0900)
https://developer.twitter.com/en/docs/developer-utilities/twitter-text

OpenTween.Tests/TwitterTest.cs
OpenTween/Api/DataModel/TwitterTextConfiguration.cs [new file with mode: 0644]
OpenTween/OpenTween.csproj
OpenTween/Resources/ChangeLog.txt
OpenTween/Twitter.cs

index 19cc6eb..f75fa41 100644 (file)
@@ -327,8 +327,8 @@ namespace OpenTween
         {
             using (var twitter = new Twitter())
             {
-                Assert.Equal(140, twitter.GetTextLengthRemain(""));
-                Assert.Equal(132, twitter.GetTextLengthRemain("hogehoge"));
+                Assert.Equal(280, twitter.GetTextLengthRemain(""));
+                Assert.Equal(272, twitter.GetTextLengthRemain("hogehoge"));
             }
         }
 
@@ -343,6 +343,13 @@ namespace OpenTween
 
                 Assert.Equal(10000, twitter.GetTextLengthRemain("D twitter "));
                 Assert.Equal(9992, twitter.GetTextLengthRemain("D twitter hogehoge"));
+
+                // t.co に短縮される分の文字数を考慮
+                twitter.Configuration.ShortUrlLength = 20;
+                Assert.Equal(9971, twitter.GetTextLengthRemain("D twitter hogehoge http://example.com/"));
+
+                twitter.Configuration.ShortUrlLengthHttps = 21;
+                Assert.Equal(9970, twitter.GetTextLengthRemain("D twitter hogehoge https://example.com/"));
             }
         }
 
@@ -352,15 +359,14 @@ namespace OpenTween
             using (var twitter = new Twitter())
             {
                 // t.co に短縮される分の文字数を考慮
-                twitter.Configuration.ShortUrlLength = 20;
-                Assert.Equal(120, twitter.GetTextLengthRemain("http://example.com/"));
-                Assert.Equal(120, twitter.GetTextLengthRemain("http://example.com/hogehoge"));
-                Assert.Equal(111, twitter.GetTextLengthRemain("hogehoge http://example.com/"));
-
-                twitter.Configuration.ShortUrlLengthHttps = 21;
-                Assert.Equal(119, twitter.GetTextLengthRemain("https://example.com/"));
-                Assert.Equal(119, twitter.GetTextLengthRemain("https://example.com/hogehoge"));
-                Assert.Equal(110, twitter.GetTextLengthRemain("hogehoge https://example.com/"));
+                twitter.TextConfiguration.TransformedURLLength = 20;
+                Assert.Equal(260, twitter.GetTextLengthRemain("http://example.com/"));
+                Assert.Equal(260, twitter.GetTextLengthRemain("http://example.com/hogehoge"));
+                Assert.Equal(251, twitter.GetTextLengthRemain("hogehoge http://example.com/"));
+
+                Assert.Equal(260, twitter.GetTextLengthRemain("https://example.com/"));
+                Assert.Equal(260, twitter.GetTextLengthRemain("https://example.com/hogehoge"));
+                Assert.Equal(251, twitter.GetTextLengthRemain("hogehoge https://example.com/"));
             }
         }
 
@@ -370,15 +376,15 @@ namespace OpenTween
             using (var twitter = new Twitter())
             {
                 // t.co に短縮される分の文字数を考慮
-                twitter.Configuration.ShortUrlLength = 20;
-                Assert.Equal(120, twitter.GetTextLengthRemain("example.com"));
-                Assert.Equal(120, twitter.GetTextLengthRemain("example.com/hogehoge"));
-                Assert.Equal(111, twitter.GetTextLengthRemain("hogehoge example.com"));
+                twitter.TextConfiguration.TransformedURLLength = 20;
+                Assert.Equal(260, twitter.GetTextLengthRemain("example.com"));
+                Assert.Equal(260, twitter.GetTextLengthRemain("example.com/hogehoge"));
+                Assert.Equal(251, twitter.GetTextLengthRemain("hogehoge example.com"));
 
                 // スキーム (http://) を省略かつ末尾が ccTLD の場合は t.co に短縮されない
-                Assert.Equal(130, twitter.GetTextLengthRemain("example.jp"));
+                Assert.Equal(270, twitter.GetTextLengthRemain("example.jp"));
                 // ただし、末尾にパスが続く場合は t.co に短縮される
-                Assert.Equal(120, twitter.GetTextLengthRemain("example.jp/hogehoge"));
+                Assert.Equal(260, twitter.GetTextLengthRemain("example.jp/hogehoge"));
             }
         }
 
@@ -387,8 +393,8 @@ namespace OpenTween
         {
             using (var twitter = new Twitter())
             {
-                Assert.Equal(139, twitter.GetTextLengthRemain("🍣"));
-                Assert.Equal(133, twitter.GetTextLengthRemain("🔥🐔🔥 焼き鳥"));
+                Assert.Equal(278, twitter.GetTextLengthRemain("🍣"));
+                Assert.Equal(267, twitter.GetTextLengthRemain("🔥🐔🔥 焼き鳥"));
             }
         }
     }
diff --git a/OpenTween/Api/DataModel/TwitterTextConfiguration.cs b/OpenTween/Api/DataModel/TwitterTextConfiguration.cs
new file mode 100644 (file)
index 0000000..635d18b
--- /dev/null
@@ -0,0 +1,86 @@
+// OpenTween - Client of Twitter
+// Copyright (c) 2017 kim_upsilon (@kim_upsilon) <https://upsilo.net/~upsilon/>
+// All rights reserved.
+//
+// This file is part of OpenTween.
+//
+// This program is free software; you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation; either version 3 of the License, or (at your option)
+// any later version.
+//
+// This program is distributed in the hope that it will be useful, but
+// WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+// or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+// for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program. If not, see <http://www.gnu.org/licenses/>, or write to
+// the Free Software Foundation, Inc., 51 Franklin Street - Fifth Floor,
+// Boston, MA 02110-1301, USA.
+
+using System.Runtime.Serialization;
+
+namespace OpenTween.Api.DataModel
+{
+    [DataContract]
+    public class TwitterTextConfiguration
+    {
+        [DataMember(Name = "version")]
+        public string Version { get; set; }
+
+        [DataMember(Name = "maxWeightedTweetLength")]
+        public int MaxWeightedTweetLength { get; set; }
+
+        [DataMember(Name = "scale")]
+        public int Scale { get; set; }
+
+        [DataMember(Name = "defaultWeight")]
+        public int DefaultWeight { get; set; }
+
+        [DataMember(Name = "transformedURLLength")]
+        public int TransformedURLLength { get; set; }
+
+        [DataContract]
+        public class CodepointRange
+        {
+            [DataMember(Name = "start")]
+            public int Start { get; set; }
+
+            [DataMember(Name = "end")]
+            public int End { get; set; }
+
+            [DataMember(Name = "weight")]
+            public int Weight { get; set; }
+        }
+
+        [DataMember(Name = "ranges")]
+        public CodepointRange[] Ranges { get; set; }
+
+        /// <exception cref="SerializationException"/>
+        public static TwitterTextConfiguration ParseJson(string json)
+        {
+            return MyCommon.CreateDataFromJson<TwitterTextConfiguration>(json);
+        }
+
+        public static TwitterTextConfiguration DefaultConfiguration()
+        {
+            // 参照: https://developer.twitter.com/en/docs/developer-utilities/twitter-text
+            return new TwitterTextConfiguration
+            {
+                Version = "2",
+                MaxWeightedTweetLength = 280,
+                Scale = 100,
+                DefaultWeight = 200,
+                TransformedURLLength = 23,
+                Ranges = new[]
+                {
+                    new CodepointRange { Start = 0, End = 4351, Weight = 100 },
+                    new CodepointRange { Start = 8192, End = 8205, Weight = 100 },
+                    new CodepointRange { Start = 8208, End = 8223, Weight = 100 },
+                    new CodepointRange { Start = 8242, End = 8247, Weight = 100 },
+                },
+            };
+        }
+    }
+}
index 7227b7d..93250ee 100644 (file)
@@ -84,6 +84,7 @@
     <Compile Include="Api\DataModel\TwitterSearchResult.cs" />
     <Compile Include="Api\DataModel\TwitterStatus.cs" />
     <Compile Include="Api\DataModel\TwitterStreamEvent.cs" />
+    <Compile Include="Api\DataModel\TwitterTextConfiguration.cs" />
     <Compile Include="Api\DataModel\TwitterUploadMediaResult.cs" />
     <Compile Include="Api\DataModel\TwitterUser.cs" />
     <Compile Include="Api\DataModel\TwitterApiAccessLevel.cs" />
index 60b89b7..d7f7d79 100644 (file)
@@ -1,6 +1,9 @@
 更新履歴
 
 ==== Ver 1.4.1-dev(xxxx/xx/xx)
+ * NEW: ツイート文字数の280文字への上限緩和に対応しました
+  - Twitter公式クライアントなどと同様に、入力する文字種によって文字数の扱いが異なるものになります
+  - DMの送信時に行われる文字数のカウントは従来通りのままです
  * CHG: 画像投稿の対応サービスから「img.ly」「yfrog」「ついっぷるフォト」を削除
   - アップデート前にこれらのサービスを投稿先に選択していた場合は「Twitter」に変更されます
  * CHG: 画像投稿のキャンセル時に選択中の画像表示を閉じないように動作を変更
index db6f09a..340b270 100644 (file)
@@ -149,6 +149,7 @@ namespace OpenTween
 
         public TwitterApi Api { get; }
         public TwitterConfiguration Configuration { get; private set; }
+        public TwitterTextConfiguration TextConfiguration { get; private set; }
 
         delegate void GetIconImageDelegate(PostClass post);
         private readonly object LockObj = new object();
@@ -181,6 +182,7 @@ namespace OpenTween
         {
             this.Api = api;
             this.Configuration = TwitterConfiguration.DefaultConfiguration();
+            this.TextConfiguration = TwitterTextConfiguration.DefaultConfiguration();
         }
 
         public TwitterApiAccessLevel AccessLevel
@@ -1708,12 +1710,12 @@ namespace OpenTween
         {
             var matchDm = Twitter.DMSendTextRegex.Match(postText);
             if (matchDm.Success)
-                return this.GetTextLengthRemainInternal(matchDm.Groups["body"].Value, isDm: true);
+                return this.GetTextLengthRemainDM(matchDm.Groups["body"].Value);
 
-            return this.GetTextLengthRemainInternal(postText, isDm: false);
+            return this.GetTextLengthRemainWeighted(postText);
         }
 
-        private int GetTextLengthRemainInternal(string postText, bool isDm)
+        private int GetTextLengthRemainDM(string postText)
         {
             var textLength = 0;
 
@@ -1738,10 +1740,54 @@ namespace OpenTween
                 textLength += shortUrlLength - url.Length;
             }
 
-            if (isDm)
-                return this.Configuration.DmTextCharacterLimit - textLength;
-            else
-                return 140 - textLength;
+            return this.Configuration.DmTextCharacterLimit - textLength;
+        }
+
+        private int GetTextLengthRemainWeighted(string postText)
+        {
+            var config = this.TextConfiguration;
+            var totalWeight = 0;
+
+            var urls = TweetExtractor.ExtractUrlEntities(postText).ToArray();
+
+            var pos = 0;
+            while (pos < postText.Length)
+            {
+                var urlEntity = urls.FirstOrDefault(x => x.Indices[0] == pos);
+                if (urlEntity != null)
+                {
+                    totalWeight += config.TransformedURLLength * config.Scale;
+
+                    var urlLength = urlEntity.Indices[1] - urlEntity.Indices[0];
+                    pos += urlLength;
+
+                    continue;
+                }
+
+                var codepoint = char.ConvertToUtf32(postText, pos);
+                var weight = config.DefaultWeight;
+
+                foreach (var weightRange in config.Ranges)
+                {
+                    if (codepoint >= weightRange.Start && codepoint <= weightRange.End)
+                    {
+                        weight = weightRange.Weight;
+                        break;
+                    }
+                }
+
+                totalWeight += weight;
+
+                var isSurrogatePair = codepoint > 0xffff;
+                if (isSurrogatePair)
+                    pos += 2; // サロゲートペアの場合は2文字分進める
+                else
+                    pos++;
+            }
+
+            var remainWeight = config.MaxWeightedTweetLength * config.Scale - totalWeight;
+
+            return remainWeight / config.Scale;
         }