/*
 * Copyright (c) 2006-2009 OrangeSignal.com All rights reserved.
 */

package jp.sourceforge.orangesignal.ta.dataset;

import static jp.sourceforge.orangesignal.ta.ArrayDataUtils.INDEX_NOT_FOUND;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jp.sourceforge.orangesignal.ta.ArrayDataUtils;
import jp.sourceforge.orangesignal.ta.TechnicalAnalysis;
import jp.sourceforge.orangesignal.ta.result.FourPrice;

/**
 * <p>価格や出来高などの基本的な時系列データセットを提供します。</p>
 * 
 * @author 杉澤 浩二
 */
public class TimeSeriesDataset {

	/**
	 * シンボルを保持します。
	 * 
	 * @since 1.1
	 */
	private String symbol;

	/**
	 * シンボルを返します。
	 * 
	 * @return シンボル。又は <code>null</code>
	 * @since 1.1
	 */
	public String getSymbol() { return symbol; }

	/**
	 * シンボルを設定します。
	 * 
	 * @param symbol シンボル
	 * @since 1.1
	 */
	public void setSymbol(final String symbol) { this.symbol = symbol; }

	/**
	 * シンボル名を保持します。
	 * 
	 * @since 1.1
	 */
	private String symbolName;

	/**
	 * シンボル名を返します。
	 * 
	 * @return シンボル名。又は <code>null</code>
	 * @since 1.1
	 */
	public String getSymbolName() { return symbolName; }

	/**
	 * シンボル名を設定します。
	 * 
	 * @param symbolName シンボル名
	 * @since 1.1
	 */
	public void setSymbolName(final String symbolName) { this.symbolName = symbolName; }

	/**
	 * 足の単位を表す列挙型を提供します。
	 * 
	 * @author <a href="mailto:sugisawa@orangesignal.com">杉澤 浩二</a>
	 */
	public enum Interval {

		/**
		 * <p>分足を表します。</p>
		 * {@link #getMaxPeriod()} は常に <code>60</code> を返します。
		 */
		MINUTELY { @Override public int getMaxPeriod() { return 60; } },

		/**
		 * <p>時足を表します。</p>
		 * {@link #getMaxPeriod()} は常に <code>24</code> を返します。
		 */
		HOURLY { @Override public int getMaxPeriod() { return 24; } },

		/**
		 * <p>日足を表します。</p>
		 * {@link #getMaxPeriod()} は常に <code>365</code> を返します。
		 */
		DAILY { @Override public int getMaxPeriod() { return 365; } },

		/**
		 * <p>週足を表します。</p>
		 * {@link #getMaxPeriod()} は常に <code>52</code> を返します。
		 */
		WEEKLY { @Override public int getMaxPeriod() { return 52; } },

		/**
		 * <p>月足を表します。</p>
		 * {@link #getMaxPeriod()} は常に <code>12</code> を返します。
		 */
		MONTHLY { @Override public int getMaxPeriod() { return 12; } };

		/**
		 * この足単位の最大期間を返します。
		 * 
		 * @return 最大期間
		 */
		public abstract int getMaxPeriod();

	}

	/**
	 * 足の単位を保持します。
	 */
	private final Interval interval;

	/**
	 * 日時データを保持します。
	 */
	private Date[] date;

	/**
	 * 時系列データ群を保持します。
	 */
	private Map<DataType, Number[]> dataset = new HashMap<DataType, Number[]>();

	/**
	 * このデータセットの4本値データが株式分割計算済みかどうかを保持します。
	 */
	private boolean splited = false;

	/**
	 * <p>足単位からこのクラスを構築する開発者向けのコンストラクタです。</p>
	 * 
	 * @param interval 足の単位
	 * @throws NullPointerException 足の単位に <code>null</code> が指定された場合
	 */
	protected TimeSeriesDataset(final Interval interval) {
		if (interval == null)
			throw new NullPointerException("Interval is null.");
		this.interval = interval;
	}

	/**
	 * <p>足単位と価格データからこのクラスを構築するコンストラクタです。</p>
	 * 
	 * @param interval 足の単位
	 * @param price 価格データ
	 * @throws NullPointerException 足の単位や価格データに <code>null</code> が指定された場合。又は価格データ中の日時データに <code>null</code> が存在する場合
	 */
	public TimeSeriesDataset(final Interval interval, final PriceData[] price) {
		this(interval, price, null, null);
	}

