2015年01月14日

C#で括弧の対応を保存したまま文字列を反転させる

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

今回の問題は、括弧の対応を維持したまま文字列を反転させるというもの。
長めの問題文ですが、そのまま引用させてもらいます。

■問題 (出題者:nobsun さん)
与えられた文字列を前後反転する関数 ReverseString を書いてください。
ただし、ReverseString は単純に文字列を反転するのではなく、括弧の対応を保存するようにしてください。

以前のお題で作成した単純に与えられた文字列を単純に前後反転したもの返す reverseString では
  reverseString("文字列(もじれつ)の反転(はんてん)")
    → ")んてんは(転反の)つれじも(列字文"

のように括弧の対応は保存されませんが、ReverseString では
  ReverseString("文字列(もじれつ)の反転(はんてん)")
    → "(んてんは)転反の(つれじも)列字文"

のように括弧の対応が保存されます。
括弧文字は、'('と')'、'{'と'}'、'['と']'で、それぞれASCII文字と仮定してください。
  ReverseString("対応[の{とれている(さまざまな)括弧}の(例)]です。")
    → "。すで[(例)の{弧括(なまざまさ)るいてれと}の]応対"

入力文字列では対応の取れている括弧の内側には対応の取れない括弧文字はないと解釈してください。たとえば、
  ReverseString("これ(は(対応のとれていない)括弧がある例です。")
    → "。すで例るあが弧括(いないてれとの応対)は(れこ"

次のような場合は対応のとれている括弧はないという解釈になります。
  ReverseString("これ(も{対応の)とれていない}括弧の例です")
    → "。すで例の弧括}いないてれと)の応対{も(れこ"

日本語対応にする場合の文字のエンコーディングは実装側で都合のよいように仮定してください。
日本語対応であることは望ましいですが、必須ではありません。

対応する開き括弧と閉じ括弧を入れ替えてから、最後に一気に反転させるという方法で実装してみました。
括弧の入れ替えは StackとListを使って実現しています。
こうやれば、もっと簡単に実現できるよ! という目から鱗のアルゴリズムがあれば、どなたか教えてください。

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

namespace Doukaku.Org {
    class Program {
        static void Main(string[] args) {
            ReverseAndPrint("文字列(もじれつ)の反転(はんてん)");
            ReverseAndPrint("対応[の{とれている(さまざまな)括弧}の(例)]です。");
            ReverseAndPrint("これ(は(対応のとれていない)括弧がある例です。");
            ReverseAndPrint("これ(も{対応の)とれていない}括弧の例です");

            Console.ReadLine();
        }

        static void ReverseAndPrint(string s) {
            Console.WriteLine(ReverseString(s));
            Console.WriteLine();
        }

        static string ReverseString(string s) {
            char[] ParenthesesA = { '{', '(',  '[' };
            char[] ParenthesesB = { '}', ')',  ']' };
            Stack<CharAadIndex> stack = new Stack<CharAadIndex>();
            List<char> chars = new List<char>();
            int index = 0;
            foreach (var c in s) {
                if (ParenthesesA.Contains(c)) {
                    // 開き括弧
                    stack.Push(new CharAadIndex(c, index));
                    chars.Add(c);
                } else {
                    int i = Array.IndexOf(ParenthesesB, c);
                    if (i >= 0 && stack.Count > 0) {
                        CharAadIndex pair = stack.Peek();
                        if (pair.Char == ParenthesesA[i]) {
                            // 閉じ括弧:対応取れている
                            char temp = stack.Pop().Char;
                            chars[pair.Index] = c;
                            chars.Add(temp);
                        } else {
                            // 閉じ括弧:対応取れていない
                            stack.Push(new CharAadIndex(c, index));
                            chars.Add(c);
                        }
                    } else {
                        chars.Add(c);
                    }
                }
                index++;
            }
            StringBuilder sb = new StringBuilder();
            foreach (var c in chars.Reverse<char>()) {
                sb.Append(c);
            }
            return sb.ToString();
        }
    }

    class CharAadIndex {
        public CharAadIndex(char c, int index) {
            Char = c;
            Index = index;
        }
        public char Char { get; set; }
        public int Index { get; set; }
    }
}

  

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

2015年01月11日

C#でUTF-16をUTF-8に変換

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

■問題 (出題者:greentea さん)
UTF-16の文字コードを16進(1オクテットごとにスペース区切り)の形で入力します。入力した文字コードを、2進数の形(1オクテットごとにスペース区切り)で出力してください。 入力する文字コードはUCS-2の範囲(サロゲートペアを使わなくてもよい範囲)のみに限定しても構いませんが、可能ならばサロゲートペアにも対応したものに挑戦してください。
     * 例1: abc(U+0041 U+0042 U+0043)

          * 入力 00 41 00 42 00 43
          * 出力 01000001 01000010 01000011

     * 例2: あいう(U+3042 U+3044 U+3046)

          * 入力 30 42 30 44 30 46
          * 出力 11100011 10000001 10000010 11100011 10000001 10000100 11100011 10000001  10000110
正攻法からトリッキーな手段まで、いろいろお待ちしております。 参考: 通信用語の基礎知識 UTF-8


出題例から、出力は、UTF-8形式に直したものを2進形式で出力するということだと理解しました。 単に16進形式→2進形式ならば、UTF16と明示する必要がないですからね。

UTF16形式のバイト配列をUTF8形式のバイト配列に直接変換する方法が分からなかったので、 いったん文字列に変換後、UTF8に変換しています。

これらは、.NET Frameworkの機能を利用しているだけなので、ごりごり書かないといけないのは、 16進形式の文字列をbyte配列に変換するところだけです。これは、HexTextToBytes メソッドとして 実装しています。

