htsign's blog

ノンジャンルで書くつもりだけど技術系が多いんじゃないかと思います

Paizaのオンラインハッカソンやってみた

土日特にすることもなく暇なので遊んでた。paiza.jp

1問目

与えられた文字列の基数番目のみを取り出せという問題。
使用言語: Ruby

input_lines = gets
chars = input_lines.each_char
odds = chars.select.with_index do |e, i| i % 2 == 0 end
puts odds.join

これは非常に簡単。どの言語使おうがすぐ書けます。
もしかしたら、「え?2で割り切れるのは偶数じゃないの?」と思うかもしれません。
ただ、プログラミング言語の世界ではインデックスは0から始まるのが主流であるため、この場合は逆転します。

2問目

曜日ごとの売り上げデータが改行区切りで流れてくるのでそれぞれ集計してね、という問題。
使用言語: C#

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

public class Hello{
    public static void Main(){
        var line = System.Console.ReadLine();
        int max = int.Parse(line);
        
        var weekDays = (DayOfWeek[])Enum.GetValues(typeof(DayOfWeek));
        var eachWeekDays = new Dictionary<DayOfWeek, List<int>>();
        foreach (DayOfWeek weekDay in weekDays)
        {
            eachWeekDays[weekDay] = new List<int>();
        }
        
        for (int i = 0; i < max; i++)
        {
            int value = int.Parse(Console.ReadLine());
            var week = (DayOfWeek)((i + 1) % 7);
            eachWeekDays[week].Add(value);
        }
        
        new[]{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"}
        .Select(s => (DayOfWeek)Enum.Parse(typeof(DayOfWeek), s))
        .Select(dow => eachWeekDays[dow])
        .ToList().ForEach(list => Console.WriteLine(list.Sum()));
    }
}

System.DayOfWeek列挙体にキャストするために無駄に回りくどいコードになってしまいました。
曜日ごとにList<int>を作って、それぞれに加算していって、最後に合計して出力、と。
正直読みにくい。こんなコード書いた奴は死ねばいいと思う。

3問目

RENA または MINAMI のどちらかを出力しろ、という問題。
正直Brainf*ck使っても5分以内に解ける自信があります。
コメントするべきことがないです。
使用言語: Ruby

puts :RENA

Rubyを使った理由は文字数が少なくて済むから。

4問目

RENAを選んだ場合の問題。
表計算ソフトの架空のシートで、選択した範囲の合計値を出力しろという問題。
選択範囲は複数の場合もありますが、セルの内容はすべて自然数です。
使用言語: C#

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

public class Hello
{
    public static void Main()
    {
        int[] intValues = Console.ReadLine().Trim().Split(' ').Select(int.Parse).ToArray();
        int columns     = intValues[0];
        int rows        = intValues[1];
        int selections  = intValues[2];
        
        Cell[,] cells = new Cell[columns, rows];
        
        for (int y = 0; y < rows; y++)
        {
            int[] values = Console.ReadLine().Trim().Split(' ').Select(int.Parse).ToArray();
            for (int x = 0; x < columns; x++)
            {
                cells[x, y].X = x;
                cells[x, y].Y = y;
                cells[x, y].Value = values[x];
            }
        }
        
        var selected = new HashSet<Cell>();
        for (int i = 0; i < selections; i++)
        {
            int[] positions = Console.ReadLine().Trim().Split(' ').Select(v => int.Parse(v) - 1).ToArray();
            int startX = positions[0];
            int startY = positions[1];
            int endX   = positions[2];
            int endY   = positions[3];
            
            cells.Cast<Cell>().Where(c =>
            {
                return
                    startX <= c.X && c.X <= endX &&
                    startY <= c.Y && c.Y <= endY;
            })
            .ToList().ForEach(c => selected.Add(c));
        }
        
        Console.WriteLine(selected.Sum(c => c.Value));
    }
}

struct Cell
{
    public int X { get; set; }
    public int Y { get; set; }
    public int Value { get; set; }
}

シート上のセルを表現するCell構造体を定義し、Cell型を持つ2次元配列を宣言、すべての要素に代入していきます。
選択範囲に含まれるセルをHashSet<Cell>に追加していきます。
HashSetクラス自体が内包するコレクションの重複を許さないため、追加する際に既に追加したかどうか、などと気にする必要がなくて楽です。
そしてHashSetクラスも当然のようにIEnumerable<T>を実装しているためLINQが使えます。
Cell構造体のValueプロパティを合計して出力、と。

余談ですが、この問題の問題文に書かれたプログラミングコードが「なでしこ」で書かれていて、コメントがいちいちエイプリルフールでやれといわんばかりの内容なので、是非一度見てみるといいと思います。

5問目

MINAMIを選んだ場合の問題。

まだやってません。
寝て起きたらやってみたいです。

Visual Studioに頼らないコーディング

で簡単なテキストエディタを作ってみました。

メモ帳(notepad.exe)だけでどこまで作れるか、みたいな自己満足のチャレンジです。
静的エラーはコンパイラが吐いてくれるから簡単に修正できるけど、実行時エラーの解決はなかなかしんどかったです。
STAThreadAttributeつけないとFileDialogクラスの派生クラスのインスタンス立てるときに死ぬのは勉強になりました。

結果、出来上がったのはメモ帳以下の機能しか持たないクソみたいなエディタでした。

コンパイラのオプションは

csc /target:winexe /o /out:editor.exe editor.cs

です。
複雑なオプションを知らないため、単純化するという目的で1ファイルで完結するようになってます。
お手軽コンパイル


以下コード

using System;
using System.Drawing;
using System.IO;
using System.Text;
using System.Windows.Forms;

public partial class Editor : Form
{
	private const int MaxFiles = 1;
	private readonly Encoding encoding = Encoding.UTF8;
	