	/**
	 * <p>足単位と価格データ及びオプションの信用取引データと株式分割データからこのクラスを構築するコンストラクタです。</p>
	 * 
	 * @param interval 足の単位
	 * @param price 価格データ
	 * @param margin 信用取引データ
	 * @param split 株式分割データ
	 * @throws NullPointerException 足の単位や価格データに <code>null</code> が指定された場合。又は価格データ中の日時データに <code>null</code> が存在する場合
	 */
	public TimeSeriesDataset(
			final Interval interval,
			final PriceData[] price,
			final MarginData[] margin,
			final SplitData[] split)
	{
		this(interval);

		if (price == null)
			throw new NullPointerException("Price is null.");

		setupPrice(price);
		if (margin != null) setupMargin(margin);
		if (split != null) setupSplit(split);
	}

	/**
	 * <p>他のデータセットからこのクラスを構築するコンストラクタです。</p>
	 * 
	 * @param dataset データセット
	 */
	public TimeSeriesDataset(final TimeSeriesDataset dataset) {
		this.symbol = dataset.symbol;
		this.symbolName = dataset.symbolName;
		this.interval = dataset.interval;
		this.date = dataset.date.clone();
		this.dataset.putAll(dataset.dataset);
		this.splited = dataset.splited;
	}

	/**
	 * <p>指定された価格データを変換し、このクラスのデータセットへ設定します。</p>
	 * 価格データが {@link PriceVolumeData} インタフェースを実装している場合は、出来高データも変換処理されます。
	 * 
	 * @param price 価格データ
	 * @throws NullPointerException 価格データに <code>null</code> が指定された場合。又は価格データ中の日時データに <code>null</code> が存在する場合。
	 */
	private void setupPrice(final PriceData[] price) {
		final int length = price.length;
		final Date[]   date   = new Date[length];
		final Number[] open   = new Number[length];
		final Number[] high   = new Number[length];
		final Number[] low    = new Number[length];
		final Number[] close  = new Number[length];
		final Number[] volume = new Number[length];
		boolean use_volume = false;

		for (int i = 0; i < length; i++) {
			final PriceData o = price[i];
			if (o == null)
				continue;
			if (o.getDate() == null)
				throw new NullPointerException("Date is null.");

			date[i]  = o.getDate();
			open[i]  = o.getOpen();
			high[i]  = o.getHigh();
			low[i]   = o.getLow();
			close[i] = o.getClose();

			if (o instanceof PriceVolumeData) {
				final PriceVolumeData v = (PriceVolumeData) o;
				if (v.getVolume() != null) {
					volume[i] = v.getVolume();
					use_volume = true;
				}
			}
		}

		setDate(date);
		setOpen(open);
		setHigh(high);
		setLow(low);
		setClose(close);
		if (use_volume) setVolume(volume);
	}

	/**
	 * <p>指定された信用残データを指定された日付データと揃う様に変換して設定します。</p>
	 * 日付データは自然順序付けに従って昇順にソートされている必要があります。<br>
	 * 
	 * @param margin 信用残データ
	 */
	private void setupMargin(final MarginData[] margin) {
		final int length = getLength();
		final Number[] sold = new Number[length];
		final Number[] bought = new Number[length];

		// FIXME - 時刻をトランケートする方がモアベター

		int pos = 0;
		Number prev_sold = null;
		Number prev_bought = null;

		for (final MarginData o : margin) {
			if (o == null)
				continue;
			if (o.getDate() == null)
				continue;

			while (pos < length && getDate(pos).compareTo(o.getDate()) < 0) {
				sold[pos] = prev_sold;
				bought[pos] = prev_bought;
				pos = pos + 1;
			}
			prev_sold = o.getSold();
			prev_bought = o.getBought();
		}
		for (; pos < length; pos++) {
			sold[pos] = prev_sold;
			bought[pos] = prev_bought;
		}

		setSold(sold);
		setBought(bought);
	}

	/**
	 * <p>指定された株式分割数データを指定された日付データと揃う様に変換して設定します。</p>
	 * 日付データは自然順序付けに従って昇順にソートされている必要があります。<br>
	 * 
	 * @param split 株式分割数データ
	 */
	private void setupSplit(final SplitData[] split) {
		final int length = getLength();
		final Number[] results = new Number[length];

		// FIXME - 時刻をトランケートする方が良いかも

		int pos = 0;
		for (final SplitData o : split) {
			if (o == null)
				continue;
			if (o.getDate() == null || o.getSplit() == null)
				continue;

			// リスト中から該当日を見つけます。
			for (int i = pos; i < length; i++) {
				final int c = getDate(i).compareTo(o.getDate());
				if (c == 0) {
					results[i] = o.getSplit().doubleValue();
					pos = i + 1;
					break;
				} else if (c > 0) {
					break;
				}
			}
		}
		setSplit(results);
	}

