2011年1月14日金曜日

IIS ExpressのVS2008/VS2010利用

IIS ExpressをVisual Studio 2010に統合するにはSP1を待たないとだめですよね。でも、そんなの待ちきれない!という人はたくさんいることでしょう。IIS Expressをコマンドラインから起動パラメータを確認してみましょう。

iisex1

c:\Program files(x86)\IIS Express\iisexpress /?

IIS Express :
------------------
iisexpress [/config:config-file] [/site:site-name] [/siteid:site-id] [/systray:true|false] [/trace:trace-level]
iisexpress /path:app-path [/port:port-number] [/clr:clr-version] [/systray:true|false] [/trace:trace-level]

/config:config-file
applicationhost.config  Documents  IISExpress\config\applicationhost.config

/site:site-name
applicationhost.config

/siteid:site-id
applicationhost.config  ID

/path:app-path
/config

/port:port-number
8080 /path

/clr:clr-version
.NET Framework  (: v2.0) v4.0 /path

/systray:true|false
   true

/trace:trace-level
'none''n''info''i''warning''w''error' 'e'

\:
iisexpress /site:WebSite1
  WebSite1

iisexpress /config:c:\myconfig\applicationhost.config


iisexpress /path:c:\myapp\ /port:80
'80  'c:\myapp'

いろいろあるけど、プロジェクトファイルのパスと実行時のポートを指定すれば概ね動くってことなので、プロジェクトのプロパティを以下のように指定しましょう。

iisex2

動作開始のところで「外部プログラムを起動する」を選択して
C:\Program Files (x86)\IIS Express\iisexpress.exe
を指定。コマンドライン引数に
/path:プロジェクトフォルダ /port:8080
を指定(ポートはかぶらなければ何でも良し)。

あとはいつもどおり実行(F5)すると、コンソールが立ち上がってIIS Express経由でWebアプリケーションが起動します。とりあえずはブラウザの自動起動はしないので、ブラウザを立ち上げて
http://localhost:8080
と、入れて自分で開きましょう。この状態でもデバッグできるのでとてもゴキゲンです!

iisex3

iisex4

しかも同じ方法でVisual Studio 2008からも使えるので超絶便利!VS2008からは/clr:v2.0でランタイムバージョン指定も忘れずに。なんせ本番環境と同一のIISでデバッグできるし並列でリクエスト処理してくれてチョッパヤだし!Cassiniさらば!

WebMatrixで初めてのヘルパー

ASP.NET MVC3 RTM出ましたね。やったね。やりたい事いろいろあるんだ!

WebMatrixもでたね。WebPagesじゃん?Razorじゃん?ヘルパーじゃん?

WebMatrixやらなんやらの細かい説明とかはよくわからないのですっ飛ばして、早速ヘルパーを作ってみようかな。

ヘルパーといえばHelperResultを受け取る@helperで定義される者たちなんだろうけど、@functionsに定義するものもUtilityメソッドという意味でヘルパーっていう扱いにしても、それはそれで許されるんじゃないかと勝手に定義。だめ~?いいっしょ!

Webアプリケーションの基本といえばキャッシュでしょ!でもとりあえず今回はOutputCacheじゃなくてデータキャッシュね。System.Runtime.Caching!と言いたいところだけど、もっとお手軽なHttpContextBase.Cache。いろいろまとめてくれてるし、System.Web.Caching.Cache互換なインターフェイスだし。

まずは、WebMatrixでスターターサイトを作成。

ルートのdefault.cshtmlをひらいて以下のように書いてみる。

@{  
    Layout = "~/_SiteLayout.cshtml";
    Page.Title = "マイ Web サイトへようこそ";

    dynamic cache = Cache["test_page"];
    if(cache==null){
        cache = DateTime.Now;
        Cache.Insert(
            "test_page",
            cache,
            null,
            DateTime.Now.AddSeconds(3),
            System.Web.Caching.Cache.NoSlidingExpiration
        );
    }
}
<p>
    ASP.NET Web Pages を使用すると、Web 用の優れた .NET ベースのアプリケーションを簡単に作成できます。
</p>
<p>
    @cache<br/>
    @cache.GetType().Name
</p>

んで、動かしてみる。

