ComponentOne Information

ComponentOne Studio/Wijmo/Xuniの最新情報を公開中

内閣府が公開した国民の祝日をカレンダーに表示する

国民的アニメに登場する少年が、6月は祝日も学校の休みも関係がなくつまらない月だと嘆き、勝手に祝日を作ってしまうお話がありました。
このエピソードにより、当時子供だった我々に「6月には祝日がない」という悲しい事実が深く記憶されたものです。

あれから数十年が経過した現在でも6月に祝日はありません。
3連休を増やすハッピーマンデー制度が登場し、7月と8月にも祝日が追加されましたが、6月はそのままです。6月だけは祝日がありません。もちろん勝手に追加できる制度もありません。

このように、日本の祝日は「国民の祝日に関する法律」で定められています。日付が変わる祝日や2つの祝日に挟まれる形で成立する祝日もあるので変動です。内閣府から確定した国民の祝日として公表されており、CSVファイル形式のオープンデータとして公開中です。

公開されている国民の祝日データがあるのは便利です。これを情報として取り込んで利用できます。 この記事では、国民の祝日データをXamarin.Fomrsで開発するアプリでカレンダーに表示する手順を解説します。 Xuni(ズーニー)のコンポーネントであるCalendarを使用するサンプルです。

内閣府が数年分を提供していることからもわかりますが、祝日は変則的な法則で決められているのでシステムに組み込む場合は工夫が必要です。
また、振替休日も取り入れたり、昨今では春節など日本以外の祝日にも対応要求がでてきます。
それらをロジックで対応していくのは困難です。祝日が追加された場合は対応できません。

そうなると設定ファイルを用意して読み込むのが現実的な手法になります。データを外部に切り出しておけばメンテナンスも容易です。
このサンプルはそのような状況を想定しました。


サンプルプロジェクトの作成

Xamarin.Formsを利用し、C#でAndroid、iOSのサンプルアプリを作成します。
Xuniでカレンダーを表示するアプリです。手順は以下になります。

  1. Xamarin.Formsのプロジェクトを作成
  2. Xuniを利用する設定
  3. CSVファイルを読み込み国民の祝日リストを作成
  4. リストを表示するクラスを作成
  5. 表示する画面の設定

1と2の説明は省略します。評価用のライセンスを組み込み済みでXuniの利用準備が整っているプロジェクトを公開していますので、これを改造して利用します。プロジェクトは以下からダウンロード可能です。

国民の祝日データを読み込む

CSVファイルの形で前述のWebサイトで公開されていますので、そのURLを指定して読み込むことができます。頻繁に内容が更新されるデータではありませんので、ダウンロードしたものをCSVファイルとしてプロジェクトに組み込み、それを読んで利用することも可能です。 なお、Xamarin.FormsでCSVファイルを読み込み、利用する手順については別記事で紹介しています。

また、Web上のCSVファイルの形式が変更された場合に、アプリ側の修正が必要になります。ここで紹介しているサンプルも、2017年3月1日時点で公開されている形式に基づいています。

実際に2017年2月28日までは、異なる形式で公開されていました。今回の更新ではよりアプリで利用しやすい形式のCSVに改良されています。
CSVファイルをVisual Studio Codeの Excel Viewerアドインを利用して表示したのが以下です。 IT利活用の視点で使いやすいのは右側であることがわかります。

f:id:ComponentOne_JP:20170302153650p:plain


Microsoft.Net.Httpの追加

それでは本題に戻ります。 Xamarin.Formsで、Web上に公開されたデータにHttp通信でアクセスしファイルを入手するにはHttpClientを利用します。
これはNuGetサーバーに公開されていますので、PCLプロジェクトにパッケージを追加してインストールします。
パッケージの追加でhttpをキーワードに検索すると表示されるMicrosoft.Net.Httpが対象です。

f:id:ComponentOne_JP:20170302161017p:plain

これをインストールすると以下がパッケージとして追加されます。下段の2つは自動で入ります。

  • Microsoft.Net.Http
    • Microsoft.Bcl
    • Microsoft.Bcl.Build
データを読み込むクラスを作成

次に、通信してデータを読み込むクラスを作成します。ここではHolidayDataSource.csという名称で、PCLプロジェクトにファイルを追加します。
このクラスでは、Http通信でCSVファイルを読み込み、国民の祝日データをDictionaryとして格納します。
Webアクセスを含むので、非同期処理のAsyncGetWebAPIDataメソッドを作成する機能の実装です。