	/**
	 * 足の単位を返します。
	 * 
	 * @return 足の単位
	 */
	public final Interval getInterval() { return interval; }

	/**
	 * データの長さを返します。
	 * 
	 * @return データの長さ
	 */
	public final int getLength() { return date == null ? 0 : date.length; }

	/**
	 * 日時データを返します。
	 * 
	 * @return 日時データ
	 */
	public final Date[] getDate() { return date; }

	/**
	 * 日時データを設定します。
	 * 
	 * @param date 日時データ
	 */
	private final void setDate(final Date[] date) { this.date = date; }

	/**
	 * 日時を返します。
	 * 
	 * @param i インデックス
	 * @return 日時
	 */
	public final Date getDate(final int i) { return date[i]; }

	/**
	 * <p>指定された日時以後 (<code>null</code> 可) を含む最初の位置を返します。</p>
	 * 
	 * @param find 検索する日時 (<code>null</code> 可)
	 * @return 指定された日時以後を含む最初の位置。見つからない場合は <code>-1</code>
	 * @since 1.1
	 */
	public final int indexOf(final Date find) { return ArrayDataUtils.indexOf(date, find); }

	/**
	 * <p>指定された日時以前 (<code>null</code> 可) を含む最後の位置を返します。</p>
	 * 
	 * @param find 検索する日時 (<code>null</code> 可)
	 * @return 指定された日時以前を含む最後の位置。見つからない場合は <code>-1</code>
	 * @since 1.1
	 */
	public final int lastIndexOf(final Date find) { return ArrayDataUtils.lastIndexOf(date, find); }

	/**
	 * <p>指定された日時以後又は <code>null</code> でない最初の位置を返します。</p>
	 * 
	 * @param find 検索する日時
	 * @return 指定された日時以後又は <code>null</code> でない最初の位置。見つからない場合は <code>-1</code>
	 * @since 1.1
	 */
	public final int defaultIndexOf(final Date find) {
		int result = INDEX_NOT_FOUND;
		if (find != null)
			result = ArrayDataUtils.indexOf(date, find);
		if (result != INDEX_NOT_FOUND)
			return result;
		return ArrayDataUtils.indexOfNotNull(date);
	}

	/**
	 * <p>指定された日時以前又は <code>null</code> でない最後の位置を返します。</p>
	 * 
	 * @param find 検索する日時
	 * @return 指定された日時以前又は <code>null</code> でない最後の位置。見つからない場合は <code>-1</code>
	 * @since 1.1
	 */
	public final int defaultLastIndexOf(final Date find) {
		int result = INDEX_NOT_FOUND;
		if (find != null)
			result = ArrayDataUtils.lastIndexOf(date, find);
		if (result != INDEX_NOT_FOUND)
			return result;
		return ArrayDataUtils.lastIndexOfNotNull(date);
	}

	/**
	 * 指定された日時間の期間を返します。
	 * 
	 * @param start 検索する開始日時
	 * @param end 検索する終了日時
	 * @return 期間
	 * @since 1.1
	 */
	public final int getPeriod(final Date start, final Date end) {
		final int startIndex = indexOf(start);
		final int endIndex = lastIndexOf(end);
		if (startIndex != INDEX_NOT_FOUND && endIndex != INDEX_NOT_FOUND)
			return endIndex - startIndex + 1;
		return 0;
	}

	/**
	 * 指定された4本値の種類に対応する価格データを返します。
	 * 
	 * @param type 4本値の種類
	 * @return 価格データ。又は <code>null</code>
	 * @since 1.1
	 */
	public final Number[] getPrice(final FourPrice type) {
		switch (type) {
			case OPEN:
				return getOpen();
			case HIGH:
				return getHigh();
			case LOW:
				return getLow();
			case CLOSE:
				return getClose();
			default:
				return null;
		}
	}

	/**
	 * 始値データを返します。
	 * 
	 * @return 始値データ
	 */
	public final Number[] getOpen() { return getData(DefaultDataType.OPEN); }

	/**
	 * 始値データを設定します。
	 * 
	 * @param open 始値データ
	 */
	private final void setOpen(final Number[] open) { dataset.put(DefaultDataType.OPEN, open); }