helper1

出ますわね。3秒キャッシュ。ホームをなんどもクリックしてみたら分かるわね。

毎回キャッシュするたびにこんなコード書くのも面倒だよね!これをヘルパーにしてみよう!

ルートにApp_Codeって名前でフォルダ作成。んで、その中にUtility.cshtmlファイルを作成。

helper2

中身は↓。

@functions {
    public static new dynamic Cache(HttpContextBase context, string key, int expireSeconds, Func<dynamic> thunk){
        dynamic value = context.Cache[key];
        if(value == null) {
            value = thunk();
            context.Cache.Insert(
                key,
                value,
                null,
                DateTime.Now.AddSeconds(expireSeconds),
                System.Web.Caching.Cache.NoSlidingExpiration
            );
        }
        
        return value;
    }
}

staticメソッドなのでHelperPage.Cacheなんて見れないからContextを渡しましょう。もちろんHelper内で

var context = new HttpContextWrapper(HttpContext.Current);

って書くのもありだとは思うけど、まぁ、ね。

んで、default.cshtmlを以下のように変更。

@{  
    Layout = "~/_SiteLayout.cshtml";
    Page.Title = "マイ Web サイトへようこそ";

    dynamic cache = Cache["test_page"];
    if(cache==null){
        cache = DateTime.Now;
        Cache.Insert(
            "test_page",
            cache,
            null,
            DateTime.Now.AddSeconds(3),
            System.Web.Caching.Cache.NoSlidingExpiration
        );
    }
}
<p>
    ASP.NET Web Pages を使用すると、Web 用の優れた .NET ベースのアプリケーションを簡単に作成できます。
</p>
<p>
    @Utility.Cache(Context, "test_helper",3,()=>DateTime.Now)<br/>
    @Utility.Cache(Context, "test_helper",3,()=>DateTime.Now).GetType().Name<br/>

    @cache<br/>
    @cache.GetType().Name
</p>

太字の部分追加。

キャッシュしたい値を返すFunc<T>を渡しましょう。

@Utility.Cache(Context,”text_helper”,3,()=>{
  return DateTime.Now
})

でもいいよ。真面目な話をすると、ちゃんとレキシカルスコープを維持してるので、ローカル変数を参照しててもOKです。...。どんまい。

helper3

ちゃんと3秒キャッシュしてるよね!例えば、外部のサイトのデータを毎回取得するなんてばからしいからキャッシュしたり、データベースから毎回取得するなんてばからしいからキャッシュしたり、いろいろキャッシュすることでパフォーマンスは劇的にあがるっしょ!

dynamicだから

@Utility.Cache(Context, "test_helper",3,()=>DateTime.Now).Second

って書いてもちゃんと動くもんね!

まずは1 helper。

2011年1月10日月曜日

新横浜公園

新横浜公園ガイドブック

ホッケーを始める前スラロームをやってたとき、日産スタジアムが横浜国際競技場だったころにはちょくちょくスケートの練習に行ってたんだけど、めっきり行かなくなって何年立つんだろう。

土曜に久しぶりにテニスをしたんだけど、あまりにもまともにできなくてショック。どうやら新横浜公園にはテニスの壁打ちをするスペースがあるらしいと聞いたので行ってみた次第です。

IMG_0118

IMG_0119

凄いね!こんなに綺麗に公園出来てるとは思ってもなかった。

SPORTSよこはまVol.3:特集(1/4)

数名の人たちがポコポコ壁打ちをしているところに乗り込んでやってみたデス。残念なことにボール持ってないから朝から買いに行って来たデス。

壁打ち難しいね~。でもスゴク楽しかった。ひとりで2時間。寂しさに負けそうになったけど、先に順番待ちの人のプレッシャーに負けた。みんなで仲良く使わないとね!

その後、いつ買ったのか覚えてないくらい前のサロモンの安物フィットネススケートを持って来てたので、ふらふらと公園内を滑ってみた。

IMG_0115

素敵なテニスコート10面。ひとりじゃできない。

IMG_0116

綺麗に舗装された道。

IMG_0120 IMG_0121

スケボーとかの専用スペース。バスケやってる人たちもたくさんいた。

IMG_0123

