htsign's blog

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

BookLiveで今まで使ってきた金額を出すスクリプト書いた

いやー久しぶりにJavaScript書いたわー。

(function(d){var s=d.createElement("script");s.src="//dl.dropboxusercontent.com/u/414379/www/BookLivePaid/script.js";d.body.appendChild(s)})(document)

一時期Amazon.co.jpで使った額を調べるスクリプトが流行ったことがありましたが、アレのBookLive版だと思えば大体あってます。

使い方

  1. 上のコードをコピーします。
  2. http://booklive.jp/my/top に移動してBookLiveにログインします。
    • すでにログイン済みならトップページで実行しても問題ないはず。
  3. 開発者コンソール出す。
    • IE/Firefox/Chromeなど主要ブラウザなら「F12」キーで出せます。
  4. 貼り付けてEnter。
  5. コンソールに結果が出力されます。


動作確認はIE11でしかしていません。
たぶん他でも動くと思いますが、Firefoxさん辺りは実行前に警告文出すかも。

IE/Firefox/Chromeのそれぞれ最新版で正常に動作することを確認しました。

ソース

実際に実行されるスクリプトの中身です。
呼び出すソースはDropboxにアップロードしたものですが、コードは同一です。

main();

function main() {
    var now = new Date();
    var startYear = now.getFullYear();
    var startMonth = now.getMonth() + 1;
    
    var paid = 0;
    var least = prompt("何年まで遡りますか?", startYear);
    if (!/^[0-9]{4}$/.test(least)) {
        console.warn("半角4文字の西暦で入力してください。");
        return;
    }
    
    for (var y = startYear + 1; --y >= least; ) {
        for (var m = (y === startYear ? startMonth : 12) + 1; --m > 0; ) {
            paid += monthSum(y, m);
            console.info("ここまでの累計: " + paid + "円");
        }
    }
    console.info("合計: " + paid + "円");
}

function monthSum(year, month) {
    var sum = 0;
    console.info(year + "年" + month + "月のリクエスト中...");
    var doc = request("/my/product?year=" + year + "&month=" + month);
    var nodes = doc.querySelectorAll('[id^="myproduct_display"]');
    
    if (nodes.length !== 0) {
        sum = [].slice.call(nodes)
            .map(function(e){ return e.textContent.trim(); })
            .filter(function(text){ return text.indexOf("円") === text.length - 1; })
            .map(function(price){ return parseInt(price.split(",").join("")); })
            .reduce(function(a, b){ return a + b; });
    }
    
    console.info(sum + "円");
    return sum;
}

function request(url) {
    var xhr = new XMLHttpRequest();
    xhr.open("GET", url, false);
    xhr.send(null);
    
    var range = document.createRange();
    return range.createContextualFragment(xhr.responseText);
}

ちなみに

私の消費金額は2013年1月から7月12日現在で654,126円らしいです。

iPadのリファビッシュ品のリスト取ってくるだけのexe作った

単に自分が欲しかったので。
手元のiPad miniがストレージもメモリもカツカツで、使い物にならなくなりつつあるからです。
TouchIDなんか必要ないので、iPad mini Retinaが安く手に入ればいいかなと。

Dropbox - iPadRefubished.exe
何も特別なことはしていないためソースコードは省略しますが、どうしても見たい物好きな人がいたら、ILSpyとかJustDecompileとかで勝手に覗いてください。

一時的に使うために作ったため、メソッド分割などしていません。すごく適当です。
一応処理の流れを言うと

  1. http://store.apple.com/jp/browse/home/specialdeals/ipadスクレイピング
  2. 各製品の名前とリンクURLと価格と詳細情報を抜き出す
  3. LinkLabelコンポーネントに名前とURLを貼り付ける
  4. Labelコンポーネントに価格貼り付けて右寄せ
  5. ToolTipコンポーネントに詳細貼り付けてLinkLabelに関連付け

以上。

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);