Strings of Life

PHP/Phalcon/MySQL/JavaScript/RegExp/Ruby/Perl/ActionScript

2014年01月

※下書き段階で投稿してしまったので、再投稿

PHPのdate('W')が返す週番号は、第53週では'01'を返します。strftime('%W')も同様です。

  <?php
  echo date('W', strtotime('2013-12-31')), PHP_EOL; // '01'

この仕様は、ISO規格に基づいています。

http://ja.wikipedia.org/wiki/ISO_8601#.E5.B9.B4.E3.81.A8.E9.80.B1.E3.81.A8.E6.9B.9C.E6.97.A5

以下、上記Wikipedia記事より引用

年末において以下の曜日に該当する場合、その日は当年最終週の曜日としてでは無く、翌年第1週の曜日として扱うものとされている。
12月29日が月曜日の場合。
12月30日が月曜日または火曜日の場合。
12月31日が月曜日・火曜日・水曜日のいずれかの場合。

この挙動は、PHPのバグではありません。しかし、PHPアプリケーションにおいてバグを作りこみがちな挙動の1つだと思います(実際、私も2014年最初に修正したバグは、この挙動に起因するバグでした)。

以下は、上記条件の場合には、'01'ではなく'53'を返す関数です。

<php
/**
* date('W')は、以下の場合に週番号として'01'を返す。
* (1) 12月29日が月曜日の場合。
* (2) 12月30日が月曜日または火曜日の場合。
* (3) 12月31日が月曜日・火曜日・水曜日のいずれかの場合。
*
* この関数では、12/29〜31で、週番号が'01'となる場合には、'53'を返す。
*
* @param $time UNIXタイムのタイムスタンプ
* @return string
*/
function getWeekNumber($time)
{
    $week_number     = date('W', $time); // 週番号(01から52)
    $month_and_day   = date('m-d', $time); // 月-日
    $day_of_the_week = date('w', $time); // 0 (日曜)から 6 (土曜)

    // 処理方法(1): 仕様通りに月日と曜日で判定
    if ($month_and_day === '12-29' && in_array($day_of_the_week, array(1))) {
        $week_number = '53';
    }
    if ($month_and_day === '12-30' && in_array($day_of_the_week, array(1, 2))) {
        $week_number = '53';
    }
    if ($month_and_day === '12-31' && in_array($day_of_the_week, array(1, 2, 3))) {
        $week_number = '53';
    }
    
    // 処理方法(2): 月日+date('W')の値で判定(こちらでも実用上は問題ないかと…)
    // if (in_array(date('m-d', $time), array('12-29', '12-30', '12-31')) && date('W', $time) === '01') {
    //    $week_number = '53';
    // }

    return $week_number;
}

「53」を返す処理については、仕様通りに「月日」+「曜日」で判定を行っています。ただ、この関数が必要な場合というのは、「01」が返されて困る場合に「53」を返すことだと思います。したがって、処理方法(2)でも、実用上は問題無いでしょう。

以下は、テスト付きのバージョンです。phpコマンドで実行して、結果が「ok」ならテストに合格したことになります。また、TAPに基づいたテストなので、proveコマンドでもテストできます(proveで実行するため、1行目にシバンを付けています)。

#!/usr/bin/env php
<?php

test(9);
is(getWeekNumber(strtotime('2014-12-29')), '53', 1, '2014年12月29日(月) => 53');
is(getWeekNumber(strtotime('2014-12-30')), '53', 2, '2014年12月30日(火) => 53');
is(getWeekNumber(strtotime('2014-12-31')), '53', 3, '2014年12月31日(水) => 53');
is(getWeekNumber(strtotime('2013-12-30')), '53', 4, '2013年12月30日(月) => 53');
is(getWeekNumber(strtotime('2013-12-31')), '53', 5, '2013年12月31日(火) => 53');
is(getWeekNumber(strtotime('2012-12-31')), '53', 6, '2012年12月31日(月) => 53');
is(getWeekNumber(strtotime('2011-12-29')), '52', 7, '2011年12月29日(木) => 52');
is(getWeekNumber(strtotime('2011-12-30')), '52', 8, '2011年12月30日(金) => 52');
is(getWeekNumber(strtotime('2011-12-31')), '52', 9, '2011年12月31日(土) => 52');

/**
 * @param int $number_of_tests
 */
function test($number_of_tests = 0)
{
    if ($number_of_tests > 0) {
        echo '1..', $number_of_tests, PHP_EOL;
    }
}

/**
 * @param        $got
 * @param        $expected
 * @param int    $test_number
 * @param string $description テストの説明
 * @param string $directive   「TODO」又は「SKIP」+ TODO/SKIPである理由
 */
function is($got, $expected, $test_number, $description = '', $directive = '')
{
    if ($expected === $got) {
        echo 'ok ', $test_number;
    } else {
        echo 'not ok ', $test_number;
    }
    if ($description !== '') {
        echo ' - ', $description;
    }
    if ($directive !== '') {
        echo ' # ' . $directive;
    }
    echo PHP_EOL;
    if ($expected !== $got) {
        echo '# got:      ', $got, PHP_EOL;
        echo '# expected: ', $expected, PHP_EOL;
    }
}