スラローム!子供たちがにょろにょろ滑ってたよ~。みんな上手だね~。スラロームなんてホッケー始めてから全くやってないけど、いまどきのスケートはシャーシも短いし、ウィールもちっちゃいのかな?

IMG_0122

1周2キロの周回コースがあって2周半滑って強風に心が折れたっす。

しばらくホッケーコートが使えないから、この公園でいろいろ遊んでみようかな。

そういえば、車で行ったけどスケート行くのもいいかもね。鶴見川沿いのサイクリングコースで行けそう。往復で15kmくらいみたいだし、ちょうどいい感じかも~。

2011年1月9日日曜日

zenbackをBloggerとTumblrで

年末年始いろんなサイトで関連記事やら関連リンクが全く同じデザインで表示されてるのを見て、なんだろな~、と。

どうもzenbackというブログパーツのサービスみたいで(ちゃんとpowered by zenbackって書いてるのに、気づかずにHTMLソースから追いかけちまった、てへ)。

zb1

zenbackであなたのブログに全てのフィードバックを。

運営は大御所シックス・アパート。エンジンはログリー株式会社のnewziaコネクト。

newziaコネクト | コンテキストマッチサービス

凄いね。こんなスゴイサービスが半年も前からベータテスト始まってたのを全然しらなかった。アルゴリズムは「当社独自の連想検索技術および行動履歴解析技術」ってことで、連想検索というのが気になる(TF/IDF的なことも計算してるんだろうけど、その辺はちょっと不明)。

コンテキストマッチ自体面白い領域なので、そろそろ真面目に取り組まないとな~とは思ってるんだけど、以前作ったものをもっとちゃんとzenbackみたいにエンジン化していきたいなと思ってる次第です。

はい、んで、こんな面白いサービスは早速自分のブログにも導入したくなるじゃないですか。仕込む時点ではBloggerとTumblrではうまく貼り付けられない的な事が書かれてた。

要するにアーカイブページ(一覧ページ)とエントリーページ(個別Permalink)の判定がごにょごにょと。そうかもね、分かりにくいよね。なので、やり方。

Tumblr

TumblrのDashboardからAccountのPreference

zb2

Customize your Blog

zb3

Themeメニューをクリックして以下の部分にzenbackのスクリプトを貼りつけ。

zb4

{block:Permalink}~{/block:Permalink}のあいだです。↑この画像だと同じ場所にTypePad Connectのコメント欄もだしてます。

Blogger

Bloggerもスクリプトを入れる場所が分かりにくいんだけど、それだけじゃなくてXML的にValidじゃないとダメだから、zenbackのサイトで出力されるscriptそのままじゃダメで、ちゃんとCDATAにしてあげないとエラーになります。

これもダッシュボードからデザイン→HTMLの編集へ進んで"ウィジェットのテンプレートを展開"にチェックを入れる。

zb5

<b:if cond='data:post.allowComments'>~<b:else>~</b:if>のelse以降のブロックにスクリプトを書きましょう。

zb6

ちょっと大きなXMLなのでエレメントブロックが分り易見れるようにVS2010で。編集はテキストエディタで十分。

zenbackのダッシュボードからCODEを取得すると

<!-- X:S ZenBackWidget -->
<script type="text/javascript">
document.write(unescape("%3Cscript")+" src='http://widget.zenback.jp/?base_uri=http%3A//takepara.blogspot.com&nsid=93212648191094697%3A%3A93212656780978809&rand="+Math.ceil((new Date()*1)*Math.random())+"' type='text/javascript'"+unescape("%3E%3C/script%3E")); </script> <!-- X:E ZenBackWidget -->

↑こうなので、ちゃんとCDATAにしましょう。

<!-- X:S ZenBackWidget -->
<script type="text/javascript">

//<![CDATA[
document.write(unescape("%3Cscript")+" src='http://widget.zenback.jp/?base_uri=http%3A//takepara.blogspot.com&nsid=93212648191094697%3A%3A93212656780978809&rand="+Math.ceil((new Date()*1)*Math.random())+"' type='text/javascript'"+unescape("%3E%3C/script%3E"));
//]]>
</script> <!-- X:E ZenBackWidget -->

これで関連記事やらなにやらいろいろ表示されますね!リコメンド素敵。

