2014年07月30日

C#でコード中の文字の頻度分析

   このエントリーをはてなブックマークに追加 Clip to Evernote
どう書く?orgに感謝を込めて」シリーズ その29

■問題 (出題者:crane さん)
プログラムコード中の文字の頻度は言語によって相当にばらつきがあると思います。ある言語はピリオドが頻出するとか、別の言語はカッコの頻出頻度が高い、とか。そこで、
  • 文字の頻度解析をするプログラムを作成し、
  • 適当なプログラムに対して実行し、結果を出力して、そのような頻度になっている理由を教えてください。
(その言語で書かれた「典型的な」プログラムコード、といえるようなものがあると良いのですが・・) 簡単すぎるという方は、複数文字にしてみたり単語の頻度にしてみてください。

参考;Wikipedia 頻度分析 http://ja.wikipedia.org/wiki/%E9%A0%BB%E5%BA%A6%E5%88%86%E6%9E%90

これはDictionaryクラスの出番ですね。 頻度分析を行うCharFreequencyクラスを定義してみました。
受け取る型をstringp[] にするか、string にするか迷いましたが、 単純な string にしました。File.ReadAllText 使えば、一発で string として取得できますからね。

頻度分析する際は、制御文字とWhiteSpaceは除いていますが、IsWhiteSpace だけでも問題はないですね。たぶん...

■C#で書いたコード
using System;
using System.Collections.Generic;
using System.Linq;
using System.IO;

namespace Doukaku.Org {
    class Program {
        static void Main(string[] args) {
            CharFrequency cd = new CharFrequency(File.ReadAllText(args[0]));  // C#はargs[1]ではなくargs[0]
            var dict = cd.Analyze();            

            // Top20だけを表示
            var total = dict.Sum(x => x.Value);
            foreach (var d in dict.OrderByDescending(x => x.Value).Take(20)) {
                Console.WriteLine("{0} = {1},  {2:#0.#0}%", d.Key, d.Value, d.Value * 100.0 / total);
            }
            Console.ReadLine();
        }
    }

    class CharFrequency {
        private string text;
        public CharFrequency(string s) {
            text = s;
        }

        public Dictionary<char, int> Analyze() {
            var dict = new Dictionary<char, int>();
            foreach (var c in text) {
                if (char.IsControl(c) || char.IsWhiteSpace(c))
                    continue;
                if (dict.ContainsKey(c))
                    dict[c]++;
                else
                    dict[c] = 1;
            }
            return dict;
        }
    }
}
あるソースを 実行してみたところ、以下のような結果となりました。

t = 1602,  8.30%
e = 1594,  8.25%
i = 923,  4.78%
n = 788,  4.08%
s = 729,  3.78%
r = 717,  3.71%
a = 715,  3.70%
m = 681,  3.53%

set, get, return, true. int, while, string, private, foreach system など、C#では、t や e を含む単語が多いですからね。
でも、通常の英文でも t や e は多そうですね。  

Posted by gushwell at 22:00Comments(0)TrackBack(0)

2014年07月27日

C#で文字列型日時のN秒後時間取得

   このエントリーをはてなブックマークに追加 Clip to Evernote
どう書く?orgに感謝を込めて」シリーズ その28

■問題 (出題者:raynstard)
日時を表す文字列と時間(秒)を受け取り
指定された日時からN秒となる日時を出力する関数 DateEx() を作成してください。

関数の仕様は次の通りです。
1. 入力となる日時の書式は任意である。
    → プログラムの都合に合わせてよい。
2. 入力となる時間(秒)は、負の値も許容すること。
    また、負の値が指定された場合、指定の日時よりも前の日時を出力すること
3. 出力する日時は入力の日時と同じ書式をとる文字列であること
4. 出力する日時は正規化されていること
5. 出力先は標準出力、または、バッファのいずれでもよい。

たとえば、DateEx("20080827235925",40)ならば
出力は
「20080828000005」です。

余力があれば時間を省略可能とし、省略された場合は「現在時刻」を利用するように
してみてください。

入力となる日時の書式は、yyyy/MM/dd HH:mm:ss とします。 DateTime.Parse使ってるので、DateTime.Parse が受け付ける書式なら何でも良いのですが、 出力はひとつに固定です。
そういう意味では、3を満足していないともいえますが、まあ、そこはご愛嬌ということで...