まずはHttpClientクラスをインスタンス化して利用し、指定したURLからシンプルな形でファイルを取得する処理です。ここでは直接CSVファイルのURLを指定し、非同期にSystem.Text.Streamクラスのオブジェクトに読み込みます。 メソッド内で利用する定義は以下です。Dictionaryは他のクラスで利用するのであらかじめpublicで定義しておきます。

// 祝日を格納するDictionaryの定義
public Dictionary<DateTime, string> holidaysDic;
// データのURLを設定
private static readonly string URL = "http://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv";
// HttpClientの定義
private static HttpClient httpClient;

HttpClientを利用する部分のコードの抜粋です。

// HttpClientの作成 
httpClient = new HttpClient();
//非同期でWebからデータを取得してStreamに格納
Task<Stream> response = httpClient.GetStreamAsync(URL);
Stream result = await response;

Streamに読み込んだデータは1行づつ処理します。
データをみると1行目はヘッダーなので読み飛ばし、2行目以降を、ヘッダーに記述された形式「国民の祝日月日,国民の祝日名称」のデータとして読み込みます。
読み込んだデータは、年月日をキーにしたDictionaryに、祝日名称を値として格納します。

読み込み時はデータのエンコードを指定する必要があります。
このCSVデータはShift-JISでエンコードされたテキストとして公開されていますので、Shift-JISを指定して読み込みます。これまでの部分に読み込みを追加したコードの全体が以下です。

public class HolidaysDataSource
{
    // 祝日リスト用のDictionaryの定義
    public Dictionary<DateTime, string> holidaysDic;
    // データのURLを設定
    private static readonly string URL = "http://www8.cao.go.jp/chosei/shukujitsu/syukujitsu.csv";
    // HttpClientの定義
    private static HttpClient httpClient;

    public async Task<Dictionary<DateTime, string>> AsyncGetWebAPIData()
    {
        // 祝日を格納するDictionaryの作成
        holidaysDic = new Dictionary<DateTime, string>();
        // HttpClientの作成 
        httpClient = new HttpClient();
        //非同期でWebからデータを取得してStreamに格納
        Task<Stream> response = httpClient.GetStreamAsync(URL);
        Stream result = await response;

        // エンコードをShift_JISに設定
        var encoding_SJIS = System.Text.Encoding.GetEncoding("Shift_JIS");
        using (var sr = new System.IO.StreamReader(result, encoding_SJIS))
        {
            // ヘッダー部分の1行を読み飛ばす
            sr.ReadLine();
            while (!sr.EndOfStream)
            {
                //2行目以降1行ずつ読み込む
                var sn = sr.ReadLine().Split(',');
                if (sn.Length > 1 && sn[1].Trim().Length > 0)
                {
                    // 日付(DateTime)をキーにして、祝日名称をDictionaryに格納
                    holidaysDic[DateTime.Parse(sn[0].ToString())] = sn[1].ToString(); 
                }
            }
        }
        // Dictionary でデータを返す
        return holidaysDic;
    }
}

以上でCSVファイルからDictionary形式の国民の祝日リストができました。日付をキーにして祝日の名称が取得可能になります。

カレンダーを表示する画面レイアウトを作成

XuniのCalendarをXAMLに貼り付けることで、カレンダー表示が可能になります。 このカレンダーの1日を表示する領域は、カスタマイズしたレイアウトをテンプレートとして設定できるので、今回は下図のようなレイアウトにします。Gridレイアウトを利用した表組みです。

日付を左側に配置し、祝日の場合のみ画像領域に日の丸を表示し2段目は2つのセルを連結して祝日の名称を表示します。 このレイアウトが、月表示されたカレンダーの各日付領域になります。

f:id:ComponentOne_JP:20170302153826p:plain

この部分を表現するXAMLは以下です。

<!--  2行2列のGridレイアウトを定義 -->
<Grid HorizontalOptions="FillAndExpand" VerticalOptions="FillAndExpand">
    <Grid.RowDefinitions>
        <RowDefinition Height="20" />
        <RowDefinition Height="20" />
    </Grid.RowDefinitions>

    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="20" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <!--  上段左:日付を表示するLabel  上段右:画像を表示するImage  -->
    <Label 
            Grid.Row="0" Grid.Column="0"
            Margin="0,0,0,0" FontSize="15"
            VerticalOptions="FillAndExpand"
            HorizontalTextAlignment="Center"
            Text="{Binding Day}"
            TextColor="{Binding TextColor}" 
            />
    <Image 
            Grid.Row="0" Grid.Column="1"
            HeightRequest="9"
            Aspect="AspectFit"
            IsVisible="{Binding IsHoliday}"
            Source="{Binding FlagImage}" 
            />
    <!--  下段左右:祝日名称を表示するLabel  -->
    <Label 
            Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2"
            FontSize="8"
            HorizontalTextAlignment="Center"
            Text="{Binding HolidayName}" 
            />