2011年1月3日月曜日

Razor Hosting

Hosting the Razor Engine for Templating in Non-Web Applications - Rick Strahl's Web Log

いや~、参っちゃった。年末にRazor Templating Engine出てたじゃないですか。で、Instapaper見てみたらこっちも”後で読む”扱いになってたので、そろそろ読むかと読んでみたわけですよ。

そしたらこっちはハナからAppDomainを別にしてコンパイルするのを前提に作られてるじゃない。もちろんプロセスのAppDomainと同じところでコンパイルも出来ますよ。その辺はぬかりないデスヨ。同じ目的でもこんなにも設計が違うんだね~、っていう感じです。ちなみにRickさんのRazor HostingではVB.NETコンパイラは入ってないです。

はい、Hello World!

var engine = new RazorEngine<RazorTemplateBase>();
dynamic context = new ExpandoObject();
context.Name = "World!";
var result = engine.RenderTemplate("Hello @Context.Name", new string[] { }, context);

匿名クラスじゃダメだったからdynamicなExpandoObjectで。まぁ、その辺は適当でね。

続いて、HtmlHelperを使うようにして見ましょう!RTE(Razor Templating Engine)にはちゃんとサンプルがあったからそのまま簡単にできたけど、今回は無いのでしょうがないから自分で書く。と、言ってもRTEのをままもってきて、RazorEngineのジェネリックに基底クラスとして指定です。この辺ちょっとあれっすね。まぁ、いいや。

internal class HtmlHelperFactory
{
    #region Methods
    /// <summary>
    /// Creates a <see cref="HtmlHelper{T}"/> for the specified model.
    /// </summary>
    /// <typeparam name="T">The model type.</typeparam>
    /// <param name="model">The model to create a helper for.</param>
    /// <param name="writer">The writer used to output html.</param>
    /// <returns>An instance of <see cref="HtmlHelper{T}"/>.</returns>
    public HtmlHelper<T> CreateHtmlHelper<T>(T model, TextWriter writer)
    {
        var container = new InternalViewDataContainer<T>(model);
        var context = new ViewContext(
            new ControllerContext(),
            new InternalView(),
            container.ViewData,
            new TempDataDictionary(),
            writer);

        return new HtmlHelper<T>(context, container);
    }
    #endregion

    #region Types
    /// <summary>
    /// Defines an internal view.
    /// </summary>
    private class InternalView : IView
    {
        #region Methods
        /// <summary>
        /// Renders the contents of the view to the specified writer.
        /// </summary>
        /// <param name="context">The current View context.</param>
        /// <param name="writer">The writer used to generate the output.</param>
        public void Render(ViewContext context, TextWriter writer) { }
        #endregion
    }

    /// <summary>
    /// Defines an internal view data container.
    /// </summary>
    private class InternalViewDataContainer<T> : IViewDataContainer
    {
        #region Methods
        /// <summary>
        /// Initialises a new instance of <see cref="InternalViewDataContainer{T}"/>.
        /// </summary>
        public InternalViewDataContainer(T model)
        {
            ViewData = new ViewDataDictionary<T>(model);
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets or sets the view data dictionary.
        /// </summary>
        public ViewDataDictionary ViewData { get; set; }
        #endregion
    }
    #endregion
}

/// <summary>
/// Provides a base implementation of a template base that supports <see cref="HtmlHelper{T}"/>s.
/// </summary>
/// <typeparam name="T">The model type.</typeparam>
[RequireNamespaces("System.Web.Mvc.Html")]
public class HtmlRazorTemplateBase<T> : RazorTemplateBase
{
    private readonly HtmlHelperFactory factory = new HtmlHelperFactory();

    /// <summary>
    /// Initialises a new instance of <see cref="HtmlTemplateBase{T}"/>.
    /// </summary>
    public HtmlRazorTemplateBase()
    {
        CreateHelper(Context);
    }
    /// <summary>
    /// Gets the <see cref="HtmlHelper{T}"/> for this template.
    /// </summary>
    public HtmlHelper<T> Html { get; private set; }