正規表現使って、日時を取り出し、それを DateTime型に変換するという方法もあるとおもいますが、 それだと、めんどうなので、素直に、DateTime.Parse 使いました。
入力書式があってるのが前提条件です。

■C#で書いたコード
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Doukaku.Org {

    class Program {
        static void Main(string[] args) {
            Console.WriteLine(DateEx("2008/08/27 23:59:25", 40));
            Console.WriteLine(DateEx("", 40));
        }

        static string DateEx(string date, int n) {
            DateTime dt = string.IsNullOrEmpty(date) ? DateTime.Now : DateTime.Parse(date);
            DateTime dt2 =  dt.AddSeconds(n);
            return string.Format("{0:yyyy/MM/dd HH:mm:ss}", dt2);
        }
    }
}
  
Posted by gushwell at 21:30Comments(0)TrackBack(0)

2014年07月23日

C#で文字列のセンタリング

   このエントリーをはてなブックマークに追加 Clip to Evernote
どう書く?orgに感謝を込めて」シリーズ その27

■問題 (出題者:nobsun さん)
文字列を指定のカラム幅にセンタリング配置する関数を示してください。 文字列の長さが指定した幅より長い場合には文字列の両端をできるだけ均等に切り落して 指定幅に収めてください。1文字は1カラムに収まるものと仮定してかまいません。

センタリングといってもCUIベースのセンタリングです。
こういったコードは、もう書くことはないのかな?
ASCII文字だけを対象とした、Centeringメソッドを書いてみました。
Mainメソッドでは、12のケースで正しくセンタリングできるか確認しています。


■C#で書いたコード
using System;

namespace Doukaku.Org {
    class Program {
        static void Main(string[] args) {
            string[] strs = {
                                "1",
                                "12",
                                "123",
                                "1234",
                                "12345",
                                "123456",
                                "1234567",
                                "1234567890",
                                "12345678901",
                                "123456789012",
                                "1234567890123",
                                "12345678901234",
                            };
            foreach (var s in strs) {
                Console.WriteLine("<{0}>",Centering(s, 10));
            }
            Console.ReadLine();
        }

        // 文字列が表示幅より短ければ、左側と右側に何文字の空白が必要なのかを計算する。
        // 文字列が表示幅より長ければ、何文字目から表示するのかを計算する。
        static string Centering(string s, int width) {
            int space = width - s.Length;
            if (space >= 0) {
                return new string(' ', space / 2) + s + new string(' ', space - (space / 2));
            } else {
                return s.Substring(-space / 2).Remove(width);
            }
        }
    }
}
  
Posted by gushwell at 22:00Comments(0)TrackBack(0)

2014年07月20日

C#で13日の金曜日を数え上げる

   このエントリーをはてなブックマークに追加 Clip to Evernote
どう書く?orgに感謝を込めて」シリーズ その26

■問題 (出題者:takano32 さん)
今日から2013年12月31日までの、13日の金曜日とその総数を表示してください。 余力のあるものはこのプログラムを短くしてみたり、短くしてみたり、短くしてください。
オリジナル:http://ll.jus.or.jp/2008/program/doukaku.html


最後の「余力のあるものは...」の部分は無視しています。
引き数ひとつのEnumFriday13は、指定日以降の13日の金曜日を無限に列挙しています。これを呼び出す引き数2つのメソッドで、TakeWhileを使い有限個にしています。
なお、もう2013年12月31日は過ぎてしまったので、2015年12月31日までとしています。
ついでに、引き数ひとつのEnumFriday13を利用し、2014年1月1日以降の 13日の金曜日を10個まで求めています。

結果は載せませんので、答えが知りたい方は是非実行してみてください。

■C#で書いたコード
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;


namespace Doukaku.Org {
    class Program {
        static void Main(string[] args) {
            var list = EnumFriday13(DateTime.Today, new DateTime(2015, 12, 31)).ToList();           
            foreach (var d in list)
                Console.WriteLine(d.ToShortDateString());
            Console.WriteLine("{0} days", list.Count());
            Console.ReadLine();

            var list2 = EnumFriday13(new DateTime(2014, 1, 1)).Take(10).ToList();
            foreach (var d in list2)
                Console.WriteLine(d.ToShortDateString());
            Console.WriteLine("{0} days", list2.Count());
            Console.ReadLine();
       
        }