お題は、2進数の形式で出力してくださいとのことですが、元がどんな文字列かが分かったほうが良いので、ついでに、文字列に変換したもの(上の例では「abc」「あいう」)も出力するようにしました。

サロゲートペアには対応してません。

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

namespace Doukaku.Org {
    class Program {
        static void Main(string[] args) {
            PrintUtf16BEByBit("00 41 00 42 00 43");
            PrintUtf16BEByBit("30 42 30 44 30 46");
            PrintUtf16BEByBit("30 D7 30 ED 30 B0 30 E9 30 DF 30 F3 30 B0");
            Console.ReadKey();     
        }

         private static void PrintUtf16BEByBit(string hexText) {
            byte[] utf16 = HexTextToBytes(hexText);
            string s = Encoding.GetEncoding("UTF-16BE").GetString(utf16);
            Console.WriteLine(s);

            byte[] utf8 = Encoding.UTF8.GetBytes(s);
            foreach (var b in utf8) {
                Console.Write("{0} ", Convert.ToString(b, 2));
            }
            Console.WriteLine();
        }

        private static byte[] HexTextToBytes(string hexText) {
            hexText = hexText.Replace(" ", "");
            List<byte> list = new List<byte>();
            for (int i = 0; i < (hexText.Length / 2) * 2; i += 2) {
                string s = "" + hexText[i] + hexText[i + 1];
                list.Add(Convert.ToByte(s, 16));
            }
            return list.ToArray();
        }

    }
}
  
Posted by gushwell at 22:30Comments(0)TrackBack(0)

2015年01月07日

C#で文字列を指定した行数に均等分割する

   このエントリーをはてなブックマークに追加 Clip to Evernote
皆さん、あけましておめでとうございます。
本年もどうぞよろしくお願いします。

昨年から懲りずに続けている 「どう書く?orgに感謝を込めて」シリーズ その60 です。


■問題 (出題者:nobsun さん)
一行の文字列を指定した数の行にできるだけ文字数が均等になるように分割してください. ただし,除算や剰余算を使わないで書いてみてください.
sample = "ゆめよりもはかなき世のなかをなげきわびつゝあかしくらすほどに四月十よひにもなりぬれば木のしたくらがりもてゆく"

divid 4 sample =>
"ゆめよりもはかなき世のなかを"
"なげきわびつゝあかしくらすほ"
"どに四月十よひにもなりぬれ"
"ば木のしたくらがりもてゆく"

divid 5 sample => 
"ゆめよりもはかなき世の"
"なかをなげきわびつゝあ"
"かしくらすほどに四月十"
"よひにもなりぬれば木の"
"したくらがりもてゆく"

divid 6 sample => 
"ゆめよりもはかなき"
"世のなかをなげきわ"
"びつゝあかしくらす"
"ほどに四月十よひに"
"もなりぬれば木のし"
"たくらがりもてゆく"
除算や剰余算を使わないでというのがいやらしいですね。
でも、高さ(行数)が分かっているわけですから、文字列を横に順に置いてゆく代わりに、 縦方向に置いてゆき、一番下までいったら、1行目に戻るようにしていけば、 各行の幅が分かります。
実際コードでは、文字を置いてゆくのではなく、カウントアップすることで、各行の幅を求めています。

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


namespace Doukaku.Org {
    class Program {
        static void Main(string[] args) {
            string sample = "ゆめよりもはかなき世のなかをなげきわびつゝあかしくらすほどに四月十よひにもなりぬれば木のしたくらがりもてゆく";
            PrintText(sample, 4);
            Console.WriteLine();
            PrintText(sample, 5);
            Console.WriteLine();
            PrintText(sample, 6);
            Console.ReadLine();
        }

        private static void PrintText(string sample,int height) {
            var list = DivideEqually(sample, height);
            foreach (var s in list)
                Console.WriteLine(s);
        }

        // 除算や剰余算を使わない方法
        static IEnumerable<string> DivideEqually(string s, int height) {
            int[] counts = new int[height];
            // 最初に、一行の幅を求めてしまう
            for (int i = 0, j = 0; j < s.Length; j++) {
                counts[i]++;
                if (++i >= height)
                    i = 0;
            }
            // 求めた幅にあうように、文字列を切り出してゆく。
            int start = 0;
            for (int i = 0; i < height; i++) {
                yield return s.Substring(start, counts[i]);
                start += counts[i];
            }
        }
    }
}

でも、後半部分で、Substringしているところがイヤだったので、Interactive Extensions(Ix) の Shareメソッド 使って 書き直してみました。

static IEnumerable<string> DivideEqually(string s, int height) {
    int[] counts = new int[height];
    // 最初に、一行の幅を求めてしまう
    for (int i = 0,j = 0; j < s.Length; j++) {
        counts[i]++;
        if (++i >= height)
            i = 0;
    }
    // 求めた幅にあうように、文字列を切り出してゆく。
    var q = s.Share();
    foreach (var count in counts) {
          yield return string.Concat(q.Take(count));
    }
}

ちょっとはすっきりしたかな。
でも、実際の仕事で書くとしたら、除算や剰余算を使うと思うので、 そのバージョンも書いてみました。

// 除算や剰余算を使った方法
static IEnumerable<string> DivideEqually2(string s, int height) {
    int width;
    int start = 0;
    for (int length = s.Length; length > 0; length -= width) {
        width = (int)Math.Ceiling((double)length / height--);
        yield return s.Substring(start, width);
        start += width;
    }
}

結局、Substring使うことになっちゃいました(^^;  
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)