</Grid>

LabelやImageに表示する内容は、データバインディング機能を利用します。 例えばLabelのTextプロパティは{Binding Day}、TextColorプロパティは{Binding TextColor}を指定しています。これは後述するビューモデルをコンテキストとしてバインディングする場合、データモデルのDayTextColorがバインディングソースになります。

作成したGridレイアウトは、Xuni CalendarのDaySlotTemplateとして定義しました。 最終的に以下のXAMLに設定した内容を、XuniCalendarに追加します。

<xuni:XuniCalendar x:Name="calendar">
    <xuni:XuniCalendar.DaySlotTemplate>
        <DataTemplate>
            <xuni:CalendarViewDaySlot>
                <xuni:CalendarViewDaySlot.View>

                <!--  この部分に作成したレイアウトが入る  -->

                </xuni:CalendarViewDaySlot.View>
            </xuni:CalendarViewDaySlot>
        </DataTemplate>
    </xuni:XuniCalendar.DaySlotTemplate>
</xuni:XuniCalendar>

カレンダーを表示するコードを作成

次に作成したXAMLのコードビハインドを作成します。
コードビハインドでは行う処理は以下です。 * 祝日用Dictionaryの作成指示 * カレンダーの設定 - 曜日の設定 - 日付領域の休日判断 - 日付領域に表示するデータのバインディング

まず、前に作成した祝日データの格納用のクラスをインスタンス化して実際の処理実行を指示します。 以下のような関数をコンストラクタから呼び出せるように定義します。

// 非同期でデータ取得のメソッドを実行する関数
async  Task getHolidayList()
{
    try
    {
        // 取得したデータをHolidaysListに設定
        HolidaysList = await new HolidaysDataSource().AsyncGetWebAPIData();
    }
    // エラー表示処理
    catch (System.Exception ex)
    {
        await DisplayAlert("Error", ex.Message.ToString(), "OK");
    }

}

コンストラクタではこの関数を呼び出します。

var task = getHolidayList();

次はカレンダーの設定です。カレンダーでは曜日領域と日付領域でカスタマイズした処理を入れます。日本では土曜日、日曜日そして祝日の色を平日と異なる色にするのが一般的です。その処理を追加します。 曜日領域はDayOfWeekSlotLoadingイベントで処理します。

calendar.DayOfWeekSlotLoading += (sender, e) =>
{
    // 曜日スロットを取得して曜日領域に表示する内容を設定
    var dayofweekSlot = e.DayOfWeekSlot as Xuni.Forms.Calendar.CalendarDayOfWeekSlot;
    dayofweekSlot.DayOfWeekTextColor = getColor(e.DayOfWeek); //曜日のテキスト色を設定
};

また日付領域については、祝日の設定とあわせてDaySlotLoadingイベントで処理します。 日付領域はDaySlotContextのビューモデルクラスを作成しそれですべてを管理します。以下がそのクラスです。

// 日付領域のビューモデル
public class DaySlotContext
{
    public DaySlotContext(DateTime date, Color color, string holidayName = null, bool isholiday = false)
    {
        this.Day = date.Day;
        this.TextColor = color;
        this.HolidayName = holidayName;
        this.IsHoliday = isholiday;

        if (isholiday)
        {
            // 祝日の場合のみリソースの画像を利用
            this.FlagImage = ImageSource.FromResource(imgID);
        }
        else
            this.FlagImage = null;
    }
    // 日付領域にバインドするためのプロパティ
    public int Day { get; set; }                // 日付
    public Color TextColor { get; set; }        // テキスト色
    public string HolidayName { get; set; }     //祝日の名称
    public bool IsHoliday { get; set; }         // 祝日フラグ
    public ImageSource FlagImage { get; set; }  // 祝日画像を設定

    // 画像用リソースIDを定数で定義
    private static readonly string imgID = "Xuni_QuickStart.Resources.Images.jp_flag.png";
}

このイベントでは、引数として描画する日付の情報などをCalendarDaySlotLoadingEventArgsクラスの引数として取得します。その引数で提供される日付で判断し、祝日とテキスト色を変更します。 イベント内の処理は以下です。 * 日付領域は引数で取得するDaySlotCalendarViewDaySlotクラスにキャスト
* 日付の種別にあわせてDaySlotContextを作成
* XAMLで定義した日付領域用の'BindingContext'に設定
これを対象月の日付領域および、カレンダー上に表示される前後の隣接月の日付領域用に処理します。