        // 引き数 fromDateからtoDateの間の 13日の金曜日を列挙する
        static IEnumerable<DateTime> EnumFriday13(DateTime fromDate, DateTime toDate) {
            return EnumFriday13(fromDate).TakeWhile(d => d <= toDate);
        }

        // 引き数 date以降の 13日の金曜日を列挙する
        static IEnumerable<DateTime> EnumFriday13(DateTime date) {
            if (date.Day > 13)
                date = date.AddMonths(1);
            date = new DateTime(date.Year, date.Month, 13);
            while (true) {
                if (date.DayOfWeek == DayOfWeek.Friday) {
                    yield return date;
                }
                date = date.AddMonths(1);
            }
        }
    }
}

  
Posted by gushwell at 23:00Comments(0)TrackBack(0)

2014年07月16日

C#で重複要素を取り除く

   このエントリーをはてなブックマークに追加 Clip to Evernote
どう書く?orgに感謝を込めて」シリーズ その25

■問題 (出題者:にしお さん)
与えられたリストxsの中から、 2回以上出現するものを全部取り除いてください。
サンプル入力
[3, 1, 4, 1, 5, 9, 2, 6, 5]
サンプル出力
[3, 4, 9, 2, 6]

これはアレイのuniqの派生問題です。 リストとかアレイという言葉は言語によってまちまちの意味で使われているので、 「配列のようなもの」という漠然とした意味にとって構いません。

重複要素を取り除く3つの異なるメソッドを書いてみました。どれもその要素の出現数を求めて、出現数が1の要素だけを返すようにしています。
1番目のバージョンは、同じ値でグルーピングして、出現数をカウントしています。
2番目のバージョンは、Dictionaryで、出現数をカウントしています。
3番目のバージョンのメソッドは、LINQのCountメソッドで出現数をカウントしています。ワンライナーでコード量は少なめですが、効率が気になります。

もっと簡潔で効率の良い書き方があるような気もしますが...

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Doukaku.Org {
    class Program {
        static void Main(string[] args) {
            int[] array = { 5, 7, 3, 1, 4, 1, 5, 9, 2, 6, 5,1,7 };
            var r = UniqOnlyone01(array);
            foreach (var x in r) {
                Console.WriteLine(x);
            }
            Console.WriteLine();

            r = UniqOnlyone02(array);
            r.ToList().ForEach(x => Console.WriteLine(x));
            Console.WriteLine();

            r = UniqOnlyone03(array);
            r.ToList().ForEach(x => Console.WriteLine(x));
            Console.WriteLine();

            Console.ReadLine();
        }

        static IEnumerable<T> UniqOnlyone01<T>(IEnumerable<T> list) {
            var q = from n in list
                    group n by n;
            var q2 = from g in q
                     where g.Count() == 1
                     select g.Key;
            return q2;
        }

        static IEnumerable<T> UniqOnlyone02<T>(IEnumerable<T> list) {
            Dictionary<T, int> mem = new Dictionary<T, int>();
            foreach (var n in list) {
                if (mem.ContainsKey(n))
                    mem[n] = mem[n] + 1;
                else
                    mem[n] = 1;
            }
            return list.Where(e => mem[e] == 1);
        }

        static IEnumerable<T> UniqOnlyone03<T>(IEnumerable<T> list) {
            return list.Where(n => (list.Count(x => x.Equals(n)) == 1));
        }
    }
}


■追記 (2014/07/17)
 
Twitterで指摘がありました。
最初に示したクエリ式使ったコードを、拡張メソッド形式にするのが一番良さそうですね。

 return list.GroupBy(n => n).Where(g => g.Count() == 1).Select(g => g.Key);

Enumerable .Empty<> との Unionとれば良いのではという意見もあったようですが、
これだと、Distinctと同じ動きになり、問題の要求を満たしません。
  
Posted by gushwell at 20:41Comments(0)TrackBack(0)

2014年07月13日

C#で配列のuniq

   このエントリーをはてなブックマークに追加 Clip to Evernote