	/**
	 * 始値を返します。
	 * 
	 * @param i インデックス
	 * @return 始値
	 */
	public final Number getOpen(final int i ) { return getData(DefaultDataType.OPEN)[i]; }

	/**
	 * 高値データを返します。
	 * 
	 * @return 高値データ
	 */
	public final Number[] getHigh() { return getData(DefaultDataType.HIGH); }

	/**
	 * 高値データを設定します。
	 * 
	 * @param high 高値データ
	 */
	private final void setHigh(final Number[] high) { setData(DefaultDataType.HIGH, high); }

	/**
	 * 高値を返します。
	 * 
	 * @param i インデックス
	 * @return 高値
	 */
	public final Number getHigh(final int i ) { return getData(DefaultDataType.HIGH)[i]; }

	/**
	 * 安値データを返します。
	 * 
	 * @return 安値データ
	 */
	public final Number[] getLow() { return getData(DefaultDataType.LOW); }

	/**
	 * 安値データを設定します。
	 * 
	 * @param low 安値データ
	 */
	private final void setLow(final Number[] low) { setData(DefaultDataType.LOW, low); }

	/**
	 * 安値を返します。
	 * 
	 * @param i インデックス
	 * @return 安値
	 */
	public final Number getLow(final int i) { return getData(DefaultDataType.LOW)[i]; }

	/**
	 * 終値データを返します。
	 * 
	 * @return 終値データ
	 */
	public final Number[] getClose() { return getData(DefaultDataType.CLOSE); }

	/**
	 * 終値データを設定します。
	 * 
	 * @param close 終値データ
	 */
	private final void setClose(final Number[] close) { setData(DefaultDataType.CLOSE, close); }

	/**
	 * 終値を返します。
	 * 
	 * @param i インデックス
	 * @return 終値
	 */
	public final Number getClose(final int i) { return getData(DefaultDataType.CLOSE)[i]; }

	/**
	 * 出来高データを返します。
	 * 
	 * @return 出来高データ。出来高データが存在しない場合は <code>null</code>
	 */
	public final Number[] getVolume() { return getData(DefaultDataType.VOLUME); }

	/**
	 * 出来高データを設定します。
	 * 
	 * @param volume 出来高データ
	 */
	private final void setVolume(final Number[] volume) { setData(DefaultDataType.VOLUME, volume); }

	/**
	 * 出来高を返します。
	 * 
	 * @param i インデックス
	 * @return 出来高。出来高データが存在しない場合は <code>null</code>
	 */
	public final Number getVolume(final int i ) { return getData(DefaultDataType.VOLUME)[i]; }

	/**
	 * 信用売残データを返します。
	 * 
	 * @return 信用売残データ
	 */
	public final Number[] getSold() { return getData(DefaultDataType.SOLD); }

	/**
	 * 信用売残データを設定します。
	 * 
	 * @param sold 信用売残データ
	 */
	private final void setSold(final Number[] sold) { setData(DefaultDataType.SOLD, sold); }

	/**
	 * 信用売残を返します。
	 * 
	 * @param i インデックス
	 * @return 信用売残
	 */
	public final Number getSold(final int i) { return getData(DefaultDataType.SOLD)[i]; }

	/**
	 * 信用買残データを返します。
	 * 
	 * @return 信用買残データ
	 */
	public final Number[] getBought() { return getData(DefaultDataType.BOUGHT); }

	/**
	 * 信用買残データを設定します。
	 * 
	 * @param bought 信用買残データ
	 */
	private final void setBought(final Number[] bought) { setData(DefaultDataType.BOUGHT, bought); }

	/**
	 * 信用買残を返します。
	 * 
	 * @param i インデックス
	 * @return 信用買残
	 */
	public final Number getBought(final int i) { return getData(DefaultDataType.BOUGHT)[i]; }

	/**
	 * 株式分割数データを返します。
	 * 
	 * @return 株式分割数データ
	 */
	public final Number[] getSplit() { return getData(DefaultDataType.SPLIT); }

	/**
	 * 株式分割数データを設定します。
	 * 
	 * @param split 株式分割数データ
	 */
	private final void setSplit(final Number[] split) { setData(DefaultDataType.SPLIT, split); }

	/**
	 * 株式分割数を返します。
	 * 
	 * @param i インデックス
	 * @return 株式分割数
	 */
	public final Number getSplit(final int i) { return getData(DefaultDataType.SPLIT)[i]; }