//日付スロットの処理
calendar.DaySlotLoading+= (object sender, CalendarDaySlotLoadingEventArgs e) => 
{
    var dayslot = e.DaySlot as CalendarViewDaySlot;
    if (!e.IsAdjacentDay)
    {
        // 日付領域の描画対象になった日付が、祝日Dictionaryに存在するかどうか確認
        if (HolidaysList.ContainsKey(e.Date))
        {
            // 祝日の場合(日付、テキスト色、祝日名称を設定)
            dayslot.BindingContext = new DaySlotContext(e.Date, getColor(System.DayOfWeek.Sunday), HolidaysList[e.Date].ToString(), true);
        }
        else
        {
            // 祝日以外の場合(日付とテキスト色を設定)
            dayslot.BindingContext = new DaySlotContext(e.Date, getColor(e.Date.DayOfWeek));
        }
    }
    else
    {
        // 隣接日の場合(日付は設定されるがテキスト色は設定しても表示には反映しない)
        dayslot.BindingContext = new DaySlotContext(e.Date, getColor(e.Date.DayOfWeek));
    }
};

日付の色を設定する関数は以下です。祝日の場合は日曜日を指定して色を取得します。

// 曜日で判断して色を返す関数
private Color getColor(System.DayOfWeek dayofWeek)
{
    Color color;
    switch (dayofWeek)
    {
        case System.DayOfWeek.Saturday: // 土曜日は青
            color = Color.Blue;
            break;
        case System.DayOfWeek.Sunday: // 日曜日は赤
            color = Color.Red;
            break;
        default:
            color = Color.Black;  // 既定値は黒
            break;
    }
    return color;
}

こちらの手順は別の記事でも解説しています。

プラットフォームごとの追加設定

Android、iOSで設定が異なる部分についてはそれぞれのプロジェクトで設定する必要があります。

Androidアプリのパーミッションを設定

以上で準備が整いましたので実行します。その前にCSVファイルをWebサイトから取得しますので、Androidアプリにインターネットアクセスの権限を設定します。
Androidプロジェクトのオプション画面で「必要なアクセス許可」に以下の3種についてチェックを入れます。 同じ手続きはPropatiesフォルダにあるAndroidManifest.xmlを編集することでも可能です。
* AccessFineLocation
* AccessNetworkState
* Internet

f:id:ComponentOne_JP:20170302153952p:plain

iOSアプリで動作するための改良

iOSアプリの場合は、Shift_JISのエンコードが正しく処理できません。そのためCSVファイルの読み込みに失敗して祝日が反映されない状態で表示されます。

f:id:ComponentOne_JP:20170302154011p:plain

前述では解説していませんがこれを回避するために、NuGetパッケージ「Portable.Text.Encording」を利用しています。これを利用し、データソースを作成するHolidayDataSource.csでエンコードを指定する記述を以下のようにしています。この改良はPCLプロジェクトが対象です。

// iOSでShift_JISエンコードを使用するためにPortable.Text.Encording を利用
var encoding_SJIS = Portable.Text.Encoding.GetEncoding("Shift_JIS");
// AndroidのみであればSystem.Text.Encording が利用できる
// var encoding_SJIS = System.Text.Encoding.GetEncoding("Shift_JIS");

このこともあわせ、実際はエンコードをUTF-8に指定したCSVファイルをリソース格納して取り込む方法を推奨します。

祝日が設定されたカレンダー

これでカスタマイズが完了し、実行した結果が以下です。
5月のゴールデンウィークと、6月には祝日が無いことを確認できます。

f:id:ComponentOne_JP:20170302154034p:plain

XAMLで定義したレイアウト通りに表示できました。

f:id:ComponentOne_JP:20170303084956p:plain

まとめ

このように、ファイルに保存した祝日情報を利用することでカレンダーのカスタマイズが可能です。
日付領域はXAMLで作成することができるので、他のコントロールを組み込んだ設定もできます。

Xamarin.FormsとXuniを活用することで、見た目や内容をカスタマイズしたカレンダーを、Android/iOSアプリに表示できます。
その処理のほとんどは共通化したPCLプロジェクトに記述しているので、少ない工数でクロスプラットフォームアプリ開発を実現します。 XuniはXamarin.Formsで利用可能な各種コンポーネントを提供していますので、あわせてご利用ください。


参考情報

[NuGet Gallery | Portable.Text.Encoding] (https://www.nuget.org/packages/Portable.Text.Encoding/)

[WinRT/Metro TIPS:シフトJISのEncodingオブジェクトを取得するには?- @IT] (http://www.atmarkit.co.jp/ait/articles/1509/30/news039.html)

ComponentOne