どう書く?orgに感謝を込めて」シリーズ その24


■問題 (出題者:にしお さん)
アレイ(複数の値が配列状になっているもの)xsが与えられたときに、同じ値が2回以上出現しないように、2回目以降の出現を取り除いたアレイを返すコードを書いてください。 Rubyで表現すると下のようになります。
irb(main):001:0> xs = [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]
=> [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9]
irb(main):002:0> xs.uniq
=> [3, 1, 4, 5, 9, 2, 6, 8, 7]


間違えないように:よくある「ハッシュを使う」「集合オブジェクトを使う」などの方法は順番が乱れてしまうので使えません。出現順序を守りつつ、2回目以降の出現だけを取り除いてください。 この投稿は匿名での挑戦状の投稿を元に作成しています。ご投稿ありがとうございます。

たぶん、LINQ to Objects の Distinctメソッド使えばOKだと思います。出現順は保持されていたと記憶しています。
 
でも、ここでは、Distinctメソッドを使わないコードとしました。結果用リストに順に値を入れていくのですが、その際、すでに結果用リストにあるものは 入れないようにするという、ごくありふれたコードです。 いちおう、要素の型に依存しないようジェネリックス使ってます。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Doukaku.Org {
    class Program {
        static void Main(string[] args) {
            int[] nums = { 3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5, 8, 9, 7, 9 };
            var result = Uniq(nums);
            string s = String.Join(", ", result.Select(n => n.ToString()).ToArray());
            Console.WriteLine("[{0}]", s);

            Console.ReadLine();
        }

        static T[] Uniq<T>(T[] array) {
            List<T> list = new List<T>();
            foreach (var x in array) {
                if (!list.Contains(x))
                    list.Add(x);
            }
            return list.ToArray();
        }
    }
}
  
Posted by gushwell at 22:00Comments(0)TrackBack(0)

2014年07月09日

C#でリストを逆順に表示

   このエントリーをはてなブックマークに追加 Clip to Evernote
どう書く?orgに感謝を込めて」シリーズ その23

■問題 (出題者:にしお さん)
簡単な問題です。
リスト状のものを逆順に表示するプログラムを書いてください。

簡単すぎる問題ですが、以下のようなジェネリックメソッドを書きました。
「逆順に表示」ということなので、リストそのものは変更しないというふうに理解しました。
そのため、逆順を求めるのに、List<T>.Reverse()メソッドではなく、LINQの Reverseメソッドを使っています。 IEnumerable を引き数にしているので、配列でもList<int>でもOKです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Doukaku.Org {
    class Program {
        static void Main(string[] args) {
            List<int> nums = new List<int> { 9, 8, 7, 6, 5, 4, 3, 2, 1 };
            ReversePrint(nums);
            string[] strs = new[] { "abc", "def", "hij", "xyz"  };
            ReversePrint(strs);
            nums.ForEach(n => Console.WriteLine(n));
            Console.ReadLine();
        }

        // 引き数listの並び順は変更されない。あくまでも表示だけ。
        static void ReversePrint<T>(IEnumerable<T> list) {
            foreach (var x in list.Reverse())
                Console.WriteLine(x);
        }
    }
}
  
Posted by gushwell at 23:00Comments(0)TrackBack(0)

2014年07月06日

C#で起動オプションの解析

   このエントリーをはてなブックマークに追加 Clip to Evernote
どう書く?orgに感謝を込めて」シリーズ その22

前回に引き続き、コマンドラインのパラメータ解析の問題です。

■問題 (※ ごめんなさい、出題者の方が分かりません)
次の起動インタフェースを持つコマンドを作成してください。

書式:cmdopt -o [-q] [-d{0|1|2}] 文字列 [文字列 ...]

書式を説明すると
- オプション「o」
  必須オプションです。指定されていない場合、異常終了してください。
- オプション「q」
  選択オプションです。
  省略されていても問題有りません。
- オプション「d」
  引数付きオプションです。
  「0」「1」「2」のいずかが続いて指定されます。
- 文字列
  パラメータです。
  1つ以上であればいくつでも指定できます。
  指定されていなかった場合、異常終了してください。

オプションの開始が「-」になっていますが
「+」や「/」でもかまいません。
余力があればロングオプションに対応してもよいです。

