(2012年の公休日とか)JTableのフィルタリング

これはJava Advent Calendar 2011の7番目のエントリーです。
≪前のエントリー#6:JUnit のセカイ:shuji_w6eさん*1
≫次のエントリー#8:AnnotationProcessorを利用して楽してintrefaceを徹底活用したプログラミングをしようぜ:t_yanoさん*2

facebookJava Advent Calendar 2011の告知に「いいね!」を押したら、ブログ書いてねというありがたいお言葉をいただき、よった勢いでJava Advent Calendar 2011 : ATNDに参加表明しちゃったはいいけど、ネタをどうしようとか思ってますw。

酔った勢いではありましたが、狙ったとおりに12/7*3をゲットできました。

2012年の日本の公休日

いきなりJavaぢゃないですけど、そろそろ2012年の準備もしなきゃいけないので。

名称 年月日 曜日
元日 2012/1/1
振替 2012/1/2
成人の日 2012/1/9
建国記念の日 2012/2/11
春分の日 2012/3/20
昭和の日 2012/4/29
振替 2012/4/30
憲法記念日 2012/5/3
みどりの日 2012/5/4
こどもの日 2012/5/5
海の日 2012/7/16
敬老の日 2012/9/17
秋分の日 2012/9/22
体育の日 2012/10/8
文化の日 2012/11/3
勤労感謝の日 2012/11/23
天皇誕生日 2012/12/23
振替 2012/12/24

国民の祝日について - 内閣府
これに某社の年末年始を加えて、日付のみを正規表現にすると、

'2012(0101|0102|0103|0104|0109|0211|0320|0429|0430|0503|0504|0505|0716|0917|0922|1008|1103|1123|1223|1224|1231)'
でしょうか。


redmine*4で日本の公休日などを表示