	/**
	 * <p>指定された時系列データを返します。</p>
	 * サブクラスはこのメソッドをオーバーライドして指定されたキーのデータが存在しない場合に、
	 * データを構築して返すなどの実装を追加できます。
	 * 
	 * @param type 時系列データの種類
	 * @return 指定された種類のデータ。指定された種類のデータが存在しない場合は <code>null</code>
	 */
	protected Number[] getData(final DataType type) { return dataset.get(type); }

	/**
	 * 指定された時系列データを設定します。
	 * 
	 * @param type 時系列データの種類
	 * @param data 時系列データ
	 * @throws IllegalArgumentException 指定された時系列データと、このクラスの日時データの長さが一致しない場合
	 */
	protected final void setData(final DataType type, final Number[] data) {
		if (!ArrayDataUtils.isSameLength(getDate(), data))
			throw new IllegalArgumentException();
		dataset.put(type, data);
	}

	/**
	 * このデータセットの4本値データが株式分割計算済みかどうかを返します。
	 * 
	 * @return このデータセットの4本値データが株式分割計算済みかどうか
	 */
	public final boolean isSplited() { return splited; }

	/**
	 * <p>株式分割修正済みのデータセットを返します。</p>
	 * このデータセットが株式分割修正済みの場合や、株式分割データを保持していない場合は、このデータセットを返します。
	 * 
	 * @return 株式分割修正済みデータセット
	 * @see TechnicalAnalysis#split(Number[], Number[], Number[], Number[], Number[])
	 */
	public final TimeSeriesDataset split() {
		if (isSplited() || getSplit() == null)
			return this;

		final TimeSeriesDataset result = new TimeSeriesDataset(this);
		final Map<FourPrice, Number[]> prices = TechnicalAnalysis.split(getOpen(), getHigh(), getLow(), getClose(), getSplit());
		result.setOpen(prices.get(FourPrice.OPEN));
		result.setHigh(prices.get(FourPrice.HIGH));
		result.setLow(prices.get(FourPrice.LOW));
		result.setClose(prices.get(FourPrice.CLOSE));
		result.splited = true;
		return result;
	}

	/**
	 * <p>このデータセットのデータ群を、指定された長さだけ拡張します。</p>
	 * 拡張する長さに正数が指定された場合はデータの後部を拡張します。<br>
	 * 拡張する長さに負数が指定された場合はデータの前部を拡張します。<br>
	 * 拡張する長さに <code>0</code> が指定された場合は、何も行いません。
	 * 
	 * @param space 拡張する長さ
	 */
	public void extend(final int space) {
		if (space == 0)
			return;

		final Map<DataType, Number[]> dataset = new HashMap<DataType, Number[]>((int) Math.ceil(this.dataset.size() * 1.5));
		for (final Map.Entry<DataType, Number[]> entry : this.dataset.entrySet())
			dataset.put(entry.getKey(), ArrayDataUtils.extend(entry.getValue(), space));

		this.date = ArrayDataUtils.extend(this.date, space);
		this.dataset = dataset;
	}

	/**
	 * <p>指定されたデータセットを指定された足単位へ変換して返します。</p>
	 * 
	 * @param target 変換する足単位
	 * @return 変換されたデータセット
	 */
	public TimeSeriesDataset compress(final Interval target) {
		return compress(target, Calendar.getInstance());
	}

	/**
	 * <p>指定されたデータセットを指定された足単位へ変換して返します。</p>
	 * 
	 * @param target 変換する足単位
	 * @param calendar 足単位変換で日時の基準とするカレンダーオブジェクト
	 * @return 変換されたデータセット
	 */
	public TimeSeriesDataset compress(final Interval target, final Calendar calendar) {
		return compress(target, calendar, new DateTruncater());
	}