    private void CreateHelper(T model)
    {
        Html = factory.CreateHtmlHelper(model, Response.Writer);
    }
}

そのままですみませんね!

後は、テンプレートを書いてテイッ!と実行。

static string Template =
@"
@using System.Web.Mvc
@using System.Web.Mvc.Html
Hello @Context.Name
@AppDomain.CurrentDomain.FriendlyName
Email: @Html.TextBoxFor(m => m.Email)";

public void ExecuteParse()
{
    ExecuteParse(-1);
}

public void ExecuteParse(int counter)
{
    var model = new PageModel
                    {
                        Name = "World", 
                        Email = "someone@somewhere.com"
                    };
    var asms = AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic ).Select(a=>a.Location);
    asms = asms.Concat(typeof(System.Web.Mvc.HtmlHelper)
                        .Assembly
                        .GetReferencedAssemblies().Select(an => Assembly.Load(an.FullName).Location)).Distinct();
    var refs = asms.Where(a => !a.EndsWith("System.dll")
                    && !a.EndsWith("System.Core.dll")
                    && !a.EndsWith("Microsoft.CSharp.dll")).ToArray();

    for (var i = 0; i < (counter != -1 ? 1 : Repeat); i++)
    {
        var sw = new Stopwatch();
        sw.Start();
        var engine = new RazorEngine<HtmlRazorTemplateBase<PageModel>>();
        var result = engine.RenderTemplate(Template, refs, model);

        sw.Stop();

        Console.WriteLine(result);
        Console.WriteLine(
            "{0} - {1} {2} ms {3:0,###} bytes",
            AppDomain.CurrentDomain.FriendlyName,
            counter != -1 ? counter : i,
            sw.ElapsedMilliseconds,
            Process.GetCurrentProcess().PrivateMemorySize64);
    }
}

なんですが、ここでかなりはまりました。

Template Execution Error: Security Transparent メソッド 'System.Web.Mvc.TypeDescriptorHelper.Get(System.Type)' がセキュリティ上重要なメソッド 'System.ComponentModel.DataAnnotations.AssociatedMetadataTypeTypeDescriptionProvider..ctor(System.Type)' にアクセスしようとして失敗しました。

アセンブリ 'System.ComponentModel.DataAnnotations, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35' は条件付きの APTCA アセンブリであり、現在の AppDomain では有効化されていません。このアセンブリを有効化して部分信頼コードまたは Security Transparent コードで使用できるようにするには、AppDomain の作成時に、アセンブリ名 'System.ComponentModel.DataAnnotations, PublicKey=0024000004800000940000000602000000240000525341310004000001000100B5FC90E7027F67871E773A8FDE8938C81DD402BA65B9201D60593E96C492651E889CC13F1415EBB53FAC1131AE0BD333C5EE6021672D9718EA31A8AEBD0DA0072F25D87DBA6FC90FFD598ED4DA35E44C398C454307E8E33B8426143DAEC9F596836F97C8F74750E5975C64E2189F45DEF46B2A2B1247ADC3652BF5C308055DA9' を PartialTrustVisibleAssemblies リストに追加してください。

なんすかこのエラー。コエー。入っちゃいけない領域に入っちゃった系のエラーなんじゃ...。

ASP.NET アプリケーションでコード アクセス セキュリティを使用します。

なんかDataAnnotationsも含まれてるね~。透過的モデルに移行したアセンブリ。

だいたいが

[assembly: System.Security.AllowPartiallyTrustedCallers()]

これ書けって書いてるんだけど、これ何さ。ってなもんで、いろいろよくわかんない。そもそもRTEでは動くんだから何か違いが有るんだろうと、参照アセンブリとかいろいろ比較してみたわけですよ。

そしたらですね、ちゃんと違いがありました。

Razor Templating Engine

↑ここに行って、RazorEngine.Templates.csprojを見てみてください。

rh1

衝撃!分かります?普通さ~、Razorっつったら、MVC3を想定するじゃないっすか。だから、HtmlHelper使うために参照するアセンブリもMVC3のものにしちゃうじゃないっすか。そこだった...。ショック。MVC2で動かしてやがった。なんか意味があるんだろうね。いつかちゃんと調べるとしてですね、とりあえずRazor Hostingの場合もMVC2のアセンブリを参照してHtmlHelperを呼び出すとちゃんとできた。