	public FileInfo[] FileInfos { get; private set; }
	
	public Editor()
	{
		this.FileInfos = new FileInfo[MaxFiles];
		this.InitializeComponent();
		
		editArea.Size = new Size(this.ClientSize.Width, this.ClientSize.Height - menu.Height);
		editArea.Location = new Point(0, menu.Height);
	}
	
	public Editor(string filePath)
		: this()
	{
		this.OpenFile(filePath);
	}
	
	private void CreateNewFile()
	{
		editArea.Text = string.Empty;
		this.FileInfos[0] = new FileInfo("new file");
		this.ChangeCaption(this.FileInfos[0]);
	}
	
	private void OpenFile()
	{
		using (var ofd = new OpenFileDialog())
		{
			ofd.FileOk += (sender, e) => this.FileInfos[0] = new FileInfo(ofd.FileName);
			DialogResult dr = ofd.ShowDialog(this);
			if (dr != DialogResult.OK) return;
		}
		if (this.FileInfos == null || this.FileInfos[0] == null) return;
		this.OpenFile(this.FileInfos[0]);
	}
	private void OpenFile(string filePath)
	{
		if (File.Exists(filePath))
		{
			using (var sr = new StreamReader(filePath, this.encoding))
			{
				editArea.Text = sr.ReadToEnd();
				editArea.SelectionStart = editArea.SelectionStart;
			}
			this.FileInfos[0] = new FileInfo(filePath);
			this.ChangeCaption(this.FileInfos[0]);
		}
	}
	private void OpenFile(FileInfo fileInfo)
	{
		this.OpenFile(fileInfo.FilePath);
	}
	
	private void SaveFile()
	{
		this.SaveFile(true);
	}
	private void SaveFile(bool overwrite)
	{
		if (!overwrite)
		{
			using (var sfd = new SaveFileDialog())
			{
				sfd.FileOk += (sender, e) => this.FileInfos[0] = new FileInfo(sfd.FileName);
				DialogResult dr = sfd.ShowDialog(this);
				if (dr != DialogResult.OK) return;
			}
		}
		if (this.FileInfos == null || this.FileInfos[0] == null) return;
		if (File.Exists(this.FileInfos[0].FilePath))
		{
			using (var sw = new StreamWriter(this.FileInfos[0].FilePath, false, this.encoding))
			{
				sw.Write(editArea.Text);
				sw.Flush();
			}
			this.FileInfos[0].Changed = false;
			this.ChangeCaption(this.FileInfos[0]);
		}
	}
	
	private void ChangeCaption(FileInfo fileInfo)
	{
		if (fileInfo != null)
		this.Text = fileInfo.ToString();
	}
}


static class Program
{
	[STAThread]
	public static void Main(string[] args)
	{
		Editor editor;
		
		if (args.Length > 0)
		{
			editor = new Editor(args[0]);
		}
		else
		{
			editor = new Editor();
		}
		Application.Run(editor);
	}
}

public class FileInfo
{
	public string FilePath { get; private set; }
	public bool Changed { get; set; }
	
	public FileInfo(string filePath)
	{
		this.FilePath = filePath;
		this.Changed = false;
	}
	