	/**
	 * <p>指定されたデータセットを指定された足単位へ変換して返します。</p>
	 * 
	 * @param target 変換する足単位
	 * @param calendar 足単位変換で日時の基準とするカレンダーオブジェクト
	 * @param truncater 変換する足単位へ日時の区切りを判断する基準
	 * @return 変換されたデータセット
	 * @throws IllegalArgumentException 指定された足単位が、このクラスの足単位の精度より小さい場合
	 */
	public TimeSeriesDataset compress(final Interval target, final Calendar calendar, final DateTruncater truncater) {
		// 変更単位を検証し、より小さい足への変換は例外をスローさせます。
		if (this.interval.ordinal() > target.ordinal())
			throw new IllegalArgumentException();
		if (this.interval == target)
			return this;

		final int mapCapacity = (int) Math.ceil(this.dataset.size() * 1.5);
		final List<Date> compress_date = new ArrayList<Date>();
		final Map<DataType, List<Number>> compress_values = new HashMap<DataType, List<Number>>(mapCapacity);

		// 一時保存用
		boolean init = false;	// 一時保存用変数群の初期設定をしたかどうか
		Date _date = null;		// 一時保存用の結果キー
		Map<DataType, Number> _values = null;	// 一時保存用の結果値
		Calendar term = truncater.truncate(calendar, Calendar.YEAR);	// 次足データの基準とする日時

		// 日時の切捨て用カレンダーと切捨て精度
		final Calendar c = (Calendar) calendar.clone();
		final int truncate = getTruncate(target);

		// データを繰返し処理して全て処理します。
		final int max = getLength();
		final Number[] open  = getOpen();
		final Number[] high  = getHigh();
		final Number[] low   = getLow();
		final Number[] close = getClose();
		for (int i = 0; i < max; i++) {
			final Date date = this.date[i];
			if (date == null)
				continue;

			// 現データの日時を次足データ基準日時と比較出来る様に切捨て日時を求めます。
			c.setTime(date);
			final Calendar current = truncater.truncate(c, truncate);
			
			// 現データの日時が次足データ基準日時以降の場合は、一時保存していたキーと値を結果へ出力します。
			if (init && current.after(term)) {
				compress_date.add(_date);
				add(_values, compress_values);
				init = false;
			}
			// 繰返し処理初回時や、結果への出力後は一時保存用変数群の初期設定を行います。
			if (!init) {
				_date = (Date) date.clone();
				_values = new HashMap<DataType, Number>(mapCapacity);
				term = current;
				init = true;
			}

			// 4本値が空の場合は処理をスキップします。
			if (open[i] == null || high[i] == null || low[i] == null || close[i] == null)
				continue;

			for (final Map.Entry<DataType, Number[]> entry : this.dataset.entrySet()) {
				final DataType type = entry.getKey();
				final Number[] data = entry.getValue();
				if (type == null || data == null)
					continue;

				if (_values.get(type) == null) {
					_values.put(type, data[i] != null ? data[i].doubleValue() : null);
				} else if (data[i] != null) {
					switch (type.getCompressType()) {
						case FIRST:
							// 既に値があるので何もしない
							break;
						case HIGHEST:
							if (_values.get(type).doubleValue() < data[i].doubleValue())
								_values.put(type, data[i].doubleValue());
							break;
						case LOWEST:
							if (_values.get(type).doubleValue() > data[i].doubleValue())
								_values.put(type, data[i].doubleValue());
							break;
						case LAST:
							// 常に更新
							_values.put(type, data[i].doubleValue());
							break;
						case SUM:
							// 加算する
							_values.put(type, _values.get(type).doubleValue() + data[i].doubleValue());
							break;
						default:
							throw new RuntimeException();
					}
				}
			}
		}

		if (_date != null) {
			compress_date.add(_date);
			add(_values, compress_values);
		}

		final TimeSeriesDataset result = new TimeSeriesDataset(target);
		result.date = compress_date.toArray(new Date[]{});
		for (final Map.Entry<DataType, List<Number>> entry : compress_values.entrySet())
			result.setData(entry.getKey(), entry.getValue().toArray(new Number[]{}));
		result.splited = this.splited;
		return result;
	}

	private static void add(final Map<DataType, Number> row, final Map<DataType, List<Number>> rows) {
		for (final Map.Entry<DataType, Number> entry : row.entrySet()) {
			final DataType key = entry.getKey();

			List<Number> list = rows.get(key);
			if (list == null)
				list = new ArrayList<Number>();

			list.add(entry.getValue());
			rows.put(key, list);
		}
	}

	/**
	 * 指定された足単位への変換で使用すべき日時切捨て精度を返します。
	 * 
	 * @param target 変換する足単位
	 * @return 日時切捨て精度
	 */
	private static int getTruncate(final Interval target) {
		switch (target) {
			case MONTHLY:
				return Calendar.DATE;
			case WEEKLY:
				return Calendar.DAY_OF_WEEK;
			case DAILY:
				return Calendar.HOUR_OF_DAY;
			case HOURLY:
				return Calendar.MINUTE;
			default:
				throw new IllegalArgumentException();
		}
	}

}