rh2

はぁ...。ぜんぜんわかんなかった。いや、今でも理由はよく分からない。ATPCAってなにさ。部分信頼ってなんだね。AllowPartiallyTrustedCallersつけたら↓こう。

rh3

型 'ConsoleApplication1.HtmlRazorTemplateBase`1<T>' で継承セキュリティ ルールの違反が発生しました。派生型は、基本型のセキュリティ アクセシビリティと一致するか、それより低いアクセスが設定されている必要があります。

CASが廃止。.NET 4のセキュリティはどうなるのか? - @IT

ふんふん、そっか!なるほど!

...

よし、がんばろう。

で、コンパイル済みのものを呼び出すときは

var engine = new RazorEngine<HtmlRazorTemplateBase<PageModel>>();
var compiledId = engine.ParseAndCompileTemplate(refs, new StringReader(Template));

var result = engine.RenderTemplateFromAssembly(compiledId, model);

こうね。はいはい。

続いて、違うAppDomainで実行するときはこう。

var host = new RazorStringHostContainer<HtmlRazorTemplateBase<PageModel>>
{
    UseAppDomain = true,
    ReferencedAssemblies = refs.ToList()
};

host.Start();
var result = host.RenderTemplate(Template, model);
host.Stop();

ちゃんとHost.Start()とStop()しようね。これがCreateAppDomainとUnload。

ちなみにこのコードはこのままじゃRazor Hostingでは動きません。何でかというと、RazirStringHostContainerはジェネリックじゃないから。基底クラスの指定方法がHost派生しかなかったから、それじゃ面倒なので、RazorStringHostContainer自体を書き換えてます。

