はじめに
プリザンターの DB にはタイムゾーン情報なしの DateTime が格納されており、入出力時に ToLocal / ToUniversal で変換しています。同じタイムゾーンのユーザーだけなら問題ありませんが、異なるタイムゾーンのユーザーが混在すると、この変換の非対称性が日付ズレとして現れます。
本記事では、具体的にどこで問題が起きるのかを整理します。
| 回 | テーマ |
|---|---|
| 第 1 回 | アーキテクチャ編 — 全体設計と変換の仕組み |
| 第 2 回 | サーバスクリプト編 — SS 固有の変換方式と注意点 |
| 第 3 回 | API 編 — API でのタイムゾーンの取り扱い |
| 第 4 回(本記事) | タイムゾーン 混在環境編 — ユーザーのタイムゾーンが混在する場合の問題と対策 |
バージョン 1.5.1.0 を対象にしています
日付のみフィールド(Ymd 形式)の問題
AddDifferenceOfDates の仕組み
プリザンターでは、EditorFormat が Ymd(日付のみ)のフィールドは、DB に保存する際に「表示日 + 1 日」の値を格納します。例えば期限日 2026-02-24 は DB 上では 2026-02-25 00:00:00(サーバーローカル時刻)として保存されます。
| EditorFormat | ピッカー形式 | DB 保存時 | 表示時 |
|---|---|---|---|
Ymd |
日付のみ | +1 日 | -1 日 |
Ymdhm |
日時(分) | そのまま | そのまま |
Ymdhms |
日時(秒) | そのまま | そのまま |
public static int DifferenceOfDates(string format, bool minus = false)
{
switch (format)
{
case "Ymd": return minus ? -1 : 1;
default: return 0;
}
}
タイムゾーン 混在時の日付ズレ
タイムゾーン変換と AddDifferenceOfDates が組み合わさると、日付のズレが発生します。
ユーザーA(JST +9)が CompletionTime を "2026-02-24"(Ymd形式)で設定
→ ToUniversal: JST 2026-02-24 00:00 → サーバーローカル 2026-02-23 15:00
→ AddDifferenceOfDates(+1日): 2026-02-24 15:00
→ DB保存値: 2026-02-24 15:00
ユーザーB(PST -8)が読み出し
→ ToLocal: 2026-02-24 15:00 → PST 2026-02-23 23:00
→ AddDifferenceOfDates(-1日): 2026-02-22 23:00
→ 表示: 2026-02-22 ← 本来の 2026-02-24 から 2日ズレ
この例はサーバー タイムゾーン が UTC の場合です。サーバー タイムゾーン が JST であれば JST ユーザーの ToUniversal は恒等変換になりますが、サーバーローカルと異なる タイムゾーン のユーザーは常に影響を受けます。
根本原因は、「日付のみ」で タイムゾーン に依存しないはずの概念が、DateTime 型で管理されることで特定の時刻を持ってしまう点にあります。AddDifferenceOfDates は単純な +/-1 日であり、タイムゾーン オフセットの差を吸収しません。
期限切れ判定(Overdue)の問題
ユーザー タイムゾーン を考慮しない判定
Overdue() はサーバーローカル時刻同士の比較だけで判定しており、ユーザー タイムゾーン を一切考慮しません。
public bool Overdue()
{
return Status.Incomplete() && Value < DateTime.Now;
}
Value は DB のサーバーローカル日時、DateTime.Now もサーバーローカル日時です。
影響の具体例
サーバーが JST、ユーザーが PST (-8) で期限日 2/24 を入力した場合を考えます。
| 処理 | 値 |
|---|---|
入力: 2/24
|
PST 2/24 00:00 |
ToUniversal (PST → JST) |
JST 2/25 01:00 |
AddDifferenceOfDates (+1) |
JST 2/26 01:00(DB 値) |
Overdue 判定 |
2/26 01:00 < DateTime.Now(JST) で比較 |
JST のユーザーから見れば 2/24 の期限日なのに、Overdue になるのは JST の 2/26 01:00 以降です。約 2 日の遅延が生じます。
SiteMenu のオーバーデュー件数も、DB サーバーの getdate() / CURRENT_TIMESTAMP との比較であり、同様に タイムゾーン を考慮しません。
カレンダー表示の終日判定
終日イベントの判定方法
カレンダーの終日表示は、日時の時刻部分が 00:00:00 かどうかで判定されます。FullCalendar ライブラリの nextDayThreshold: "00:00:00" がデフォルトで使われています。
しかし、タイムゾーン 変換によって時刻部分が非ゼロになると、終日イベントではなく時間指定イベントとして表示されてしまいます。
private static DateTime ConvertIfCompletionTime(
Context context, Column column, DateTime dateTime)
{
switch (column?.ColumnName)
{
case "CompletionTime":
return dateTime
.ToLocal(context: context)
.AddDifferenceOfDates(column.EditorFormat, minus: true);
default:
return dateTime.ToLocal(context: context);
}
}
例えば、Ymd 形式の CompletionTime で DB 値が 2026-02-25 15:00(UTC)の場合、JST ユーザーが閲覧すると ToLocal で 02-26 00:00 → -1日 で 02-25 00:00 となり終日判定は正しくなりますが、PST ユーザーでは 02-25 07:00 → 02-24 07:00 となり、時刻部分が残ります。
ガントチャートの日付境界
ガントチャートでは、バーの描画用の値と表示ラベル用の値で異なる変換タイミングが使われています。
// バーの描画用: AddDifferenceOfDates 前
CompletionTime = completionTime.ToLocal(context: context,
format: Displays.YmdFormat(context: context));
// 表示ラベル用: AddDifferenceOfDates 後
DisplayCompletionTime = completionTime
.AddDifferenceOfDates(completionTimeColumn.EditorFormat, minus: true)
.ToLocal(context: context,
format: Displays.YmdFormat(context: context));
タイムゾーン の変動で日付がまたぐと、バーの長さと表示ラベルが 1 日ズレるケースがあります。
フィルター・ビューの日付範囲
NearCompletionTime フィルター
「期限日が近い」レコードを抽出するフィルターでは、ユーザー タイムゾーン の「今日」と DB 値の比較で不整合が発生する可能性があります。
DateTime.Now.ToLocal(context: context).Date
.AddDays(ss.NearCompletionTimeBeforeDays.ToInt() * (-1)),
DateTime.Now.ToLocal(context: context).Date
.AddDays(ss.NearCompletionTimeAfterDays.ToInt() + 1)
DateTime.Now.ToLocal(context).Date でユーザー タイムゾーン の「今日」を正しく算出しますが、この日付範囲がサーバーローカル時刻として格納された DB 値と直接比較されます。登録者と閲覧者の タイムゾーン が異なれば、比較が意図通りにならないことがあります。
DateFilterOptions(「今日」「今月」等)
var now = DateTime.Now.ToLocal(context: context);
フィルターの基準日はユーザー タイムゾーン で算出されますが、DB 値はサーバーローカル時刻です。タイムゾーン 差がある場合、フィルター結果に日付ずれが含まれる可能性があります。
CSV エクスポートでの日付ズレ
CSV エクスポートは実行者の タイムゾーン が反映されます。
public string ToExport(Context context, Column column,
ExportColumn exportColumn = null)
{
return DisplayValue.Display(
context: context,
format: exportColumn?.Format ?? column?.EditorFormat ?? "Ymd");
}
DisplayValue は読み出し時に ToLocal(context) 済みです。異なる タイムゾーン のユーザーが同じデータをエクスポートすると、日付のみカラムでも タイムゾーン の差分が反映された結果になりえます。
サーバータイムゾーン: JST (+9)
ユーザーA(JST)が "2026-02-24" を登録
→ DB値: 2026-02-25 00:00 (JST)
ユーザーB(PST -8)がCSVエクスポート
→ ToLocal(PST): 2026-02-24 07:00
→ AddDifferenceOfDates(-1): 2026-02-23 07:00
→ Ymdフォーマット出力: "2026/02/23" ← 1日ズレ
リマインダーの日付判定
リマインダーはバックグラウンドジョブとして実行されるため、context がシステム タイムゾーン を持つことが多いです。
特定ユーザー タイムゾーン で登録されたデータとの比較で日付がずれる可能性があります。また、日付のみカラム(Ymd)の場合、AddDifferenceOfDates による +1 日を考慮した演算子が使われていますが、タイムゾーン 差で DB 値が 0:00 ジャストでない場合に判定がずれることがあります。
その他の影響箇所
| 機能 | 影響度 | 問題 |
|---|---|---|
| BurnDown チャート | 小 | 「今日の線」がユーザー タイムゾーン とサーバーローカル タイムゾーン の差で数時間ズレる |
DataChange 日付自動設定 |
小 | ユーザー タイムゾーン による「今日」が DB 保存値に影響 |
ServerScript Today()
|
小 | ユーザー タイムゾーン により異なる Today() 値が返る |
| クロス集計 | 小 | タイムゾーン 差 + AddDifferenceOfDates の二重補正による集計軸のズレ |
影響のサマリ
根本原因の分析
タイムゾーン 混在環境の問題には、4 つの根本原因があります。
1. 日付のみの値に時刻情報が付随する
Ymd 形式でも DateTime 型で管理されるため、タイムゾーン 変換を経由すると時刻成分が混入します。AddDifferenceOfDates はこの時刻成分を考慮しない単純な +/-1 日の加算です。
2. ToUniversal / ToLocal がサーバーローカル タイムゾーン 基準
UTC を経由しないため、変換結果がサーバーの タイムゾーン 設定に依存します。
3. 判定ロジックが タイムゾーン を考慮しない
Overdue() や SiteMenu の件数カウントは DateTime.Now やDB サーバーの現在時刻を直接使用しており、ユーザーの タイムゾーン を加味しません。
4. 日付フィルターの不整合
フィルター条件はユーザー タイムゾーン で生成されますが、比較対象の DB 値は別のユーザーの タイムゾーン で保存された可能性があります。
対策のまとめ
単一 タイムゾーン 環境の場合(問題なし)
全ユーザーがサーバーと同じ タイムゾーン を使う環境では、ToLocal / ToUniversal が恒等変換になるため、上記の問題はすべて発生しません。
タイムゾーン 混在環境での対策
| 対策 | 内容 |
|---|---|
| サーバー タイムゾーン を UTC にする | タイムゾーン 変換の影響を最小限にできる |
日時フィールド(Ymdhm / Ymdhms)を使う |
AddDifferenceOfDates の +/-1 日が適用されないため、日付ズレが軽減される |
| API キーの タイムゾーン を統一する | 外部連携での日時混乱を防止 |
| CSV のインポート/エクスポートは同じ タイムゾーン のユーザーで行う | タイムゾーン 差による日付変動を回避 |
Overdue 判定のズレを認識する |
タイムゾーン 差がある環境では、期限切れの表示が正確でない可能性があることを利用者に周知 |
連載のまとめ
全 4 回にわたって、プリザンターのタイムゾーン処理を見てきました。
| 回 | 要点 |
|---|---|
| 第 1 回 | DB にはサーバーローカル タイムゾーン で格納。ToLocal / ToUniversal が変換の中核 |
| 第 2 回 | 通常サーバスクリプトだけが ConvertTimeFromUtc を使う独自方式。サーバー タイムゾーン が UTC 以外だと日付ズレの原因に |
| 第 3 回 | API は API キーのユーザー タイムゾーン が基準。リクエストで タイムゾーン 指定はできない |
| 第 4 回 | タイムゾーン 混在環境では日付のみフィールドや判定ロジックで日付ズレが発生しうる |
タイムゾーン情報を持たない DateTime でデータを保持しているからこそ、変換の仕組みを理解しておくことが大切です。ぜひ設計・運用の参考にしてみてください。