またまたJavaぢゃn(ry
徒然さめざめ Redmine hack! -カレンダーに休日色を-を参考にしました。

以下/usr/share/redmine/がインストール先として。

作成/修正ファイル一覧。


lib/date2.rb
public/javascripts/calendar/holiday.js
app/views/common/_calendar.rhtml
public/stylesheets/application.css
public/stylesheets/calendar.css
public/javascripts/calendar/calendar.js
public/themes/farend_basic/stylesheets/application.css
公休日の判定をRubyJavaScriptでやっているんですね。>Redmine
Rubyの方はカレンダーで、JavaScriptの方はDatePicker的なやつ。
公休日の正規表現をハードコーディングしています。ymlなファイルで外に出せたらよかったんですが…
スクリーンショットはないです。ごめんなさい。
あと、ガントチャートには公休日を表示しないポリシーということで。(^^;

lib/date2.rbを新規。

#
require "date"

class  Date2 < Date
  COMPANY_HOLIDAY =
  [
    [ "%a", 'Sun' ], #日曜はデフォルトで
    [ "%a", 'Sat' ], 
    [ "%Y%m%d", '2011(0101|0103|0104|0110|0211|0321|0429|0503|0504|0505|0718|0919|0923|1010|1103|1123|1223|1230)'],
    [ "%Y%m%d", '2012(0101|0102|0103|0104|0109|0211|0320|0429|0430|0503|0504|0505|0716|0917|0922|1008|1103|1123|1223|1224|1231)'],
  ]

  def holiday?
    COMPANY_HOLIDAY.each do |fil|
      return true if self.strftime(fil.first) =~ Regexp.new(fil.last)
    end
    return false
  end
end

public/javascripts/calendar/holiday.jsを新規

var holidays = [
  '2011(0101|0103|0104|0110|0211|0321|0429|0503|0504|0505|0718|0919|0923|1010|1103|1123|1223|1230)',
  '2012(0101|0102|0103|0104|0109|0211|0320|0429|0430|0503|0504|0505|0716|0917|0922|1008|1103|1123|1223|1224|1231)',
];

var ymd = function(date) {
  return date.getFullYear()
    + ('00' + (date.getMonth()+1)).slice(-2)
    + ('00' +  date.getDate()    ).slice(-2)
};

var isholiday = function(date) {
  var s = ymd(date);
  for (i=0; i<holidays.length; i++) {
    if (holidays[i] === undefined) {
	continue; }
    if (s.match(new RegExp(holidays[i]))) {
      return true;
    }
  }
  return false;
};

app/views/common/_calendar.rhtml を編集

 *** app/views/common/_calendar.rhtml.old 2011-10-12 15:33:52.663388381 +0900
 --- app/views/common/_calendar.rhtml 2011-10-12 16:25:00.933356562 +0900
***************
*** 5,13 ****
  <tbody>
  <tr>
  <% day = calendar.startdt
! while day <= calendar.enddt %>
  <%= "<td class='week-number' title='#{ l(:label_week) }'>#{(day+(11-day.cwday)%7).cweek}</td>" if day.cwday == calendar.first_wday %>
! <td class="<%= day.month==calendar.month ? 'even' : 'odd' %><%= ' today' if Date.today == day %>">
  <p class="day-num"><%= day.day %></p>
  <% calendar.events_on(day).each do |i| %>
    <% if i.is_a? Issue %>
--- 5,15 ----
  <tbody>
  <tr>
  <% day = calendar.startdt
! while day <= calendar.enddt
! ddd = Date2.new(day.year, day.month, day.day)
! %>
  <%= "<td class='week-number' title='#{ l(:label_week) }'>#{(day+(11-day.cwday)%7).cweek}</td>" if day.cwday == calendar.first_wday %>
! <td class="<%= day.month==calendar.month ? 'even' : 'odd' %><%= ' today' if Date.today == day %><%= ' holiday' if ddd.holiday? %>">
  <p class="day-num"><%= day.day %></p>
  <% calendar.events_on(day).each do |i| %>
    <% if i.is_a? Issue %>

public/stylesheets/application.cssを修正

 *** public/stylesheets/application.css.old 2011-10-12 15:32:23.453586046 +0900
 --- public/stylesheets/application.css 2011-10-12 16:55:49.854312806 +0900
***************
*** 550,555 ****
--- 550,556 ----
  table.cal td p.day-num {font-size: 1.1em; text-align:right;}
  table.cal td.odd p.day-num {color: #bbb;}
  table.cal td.today {background:#ffffdd;}
+ .holiday {background-color: #ffe4e1; color: red; }
  table.cal td.today p.day-num {font-weight: bold;}
  table.cal .starting a, p.cal.legend .starting {background: url(../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
  table.cal .ending a, p.cal.legend .ending {background: url(../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}

public/stylesheets/calendar.cssを編集

 *** public/stylesheets/calendar.css.old 2011-10-12 21:36:14.148784485 +0900
 --- public/stylesheets/calendar.css 2011-10-12 21:37:57.427756286 +0900
***************
*** 125,130 ****
--- 125,134 ----
    color: #f00;
  }
  
+ div.calendar tbody td.holiday { /* Cell showing selected date */
+   background-color: #FFE4E1;
+ }
+ 
  div.calendar tbody .disabled { color: #999; }
  
  div.calendar tbody .emptycell { /* Empty cells (the best is to hide them) */

public/javascripts/calendar/calendar.jsを編集

 *** public/javascripts/calendar/calendar.js.old 2011-07-11 20:47:24.000000000 +0900
 --- public/javascripts/calendar/calendar.js 2011-10-12 21:34:11.937288664 +0900
***************
*** 1177,1182 ****
--- 1177,1185 ----
       cell.className += " today";
       cell.ttip += Calendar._TT["PART_TODAY"];
      }
+     if (isholiday(date)) {
+      cell.className += " holiday";
+     }
      if (weekend.indexOf(wday.toString()) != -1)
       cell.className += cell.otherMonth ? " oweekend" : " weekend";
     }

テーマを使っている場合、
public/themes/farend_basic/stylesheets/application.cssなども編集。

 *** public/themes/farend_basic/stylesheets/application.css.old 2011-10-12 16:57:55.483273982 +0900
 --- public/themes/farend_basic/stylesheets/application.css 2011-10-12 16:58:25.624123111 +0900
***************
*** 582,587 ****
--- 582,588 ----
  table.cal td p.day-num {font-size: 1.1em; text-align:right;}
  table.cal td.odd p.day-num {color: #bbb;}
  table.cal td.today {background:#ffffdd;}
+ table.cal td.holiday {background:#FFE4E1;}
  table.cal td.today p.day-num {font-weight: bold;}
  table.cal .starting a, p.cal.legend .starting {background: url(../../../images/bullet_go.png) no-repeat -1px -2px; padding-left:16px;}
  table.cal .ending a, p.cal.legend .ending {background: url(../../../images/bullet_end.png) no-repeat -1px -2px; padding-left:16px;}

JTableのソートやフィルタリング

上の公休日やredmineと無関係ですが。
(やっとJavaの話題です。)
JavaSE6から、JTableのソートやフィルタリングができる様になったので、業務で使ってます。
さすがにそっちは公開できないので、できたてほやほやのソースを晒します。

テキストフィールドの

java.awt.event.KeyListener#keyReleased(KeyEvent)
で、フィルターを設定しているので、インクリメンタルサーチぽい動きをします。

JavaAdventCalendar2011.zip 直
「Java SE 6完全攻略」第24回 JTableクラスでのフィルタリング | 日経 xTECH(クロステック)を参考にしました。

以下は実行中のスクリーンショット
起動直後。

テキストフィールドに「2」を入力したところ。

テキストフィールドに「^\d\d\d$」*5を入力したところ。

テキストフィールドに「ほし」と入力し、インプットメソッドで変換候補「☆」を選んだところ。ただしまだ確定はしていない。

あと、見出しをクリックすると並べ替えもしてくれますね。
いちど並べ替えると、並べ替えないにできないのでリセットボタンをでっちあげました。

動作環境は

$ java -version
java version "1.7.0"
Java(TM) SE Runtime Environment (build 1.7.0-b147)

$ uname -a
Linux *** 2.6.38-13-generic #52-Ubuntu SMP Tue Nov 8 16:53:51 UTC 2011 x86_64 x86_64 x86_64 GNU/Linux

$ cat /etc/lsb-release 
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=11.04
DISTRIB_CODENAME=natty
DISTRIB_DESCRIPTION="Ubuntu 11.04"

さいごにソースを晒しますね。

package jp.ne.hatena.d.ttmmrr.javaadventcalendar2011;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.PatternSyntaxException;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

import javax.swing.BoxLayout;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.RowFilter;
import javax.swing.SwingUtilities;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableRowSorter;

/**
 * @author ttmmrr
 */
@SuppressWarnings("serial")
public class WishList extends JFrame {
    public WishList() {
        super();

        final ItemTable table = new ItemTable();
        final KeywordPanel pnlKeyword = new KeywordPanel(table);

        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        final Container pane = getContentPane();
        pane.setLayout(new BorderLayout());
        pane.add(pnlKeyword, BorderLayout.NORTH);
        pane.add(new JScrollPane(table), BorderLayout.CENTER);
        pack();
        setVisible(true);
    }

    public static void main(final String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new WishList();
            }
        });
    }

    static class KeywordPanel extends JPanel {
        final ItemTable _table;
        final JTextField _txtKeyWord = new JTextField(30/*columns*/);
        final JButton _btnReset = new JButton("リセット");

        public KeywordPanel(final ItemTable table) {
            super();
            _table = table;
            setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
            add(_txtKeyWord);
            add(_btnReset);

            _txtKeyWord.addKeyListener(new KeyAdapter() {
                @Override public void keyReleased(final KeyEvent e) {
                    final String keyword = _txtKeyWord.getText();
                    _table.search(keyword);
                }
            });
            _btnReset.addActionListener(new ActionListener() {
                @Override public void actionPerformed(ActionEvent e) {
                    _txtKeyWord.setText("");
                    _table.reset();
                }
            });
        }
    } // KeyWordPanel

    static class ItemTable extends JTable {
        final ItemModel _model = new ItemModel();
        final TableRowSorter<ItemModel> _rowSorter;

        public ItemTable() {
            super();
            setModel(_model);
            _rowSorter = new TableRowSorter<>(_model);

            setRowHeight(150);

            setCellSelectionEnabled(false);
            setColumnSelectionAllowed(false);
            setRowSelectionAllowed(true);

            reset();
        }

        public void search(final String text) {
            if (null == text || "".equals(text)) {
                _rowSorter.setRowFilter(null);
            } else {
                RowFilter<Object, Object> regexFilter;
                try {
                    regexFilter = RowFilter.regexFilter(text); // int...indicesを省略
                } catch (final PatternSyntaxException pse) {
                    regexFilter = null;
                }
                _rowSorter.setRowFilter(regexFilter);
            }
        }

        void reset() {
            resetTable();
            resetWidths();
        }
        void resetTable() {
            _rowSorter.setSortKeys(null);
            _rowSorter.setRowFilter(null);
            setRowSorter(_rowSorter);
            getTableHeader().repaint();
        }

        void resetWidths() {
            final TableColumnModel cm = getColumnModel();
            for (int i = 0; i < WIDTHS.length; i++) {
                final int[] widths = WIDTHS[i];
                final TableColumn c = cm.getColumn(i);
                if (0 <= widths[0]) {
                    c.setMinWidth(widths[0]);
                }
                if (0 <= widths[1]) {
                    c.setPreferredWidth(widths[1]);
                }
                if (0 <= widths[2]) {
                    c.setMaxWidth(widths[2]);
                }
            }
        }
    } // ItemTable

    static class ItemModel extends AbstractTableModel {
        final Map<Item, ImageIcon> _images = new HashMap<>();

        public ItemModel() {
            super();
            for (final Item item : ITEMS) {
                URL url = item.getImageURL();
                ImageIcon image = null;
                if (null == url) {
                } else {
                    image = new ImageIcon(url);
                }
                _images.put(item, image);
            }
        }

        /** {@inheritDoc} */
        @Override public int getRowCount() { return ITEMS.length; }

        /** {@inheritDoc} */
        @Override public int getColumnCount() { return 3; }

        /** {@inheritDoc} */
        @Override public String getColumnName(final int column) {
            if (0 == column) {
                return "価格";
            } else if (1 == column) {
                return "イメージ";
            } else if (2 == column) {
                return "品名";
            }
            return super.getColumnName(column);
        }

        /** {@inheritDoc} */
        @Override public Object getValueAt(final int rowIndex, final int columnIndex) {
            if (rowIndex < 0 || ITEMS.length <= rowIndex) { return ""; }
            if (columnIndex < 0 || 3 <= columnIndex) { return ""; }
            if (0 == columnIndex) {
                return Integer.valueOf(ITEMS[rowIndex].getPrice());
            }
            if (1 == columnIndex) {
                return _images.get(ITEMS[rowIndex]);
            }
            if (2 == columnIndex) {
                return ITEMS[rowIndex].getName();
            }
            return "";
        }

        /** {@inheritDoc} */
        @Override public Class<?> getColumnClass(final int columnIndex) {
            if (0 == columnIndex) {
                return Integer.class;
            } else if (1 == columnIndex) {
                return ImageIcon.class;
            } else if (2 == columnIndex) {
                return String.class;
            }
            return Object.class;
        }
    } // ItemModel

    static final int[][] WIDTHS = {
        // min,pref,max
        {40, 120, 120}, {150, 150, 150}, {150, 600, Integer.MAX_VALUE},
    };

    static final Item[] ITEMS = new Item[] {
        new Item(34966, "住友スリーエム 3M ポケットプロジェクター ブラック MP180", "http://ecx.images-amazon.com/images/I/41gF9aI6o7L._SL500_SL135_.jpg"),
        new Item(46980, "aigo SiLK シリーズ 小型プロジェクター nano PT6216", "http://ecx.images-amazon.com/images/I/41UZhMjShzL._SL500_SL135_.jpg"),
        new Item(299, "【メール便 送料無料】 Hanwha ハイスピード HDMIケーブル 1m [3D/イーサネット対応] [HDMI Ver1.4] [1メートル] [PS3/Xbox360対応]&#8203; UMA-HDMI10", "http://ecx.images-amazon.com/images/I/51zrMzW5n3L._SL500_SL135_.jpg"),
        new Item(5145, "魔法少女まどか☆マギカ 6 【完全生産限定版】 [Blu-ray]", "http://ecx.images-amazon.com/images/I/515W7cHVttL._SL500_SL135_.jpg"),
        new Item(5774, "魔法少女まどか☆マギカ 4 【完全生産限定版】 [Blu-ray]", "http://ecx.images-amazon.com/images/I/51Vk2uZjtTL._SL500_SL135_.jpg"),
        new Item(2300, "figma 魔法少女まどか☆マギカ 鹿目まどか", "http://ecx.images-amazon.com/images/I/41c3PopV8bL._SL500_SL135_.jpg"),
        new Item(2336, "figma 魔法少女まどか☆マギカ 巴マミ", "http://ecx.images-amazon.com/images/I/412351Rb2sL._SL500_SL135_.jpg"),
        new Item(5341, "魔法少女まどか☆マギカ 5 【完全生産限定版】 [Blu-ray]", "http://ecx.images-amazon.com/images/I/515jYWFP0YL._SL500_SL135_.jpg"),
        new Item(5900, "魔法少女まどか☆マギカ 3 【完全生産限定版】 [Blu-ray]", "http://ecx.images-amazon.com/images/I/51dFkKTKYiL._SL500_SL135_.jpg"),
        new Item(3630, "魔法少女まどか☆マギカ 2 【完全生産限定版】 [Blu-ray]", "http://ecx.images-amazon.com/images/I/51CS8vsUzZL._SL500_SL135_.jpg"),
        new Item(5900, "魔法少女まどか☆マギカ 1 【完全生産限定版】 [Blu-ray]", "http://ecx.images-amazon.com/images/I/51kDXEQIAnL._SL500_SL135_.jpg"),
        new Item(4980, "20世紀少年<第2章> 最後の希望 [Blu-ray]", "http://ecx.images-amazon.com/images/I/51-RUew9nvL._SL500_SL135_.jpg"),
        new Item(3444, "20世紀少年 第1章 終わりの始まり [Blu-ray]", "http://ecx.images-amazon.com/images/I/51Go1f2a9HL._SL500_SL135_.jpg"),
        new Item(620, "数学ガール 下 (MFコミックス フラッパーシリーズ)", "http://ecx.images-amazon.com/images/I/518-%2BGvqJ1L._SL500_SL135_.jpg"),
        new Item(1890, "数学ガール/フェルマーの最終定&#8203;理", "http://ecx.images-amazon.com/images/I/51CkaKAKglL._SL500_PIsitb-sticker-arrow-big,TopRight,35,-73_OU09_SL135_.jpg"),
        new Item(1890, "数学ガール/ゲーデルの不完全性&#8203;定理", "http://ecx.images-amazon.com/images/I/41wtOpDHFSL._SL500_PIsitb-sticker-arrow-big,TopRight,35,-73_OU09_SL135_.jpg"),
        new Item(620, "数学ガール 上 (MFコミックス フラッパーシリーズ)", "http://ecx.images-amazon.com/images/I/51AlFOZhUVL._SL500_SL135_.jpg"),
    };

    static class Item {
        final int _price;
        final String _name;
        final URL _imageUrl;

        Item(final int price, final String name, final String image) {
            super();
            _price = price;
            _name = name;
            URL temp;
            try {
                temp = new URL(image);
            } catch (final MalformedURLException e) {
                temp = null;
            }
            _imageUrl = temp;
        }

        public int getPrice() { return _price; }
        public String getName() { return _name; }
        public URL getImageURL() { return _imageUrl; }

        /** {@inheritDoc} */
        @Override public int hashCode() {  return _price * _name.hashCode(); }

        /** {@inheritDoc} */
        @Override public boolean equals(final Object obj) {
            if (null == obj) { return false; }
            if (this == obj) { return true; }
            if (!(obj instanceof Item)) { return false; }
            final Item that = (Item) obj;
            boolean rtn;
            rtn = getPrice() == that.getPrice();
            if (!rtn) { return rtn; }
            rtn = getName().equals(that.getName());
            if (!rtn) { return rtn; }
            return true;
        }

        /** {@inheritDoc} */
        @Override
        public String toString() {
            return _price + "," + _name;
        }
    } // Item
} // WishList

これはJava Advent Calendar 2011の7番目のエントリーです。
≪前のエントリー#6:JUnit のセカイ:shuji_w6eさん
≫次のエントリー#8:AnnotationProcessorを利用して楽してintrefaceを徹底活用したプログラミングをしようぜ:t_yanoさん

*1:札幌にお住まいなんですね。北海道はまだ行ったことないです

*2:東京都中野区か新宿区にお住まいなんですね。私も十数年前に中野6丁目とか上落合2丁目に住んでましたよ

*3:http://www.amazon.co.jp/gp/registry/wishlist/ref=wish_list

*4:redmine 1.2.1

*5:正規表現で3桁の数値