	public override string ToString()
	{
		string symbol = this.Changed ? "*" : string.Empty;
		return symbol + this.FilePath;
	}
}
enum ExitStatus
{
	Success = 0,
	GeneralError = 1,
}

public partial class Editor : Form
{
	private void InitializeComponent()
	{
		menu = new MenuStrip();
		fileMenu = new ToolStripMenuItem();
		fileCreateNewMenu = new ToolStripMenuItem();
		fileOpenMenu = new ToolStripMenuItem();
		fileSaveAsMenu = new ToolStripMenuItem();
		fileSaveMenu = new ToolStripMenuItem();
		fileExitMenu = new ToolStripMenuItem();
		editArea = new TextBox();
		menu.SuspendLayout();
		this.SuspendLayout();
		
		fileMenu.Text = "ファイル(&F)";
		
		fileCreateNewMenu.Text = "新規作成(&N)";
		fileCreateNewMenu.ShortcutKeys = Keys.Control | Keys.N;
		fileCreateNewMenu.Click += (sender, e) => this.CreateNewFile();
		
		fileOpenMenu.Text = "開く(&O)";
		fileOpenMenu.ShortcutKeys = Keys.Control | Keys.O;
		fileOpenMenu.Click += (sender, e) => this.OpenFile();
		
		fileSaveAsMenu.Text = "名前を付けて保存(&A)";
		fileSaveAsMenu.ShortcutKeys = Keys.Control | Keys.Shift | Keys.S;
		fileSaveAsMenu.Click += (sender, e) => this.SaveFile(false);
		
		fileSaveMenu.Text = "上書き保存(&S)";
		fileSaveMenu.ShortcutKeys = Keys.Control | Keys.S;
		fileSaveMenu.Click += (sender, e) => this.SaveFile(true);
		
		fileExitMenu.Text = "終了(&X)";
		fileExitMenu.Click += (sender, e) => this.Close();
		
		editArea.Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right | AnchorStyles.Bottom;
		editArea.Multiline = true;editArea.AcceptsTab = true;
		editArea.AcceptsReturn = true;
		editArea.Font = new Font("MS Gothic", 10, FontStyle.Regular);
		editArea.WordWrap = false;
		editArea.ScrollBars = ScrollBars.Both;
		editArea.TextChanged += (sender, e) =>
		{
			if (this.FileInfos == null || this.FileInfos[0] == null) return;
			this.FileInfos[0].Changed = true;
			this.ChangeCaption(this.FileInfos[0]);
		};
		
		fileMenu.DropDownItems.AddRange(new ToolStripItem[] {
			fileCreateNewMenu,
			fileOpenMenu,
			new ToolStripSeparator(),
			fileSaveAsMenu,
			fileSaveMenu,
			new ToolStripSeparator(),
			fileExitMenu,
		});
		menu.Items.Add(fileMenu);
		menu.ResumeLayout(false);
		menu.PerformLayout();
		this.Size = new Size(600, 800);
		this.FormClosing += (sender, e) =>
		{
			if (this.FileInfos != null && this.FileInfos[0] != null && this.FileInfos[0].Changed)
			{
				DialogResult dr = MessageBox.Show("終了しますか?", "確認",
					MessageBoxButtons.OKCancel,
					MessageBoxIcon.Question);
				if (dr == DialogResult.OK)
				{
					Environment.Exit((int)ExitStatus.Success);
				}
				else
				{
					e.Cancel = true;
				}
			}
			else
			{
				Environment.Exit((int)ExitStatus.Success);
			}
		};
		this.Controls.AddRange(new Control[] { menu, editArea, });
		this.ResumeLayout(false);
		this.PerformLayout();
	}
	
	internal MenuStrip menu;
	internal ToolStripMenuItem fileMenu;
	internal ToolStripMenuItem fileCreateNewMenu;
	internal ToolStripMenuItem fileOpenMenu;
	internal ToolStripMenuItem fileSaveAsMenu;
	internal ToolStripMenuItem fileSaveMenu;
	internal ToolStripMenuItem fileExitMenu;
	internal TextBox editArea;
}

一応、せっかくここまでやったので、