起動例:(すべて許容されるのが望ましいです)
1. cmdopt -o AAA
2. cmdopt -o AAA BBB CCC
3. cmdopt -oq AAA
4. cmdopt -o  -q AAA
5. cmdopt -o -d1 AAA
6. cmdopt -o -d 1 AAA
7. cmdopt -q -d2 -o AAA


出力例:
[オプション情報]
o(output): ON|OFF
q(quote): ON|OFF
d(debug): 0|1|2

[パラメータ情報]
指定数: N
1: 文字列1
2: 文字列2
...
N: 文字列N
 

CommandOptionsというクラスを定義し、そこに解析した結果を入れています。 オプション o, q, d は大文字でもOKにしています。

この手のコードって、どうしても力技になっちゃいますね。 もっと、スマートな方法はないのかなーといつも思うのですが...

「指定されていない場合、異常終了してください。」の異常終了は、例外を発生させるのでは なく、標準エラー出力にメッセージを表示し、プログラムを正常終了させています。まあ当たり前か...

■C#で書いたコード

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace Doukaku.Org { class Program { static void Main(string[] args) { CommandOptions co = GetOptions(args); if (co.OptionOutput == false) StopProgram("Oの指定がありません"); if (co.Parameters.Count == 0) StopProgram("文字列の指定がありません"); if (0 > co.OptionDebug || co.OptionDebug > 2) StopProgram("Dオプションは、0|1|2のいずれかです"); Console.WriteLine("[オプション情報]"); Console.WriteLine("o(output):{0}", co.OptionOutput ? "ON" : "OFF"); Console.WriteLine("q(quote):{0}", co.OptionQuote ? "ON" : "OFF"); Console.WriteLine("d(debug):{0}", co.OptionDebug); Console.WriteLine("[パラメータ情報]"); Console.WriteLine("指定数: {0}", co.Parameters.Count); for (int i = 0; i < co.Parameters.Count; i++) { Console.WriteLine("{0}:{1}", i, co.Parameters[i]); } } private static void StopProgram(string msg) { Console.Error.WriteLine(msg); Environment.Exit(1); } static private CommandOptions GetOptions(string[] args) { CommandOptions co = new CommandOptions(); bool dopt = false; co.Parameters = new List<string>(); foreach (var arg in args) { if (arg[0] == '-' || arg[0] == '/') { for (int i = 1; i < arg.Length; i++) { switch (char.ToUpper(arg[i])) { case 'O': co.OptionOutput = true; break; case 'Q': co.OptionQuote = true; break; case 'D': dopt = true; if (i + 1 < arg.Length) { int num = arg[++i] - '0'; co.OptionDebug = num; dopt = false; } break; } } } else { if (dopt) { int num = -1; int.TryParse(arg, out num); co.OptionDebug = num; } else { co.Parameters.Add(arg); } dopt = false; } } return co; } } public class CommandOptions { public bool OptionOutput { get; set; } public bool OptionQuote { get; set; } public int OptionDebug { get; set; } public List<string> Parameters { get; set; } } }

GetOptionsメソッドを、ちょっと手直ししたバージョンも載せておきます。
こっちのほうが少し簡潔になったかな。
        static private CommandOptions GetOptions(string[] args) {
            CommandOptions co = new CommandOptions();
            co.Parameters = new List<string>();
            var ite = args.GetEnumerator();
            while (ite.MoveNext()) {
                string arg = (string)ite.Current;
                if (arg[0] == '-' || arg[0] == '/') {
                    for (int i = 1; i < arg.Length; i++) {
                        switch (char.ToUpper(arg[i])) {
                        case 'O':
                            co.OptionOutput = true;
                            break;
                        case 'Q':
                            co.OptionQuote = true;
                            break;
                        case 'D':
                            int num = -1;
                            if (i + 1 < arg.Length) {
                                num = arg[++i] - '0';
                            } else {
                                ite.MoveNext();
                                int.TryParse((string)ite.Current, out num);
                            }
                            co.OptionDebug = num;
                            break;
                        }
                    }
                } else {
                    co.Parameters.Add(arg);
                }
            }
            return co;
        }  

  
Posted by gushwell at 22:20Comments(0)TrackBack(0)