といっても、定義だけ。

    public class RazorStringHostContainer<T> : RazorBaseHostContainer<T>
        where T : RazorTemplateBase, new()
    {        

rh4

ちょいちょいAppDomainを生成するのはやっぱり遅すぎ。もうちょっと賢くやんないとね。

と、言うわけでRazorのテンプレートエンジン2作品を取り上げてみたけど、HtmlHelperを使ったりするようにWebで使う場合はAPTCAのところをちゃんとクリアしてからじゃないとダメですね。

ここまで書いてて気がついたんだけど、MVC3ってどうやってるのか見るといいのかな。BuildManagerだと違ったりするのかな。うぬ~。なぞは深まるばかり。

2010年12月31日金曜日

Feedのアドレス

ちょっとあまりにも配信に時差がでちゃうので、Feedのアドレス変えます。すません。

Blogpost:無聊を託つ

http://feeds.feedburner.com/blogspot/jNRCtw

Tumblr:等閑に付さないように

http://feeds.feedburner.com/tumblr/ijTN

旧FeedのアドレスはTumblrの方に向けます。ホントすません。

sorry009

2010年12月30日木曜日

問題だと思っていることはホントに問題なの?

すっかり忘れてました。新しく開発者を採用するにあたり、会社の方針に合わせてコードを書いてもらいました。で、採用担当としてこんな問題を考えました。

問題内容

2つの文字列を与えられた時、それぞれの文字列の一致率を0~1の間で算出する関数を作成せよ。
※一致率の定義の仕様も含めて考えてください。

いろいろ考え方や思うところはあると思いますが、今回に関しては問題を解く過程をとても重視しました。どういうふうに仕様を決めるのかも含め、です。内容からして勝手に仕様を決めていいように思われるかもしれないですが、そこの解釈も重視です。与えられた情報の範囲内でどこまで飛べるのか、みたいな。

解答例

public static decimal Rate(string value1, string value2)
{
    if (string.IsNullOrEmpty(value1) || string.IsNullOrEmpty(value2))
        return 0m;

    if (string.IsNullOrEmpty(value1.Trim()) || string.IsNullOrEmpty(value2.Trim()))
        return 0m;

    if (string.Compare(value1, value2, StringComparison.CurrentCulture) == 0)
        return 1m;

    var longer2nd = value1.Length < value2.Length;
    var val1 = longer2nd ? value1 : value2; // 短い文字列
    var val2 = longer2nd ? value2 : value1; // 長い文字列
    int match = 0, p1 = 0, p2 = 0, p2Index = 0;

    while (p1 < val1.Length && p2 < val2.Length)
    {
        if (val1[p1] == val2[p2Index])
        {
            match += 2;
            p1++;
            p2 = p2Index++;
        }
        else
        {
            p2Index++;
            if (p2Index >= val2.Length)
            {
                p1++;
                p2Index = p2;
            }
        }
        
        if (val2.Length - 1 < p2Index)    // 進みすぎたときには引き戻す
            p2Index = p2;
    }

    return match / (decimal)(value1.Length + value2.Length);
}

最初のほうはエラーチェックだから、今回別に無くてもよかったです。

あれっすよ、こんなの10分やそこらでコード化するもんじゃないかもね。でも、考え方というか解き方というか、問題はどこでどういう回答をビジネスオーナーは望んでいるのか、ですよね。

問題を考えたものとしては、この問題で先頭一致の割合だけでとくのが一番簡単なのは分かってて、それだけじゃない解き方を答えて欲しいな~、と思ってました。

“どのくらい一致しているか”

の定義は様々だけど「全体文字数に占める一致文字数の割合」が手頃な回答ですよね。んじゃ、一致文字数というのはどういう意味だと解釈するのが面白いか。文字列を前半・中央・後半の3つの集合として考えて、それぞれの文字列の比較開始位置と一致文字数を数えていけば、それらしい数値を出すこと方法として面白いんじゃないかと思うんだけど、どうかな。

前半 中央 後半 文字列1 文字列2

AABBCC AABBCC

AABBCC AABBzz
※AABBも同じ

zzBBCC yyBBCC
※BBCCも同じ

AAzzCC AAyyCC
※AACCも同じ

  zzAAyy xxAAww

※前半だけ、後半だけは前半+中央と中央+後半と同じ。

こんな感じのパターン。ちなみにこのパターンを答えた人がひとりだけいましたよ。なんで同じ答えにたどり着いたのかというと、そういう定義をオーナーが望んでると聞き出したからです。そりゃ同じになるわね。

じゃー、このコードがホントにそれっぽく一致率がでるのか確認するテストコード。

[TestMethod]
public void 空文字渡し()
{
	var rate = MatchRate.Rate(null, null);
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("", null);
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate(null, "");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("", "");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("", "AA");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("AA", "");
	Assert.AreEqual(0, rate);
}

[TestMethod]
public void 空白渡し()
{
	var rate = MatchRate.Rate(" ", "");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("", "  ");
	Assert.AreEqual(0, rate);

	rate = MatchRate.Rate("AA", " ");
	Assert.AreEqual(0, rate);
}

[TestMethod]
public void 完全一致()
{
	var rate = MatchRate.Rate("AA", "AA");
	Assert.AreEqual(1m, rate);

	rate = MatchRate.Rate("aa", "aa");
	Assert.AreEqual(1m, rate);
}

[TestMethod]
public void 前方一致で66パーセント()
{
	var rate = MatchRate.Rate("AA", "AABB");
	Assert.AreEqual(2m / 3m, rate);
}

[TestMethod]
    public void 後方一致で66パーセント()
{
	var rate = MatchRate.Rate("AA", "BBAA");
	Assert.AreEqual(2m / 3m, rate);
}

[TestMethod]
public void 中央一致で66パーセント()
{
	var rate = MatchRate.Rate("AA", "BAAB");
	Assert.AreEqual(2m / 3m, rate);
}

[TestMethod]
public void 前後半一致で66パーセント()
{
	var rate = MatchRate.Rate("AA", "ABBA");
	Assert.AreEqual(2m / 3m, rate);
}

[TestMethod]
public void 前後半一致で75パーセント()
{
  var rate = MatchRate.Rate("ABCDE", "ADE");
  Assert.AreEqual(6m / 8m, rate);
}

だいたいいい感じなんじゃないっすかね。直感的にそんな感じ。セマンティックとか関係なく、ですから。問題はここまでで終了。

だたひとり答えにたどり着いたクマさんに拍手。

ここからさらに考え方を進めてみましょう。もし文字列が文章だとしたら。文章だと”の”とか”を”とかが入ってくるから、普通に一致させるだけだと、それっぽくならないですね。なので、このアルゴリズムを再帰に改造して、前半を無視したほうが一致率が上がるなら無視するというふうに改造してみます。何でかというと、このアルゴリズムだと一致率は上昇して頂点になったあと下降するっていう山型になるはずだからです。現在の位置と次の位置との比較で一致率が上昇してるなら、現在の位置は無視したほうがそれっぽい数値に近づくってことです。そうするとコピペ文章なんじゃん?っていうものに関しての判定が”それっぽく”できます。あくまで”それっぽく”ですよ。

public static decimal Rate(string value1, string value2)
{
    return Rate(value1, value2, null);
}

public static Func<string, int, string, int, bool> RecursiveCommand = (val1, idx1, val2, idx2) =>
{
    var p1Rate = Rate(val1.Substring(idx1), val2.Substring(idx2), null);
    var p1IncRate = Rate(val1.Substring(idx1 + 1), val2.Substring(idx2), null);
    return p1Rate < p1IncRate;
};

public static decimal Rate(string value1, string value2, Func<string, int, string, int, bool> command)
{
    if (string.IsNullOrEmpty(value1) || string.IsNullOrEmpty(value2))
        return 0m;

    if (string.IsNullOrEmpty(value1.Trim()) || string.IsNullOrEmpty(value2.Trim()))
        return 0m;

    if (string.Compare(value1, value2, StringComparison.CurrentCulture) == 0)
        return 1m;

    var longer2nd = value1.Length < value2.Length;
    var val1 = longer2nd ? value1 : value2; // 短い文字列
    var val2 = longer2nd ? value2 : value1; // 長い文字列
    int match = 0, p1 = 0, p2 = 0, p2Index = 0;

    while (p1 < val1.Length && p2 < val2.Length)
    {
        // p1とp1+1で比較してp1+1のほうがRateがよければp1はスキップする
        if (command != null)
        {
            p1 += command(val1, p1, val2, p2Index) ? 1 : 0;
        }

        if (val1[p1] == val2[p2Index])
        {
            match += 2;
            p1++;
            p2 = p2Index++;
        }
        else
        {
            p2Index++;
            if (p2Index >= val2.Length)
            {
                p1++;
                p2Index = p2;
            }
        }

        if (val2.Length - 1 < p2Index)    // 進みすぎたときには引き戻す
            p2Index = p2;
    }

    return match / (decimal)(value1.Length + value2.Length);
}

で、最初のテストがパスするのを確認したうえで、以下のテスト。

[TestMethod]
public void 再帰のほうがそれっぽくなる()
{
  var rate = MatchRate.Rate(
      "ABCDEF",
      "AECDEF"
  );
  var rateR = MatchRate.Rate(
      "ABCDEF",
      "AECDEF",
      MatchRate.RecursiveCommand
  );
  Console.WriteLine("Rate={0} < RateR={1}", rate, rateR);
  Assert.IsTrue(rate < rateR);
}

[TestMethod]
public void コピペしてちょっと手直しした文章()
{
  var rate = MatchRate.Rate(
		"衆院は31日の夜、会議を開き、日本郵政3社への再編などを柱とする郵政改革法案を賛成多数で可決し、参院に送付した。与党は参院でも短時間で審議し、6月16日までの会期内成立を目指す構え。自民、公明、みんな、たちあがれ日本の野党4党は1日、与党の意向を受け、本会議開催を強行した横路孝弘衆院議長に対する不信任決議案を提出する。",
		"衆院は31日深夜、本会議を開き、日本郵政グループの3社への再編などを柱とする郵政改革法案を民主、国民新党などの賛成多数で可決し、参院に送付した。与党は参院でも短時間で審議を終え、6月16日までの会期内成立を目指す構え。自民、公明、みんな、たちあがれ日本の野党4党は1日、与党の意向を受け、本会議開催を強行した横路孝弘衆院議長に対する不信任決議案を提出する。",
            MatchRate.RecursiveCommand
	);
	Console.WriteLine(rate);
}

ソースアップしておきまーす。何かの役に立つコードではないけどね!

dotnetConf2015 Japan

https://github.com/takepara/MvcVpl ↑こちらにいろいろ置いときました。 参加してくださった方々の温かい対応に感謝感謝です。