  • カーソルの位置(行と列)を表示する機能
  • ウィンドウ内へファイルをDnDすることで開く機能
  • オートインデント機能
  • アンドゥ&リドゥ機能
  • ワイルドカード対応の検索機能
  • 正規表現対応の置換機能
  • 単純なシンタックスハイライト機能

くらいはつけたいな、と思ってます。思ってるだけでやらない可能性の方がはるかに高いけど。

Configクラスの実装サンプル

前回のエントリでちょっと載せたコードについて。
Configクラスを毎回書くのがだるかったので基底クラスを作ってそれを継承するようにしました。

この3つを基底クラスに定義したことで、Configクラスは非常にスッキリしたように思います。

本当はシングルトンパターンもSingleton<T>みたいなクラス作って継承させたかったんですが、Instanceプロパティの型の定義に四苦八苦して、結局派生クラス(この場合Configクラス)に直接書きました。

ちなみに、ConfigクラスにはINotifyPropertyChangedインターフェースは実装していません。
せっかくシンプルにまとめたクラスが見にくくなってしまうと思ったからです。
Notify Property Weaverという大変便利なVisual Studio用のアドオンもあるのですが、配布サイトが既になくなってしまっているのと、VS2010専用であるため、今回は考慮していません。

Configクラスの使い方

const string ConfigFileName = "hogefuga.xml";

string dataPath = mbApiInterface.Setting_GetPersistentStoragePath();
string configPath = Path.Combine(dataPath, ConfigFileName);

// 読み込み
// ※configPathにファイルが存在しない場合や、
//  XMLのデシリアライズに失敗すると、勝手に内部でLoadDefaultを呼びます。
Config.Instance.Load(configPath);

// プロパティ取り出し
Console.WriteLine(Config.Instance.ExampleString); // => example

// プロパティ書き込み
Config.Instance.ExampleBoolean = false;

// 保存
Config.Instance.Save(configPath);

MusicBeeのプラグイン開発

最近、MusicBeeというWindows向けのオーディオプレイヤーのプラグイン開発ばかりやってます。
本体がVer2.0辺りから.NET化したのもあって、プラグインも.NET(C#VB.NETC++/CLI)で非常に手軽に開発することができます。
コントロールはWinFormsです。
ビルドしたプラグインは主に2chのMusicBeeスレで公開していますが、
開発していくにあたっていろいろとノウハウも貯まってきたので、ここいらでいくつかご紹介します。
公式Wikiに書かれている内容もありますが、一応。

以下出てくるmbApiInterfaceという謎の変数ですが、
これはMusicBeeのAPIにアクセスするためのインターフェースのインスタンスです。
公式Wikiで配布されているプラグインソースコードをDLすると初めから宣言されています。

メインパネルで表示されている曲をすべて取得

public IEnumerable<string> GetDisplayedSongs()
{
    return GetSongs("domain=DisplayedFiles");
}

public IEnumerable<string> GetSongs(string query)
{
    if (mbApiInterface.Library_QueryFiles(query))
    {
        while (true)
        {
            string filepath = mbApiInterface.Library_QueryGetNextFile();
            if (string.IsNullOrEmpty(filepath)) yield break;
            yield return filepath;
        }
    }
}

ちなみに、ライブラリに存在する曲を丸ごとすべて、という場合には

public IEnumerable<string> GetAllSongs()
{
    return GetSongs("domain=Library");
}

とすることで可能です。

設定画面のコントロール作成をべた書きしなければいけない問題

MusicBeePlugin.PluginクラスのConfigureメソッドは、
内部変数aboutConfigurationPanelHeightプロパティに0以外を代入することによって、設定画面のPanelコントロールのハンドルをIntPtr構造体で受け取ります。
サンプルソースコードではこれに直接コーディングでコントロールを生やしていますが、
これをユーザーコントロールとして一まとめに作成したものを追加することで見通しがよくなります。
MusicBeeの設定画面で「保存」を押して閉じたときにMusicBeePlugin.PluginクラスのSaveSettingsメソッドが走るのですが、ここで編集した内容を反映させるため、私は設定を一まとめにプロパティで持ったConfigクラスを作り、そのインスタンスを作成したユーザーコントロールに参照渡ししています。

public bool Configure(IntPtr panelHandle)
{
    if (panelHandle != IntPtr.Zero)
    {
        Panel configPanel = (Panel)Control.FromHandle(panelHandle);
        var childUserControl = new MyUserControl(ref this.config);
        configPanel.Controls.Add(childUserControl);
    }
    return false;
}
追記(02/13 0:27)

Configクラスのインスタンスを参照渡しするより、シングルトンパターン使った方がスッキリしますね。

public class Config
{
    private Config() { }
    
    private static Config instance = new Config();
    public  static Config Instance { get { return Config.instance; } }
}

ファイルへのタグ書き込みが上手くいかない問題の解決

公式のフォーラムをよく読むと分かるのですが、MusicBeePlugin.Plugin.MusicBeeApiInterfaceクラスの中身を読んでいるだけでは気づけないんじゃないかと思います。

public void WriteTag(string filepath, MetaDataType type, string value)
{
    mbApiInterface.Library_SetFileTag(filepath, type, value);
    mbApiInterface.Library_CommitTagsToFile(filepath); // ここ大事
}

実はLibrary_SetFileTagメソッドはただバッファリングしているだけで、このメソッドを走らせているだけでは何重に実行しようとも実際に反映されることはありません。
反映させるためにはLibrary_CommitTagsToFileメソッドを走らせる必要があります。
ここで割とハマりました。


こんなところですか。
本当はもっとある気がするんだけど、すぐには浮かんでこないのと、文章にとりとめがなくなりすぎてて意味不明になってしまうので。

蛇足

デザイナで編集してると気づかないうちに余計なプロパティまで変更してること、ありますよね。

.NET Framework における System.String について

今年に入って初めて .NET Framework にまともに触れるようになってきたのでまだまだ勉強中です。
趣味で触るのはC#、仕事で触るのはVB.NETって感じです。

さて、だいたいどの解説サイトを見ても「C#VB.NETもできることは同じ」というような解釈になってます。
ただ、System.Stringが言語によって扱いが異なる気がしています。

あるとき、VB.NETNullable(Of String)と書いたときに静的エラーが出力されました。
妙だと思い調べてみると、System.Stringは参照型であるというのです。

基本的な型のうち int, double, byte, char などは値型ですが、
string は「値型のように振る舞う参照型」であるというような記述を見ました。

参照型であるということは null を代入できるのか。
と思い、以下のコードを書きました。

C#
public class Program
{
    public static void Main(string[] args)
    {
        string string1 = null;
        string string2 = "";
        string string3 = string.Empty;

        Console.WriteLine("1:{0}", string1 ?? "string1 is null");
        Console.WriteLine("2:{0}", string2 ?? "string2 is null");
        Console.WriteLine("3:{0}", string3 ?? "string3 is null");

        Console.WriteLine(string1 == string2); // null         == ""
        Console.WriteLine(string2 == string3); // ""           == string.Empty
        Console.WriteLine(string3 == string1); // string.Empty == null
    }
}
VB.NET
Public Module Program
    Public Sub Main(args() As string)
        Dim string1 As String = Nothing
        Dim string2 As String = ""
        Dim string3 As String = String.Empty
        
        Console.WriteLine("1:{0}", If(string1, "string1 is Nothing"))
        Console.WriteLine("2:{0}", If(string2, "string2 is Nothing"))
        Console.WriteLine("3:{0}", If(string3, "string3 is Nothing"))
        
        Console.WriteLine(string1 = string2) ' Nothing      = ""
        Console.WriteLine(string2 = string3) ' ""           = String.Empty
        Console.WriteLine(string3 = string1) ' String.Empty = Nothing
    End Sub
End Module

動作デモ*1

C#     : http://rextester.com/ELTMU58022
VB.NET : http://rextester.com/IRCW50181

と、ここまで来て二つの言語間で結果が違ったことに気づきました。

これ、 ""String.Emptyエイリアスの関係なので言うまでもなく同じものです。
ですから、比較したときにtrueになるのは当然だと思います。
でも、

string string1 = null;
string string2 = "";
Console.WriteLine(string1 == string2); // ==> false
Dim string1 As String = Nothing
Dim string2 As String = ""
Console.WriteLine(string1 = string2) ' ==> True

なのです。
VB.NETではデフォルトでOption Strict Offであるため暗黙の型変換が行われたのかと思い、Option Strict Onを記述するも変わらず。

ちなみに、

Dim string1 As String = Nothing
Dim string2 As String = ""
Console.WriteLine(string2.Equals(string1)) ' ==> False

でした。

この挙動について、このエントリを書いている途中まではSystem.Stringの特殊性によるものかと思っていましたが、書いている途中でそれぞれの言語の等価演算子の振る舞いの違いによるものかも?ともちょっと思っています。
どうなんですかね、これ。

何とも尻切れトンボ感あるエントリになってしまいましたが、この辺で。

追記 (2014/11/11 1:21)

どうやら、やはり等価演算子自体が違う振る舞いをするようです。

*1:rextester.comは鯖が落ちていることが稀によくある