/**
* date('W')は、以下の場合に週番号として'01'を返す。
* (1) 12月29日が月曜日の場合。
* (2) 12月30日が月曜日または火曜日の場合。
* (3) 12月31日が月曜日・火曜日・水曜日のいずれかの場合。
*
* この関数では、12/29〜31で、週番号が'01'となる場合には、'53'を返す。
*
* @param $time UNIXタイムのタイムスタンプ
* @return string
*/
function getWeekNumber($time)
{
    $week_number     = date('W', $time); // 週番号(01から52)
    $month_and_day   = date('m-d', $time); // 月-日
    $day_of_the_week = date('w', $time); // 0 (日曜)から 6 (土曜)

    // 処理方法(1): 仕様通りに月日と曜日で判定
    if ($month_and_day === '12-29' && in_array($day_of_the_week, array(1))) {
        $week_number = '53';
    }
    if ($month_and_day === '12-30' && in_array($day_of_the_week, array(1, 2))) {
        $week_number = '53';
    }
    if ($month_and_day === '12-31' && in_array($day_of_the_week, array(1, 2, 3))) {
        $week_number = '53';
    }
    
    //処理方法(2): 月日+date('W')の値で判定(こちらでも実用上は問題ないかと…)
    // if (in_array(date('m-d', $time), array('12-29', '12-30', '12-31')) && date('W', $time) === '01') {
    //    $week_number = '53';
    // }

    return $week_number;
}

構造体

  • structは、一連の他のデータ型から作成したデータ型である
  • structは固定長である
  • structフィールドは、.<field_name>という構文を使って名前でアクセスする
  • structフィールドは、コードの順序と同じ順序でメモリに格納される
  • structは入れ子にできる
  • typedefはデータ型のエイリアスを作成する
  • structでtypedefを使う場合は、structの名前を省略できる

構造体とポインタ

  • 関数を呼び出すと、値はパラメータ変数にコピーされる
  • 他の型と同様に、structへのポインタを作成できる
  • pointer->fieldは(*pointer).fieldと同じである

共用体とビットフィールド

  • unionは、同じメモリ位置に異なるデータ型を格納できる
  • 指示付き初期化子は名前でフィールド値を設定する
  • 指示付き初期化子はC99で使える。Objective-Cでは使えるが、C++ではサポートされていない。
  • 中括弧({})で値を指定してunionを宣言すると、その値を最初のフィールドの型で格納する
  • unionのあるフィールドに書き込んだ値を別のフィールドとして読みだす場合、コンパイラは警告を出さない
  • enumはシンボルを格納する
  • ビットフィールドは任意のビット数でフィールドを格納できる
  • ビットフィールドはunsigned intとして宣言すべきである
#include <stdio.h>

typedef enum {
    COUNT, POUNDS, PINTS
} unit_of_measure;

typedef union {
    short count;
    float weight;
    float volume;
} quantity;

typedef struct {
    const char *name;
    const char *country;
    quantity amount;
    unit_of_measure units;
} fruit_order;

void display(fruit_order order)
{
    if (order.units == PINTS)
        printf("%.2fパイントの%sです\n", order.amount.volume, order.name);
    else if (order.units == POUNDS)
        printf("%2.2fポンドの%sです\n", order.amount.weight, order.name);
    else
        printf("%i個の%sです\n", order.amount.count, order.name);
}

int main()
{
    fruit_order apples = {"りんご", "イギリス", .amount.count=144, COUNT};
    fruit_order strawberries = {"いちご", "スペイン", .amount.weight=17.6, POUNDS};
    fruit_order oj = {"オレンジジュース", "アメリカ", .amount.volume=10.5, PINTS};
    display(apples);
    display(strawberries);
    display(oj);

    return 0;
}

Cのデータ型

char:文字コードの数値
int:整数(少なくとも15ビット)
short:整数(intの半分)
long:整数(少なくとも32ビット)
float:浮動小数点数
double:倍精度浮動小数点数

関数宣言

  • コンパイラは未知の関数への呼び出しがあると、その関数がintを返すとみなす
  • そのため、関数を定義する前に呼び出すと、問題となる場合がある
  • 関数宣言は、関数を定義する前に関数の概要をコンパイラに知らせる
  • 関数宣言をソースコードの先頭に記述すると、コンパイラが戻り値の型で混乱することはない
  • 関数宣言は、多くの場合、ヘッダファイルに入れる
  • #include を使って、コンパイラにヘッダファイルの内容を読み込むよう指示できる
  • #include <ファイル名>とすると、コンパイラは標準ヘッダファイルのディレクトリからファイルを探す
  • #include "ファイル名"とすると、コンパイラは現在のディレクトリからファイルを探す

コードの共有

  • コードを共有するには、そのコードを別のCファイルに入れる
  • 関数宣言は別の.hヘッダファイルに入れる
  • 共有コードを使いたいCファイルで、そのヘッダファイルをインクルードする
  • コンパイラコマンドでは、必要となる全てのCファイルを列挙する

例:hide_massage.cで、共有コードencrypt.cを使う場合
(1) encrypt.cの冒頭で #include "encrypt.h"
(2) hide_message.cの冒頭で、 #include "encrypt.h"
(3) コンパイラコマンドで、必要となる全てのCファイルを列挙:gcc hide_message.c encrypt.c -o hide_message

コンパイル時間の短縮(オブジェクトコードのキャッシュとmakeによる自動化)

  • 大量のファイルをコンパイルする場合、長い時間がかかることもある
  • オブジェクトコードを*.oファイルに格納すると、コンパイル時間を短縮できる
  • gccは、ソースファイルだけでなくオブジェクトファイルからもプログラムをコンパイルできる
  • makeツールはビルドの自動化に利用できる
  • makeはファイル間の依存関係を知っているので、変更したファイルだけをコンパイルできる
  • makeには、makefileを使ってビルドについて知らせる必要がある
  • makefileでは、行のインデントには空白ではなくタブを使う

このページのトップヘ