タイムゾーンの考え方
プリザンターの DB には、日時がタイムゾーン情報なしの DateTime 型で、サーバーローカル タイムゾーン(UTC ではありません)の値として格納されています。入出力のたびに「サーバーローカル」「ユーザー」「UTC」の 3 つのタイムゾーンを使い分けて変換しています。 このページでは、その変換の仕組みと、入力経路(フォーム・API・CSV・サーバースクリプト)ごとの違い、タイムゾーンが混在する環境で起きる日付ズレと対策を整理します。
結論
- 全ユーザーがサーバーと同じタイムゾーンなら、変換は恒等変換になり問題は起きません
- 通常のサーバースクリプトだけは
ConvertTimeFromUtcによる独自の変換を使うため、サーバーのタイムゾーンが UTC 以外だと日付がズレます - API はリクエストでタイムゾーンを指定できず、API キーの所有ユーザーのタイムゾーンが基準になります
3 つのタイムゾーン
| 名称 | 実体 | 決定方法 | 用途 |
|---|---|---|---|
| サーバーローカル タイムゾーン | TimeZoneInfo.Local | OS から自動取得 | DB 格納、内部処理の基準 |
| ユーザー タイムゾーン | context.TimeZoneInfo | ユーザー設定 / パラメータ | 画面表示、フォーム入力、API 入出力 |
| UTC | DateTimeKind.Utc | 固定 | JavaScript エンジン(V8)の内部形式 |
図を読み込み中…
タイムゾーン設定の階層
システムデフォルト(Service.json)
App_Data/Parameters/Service.json の TimeZoneDefault で、システム全体のデフォルトタイムゾーンを設定します。配布時の既定値は "UTC" で、次の例は日本時間に変えた場合です。
{
"TimeZoneDefault": "Tokyo Standard Time"
}この値は起動時に Environments.TimeZoneInfoDefault に解決されます。OS に存在しない ID を書いた場合は UTC になり、Users_TimeZone 列の既定値もこの値に置き換わります(1.5.8.1 のソースでは Initializer.cs#L1333-L1340、Service.json#L4)。次の場面で使われます。
| 利用場面 | 説明 |
|---|---|
| ユーザー新規作成 | TimeZone カラムのデフォルト値 |
| ユーザー一括登録 | タイムゾーン未指定時のフォールバック |
Context 初期化 | 未認証リクエスト時のタイムゾーン |
| バックグラウンドサーバースクリプト | スケジュールのタイムゾーン未設定時のフォールバック |
WARNING
TimeZoneDefault はサーバーローカル タイムゾーン(TimeZoneInfo.Local)とは別物です。サーバーローカル タイムゾーンは OS のタイムゾーン設定によって自動決定されます。
ユーザーごとのタイムゾーン
各ユーザーの管理画面でタイムゾーンを個別に設定でき、Users テーブルの TimeZone カラムに文字列で保存されます。
public string TimeZone = "UTC"; // DB カラム
public TimeZoneInfo TimeZoneInfo
{
get
{
return TimeZoneInfo.GetSystemTimeZones()
.FirstOrDefault(o => o.Id == TimeZone);
}
}無効な値が設定されている場合のフォールバック順序は、設定値 → 東京標準時 → サーバーローカル タイムゾーンです(1.5.8.1 のソースでは UserModel.InitializeTimeZone()、UserModel.cs#L6085-L6089)。
SAML 認証では、Authentication.json の SamlParameters.Attributes.TimeZone(既定の属性名は TimeZone)に対応する属性が IdP から送られてくると、その値がユーザーの TimeZone に同期されます。属性が無ければ同期しません(Authentication.json#L56、Saml.cs#L214-L216)。
実行時のタイムゾーン(Context.TimeZoneInfo)
図を読み込み中…
ログイン時や API 認証時にユーザーのタイムゾーンで上書きされ、未認証の場合は TimeZoneDefault がそのまま使われます。
DB 格納形式
プリザンターはすべての日時をサーバーローカル タイムゾーンで DB に格納します。
| RDBMS | 現在日時の取得 | 格納されるタイムゾーン |
|---|---|---|
| SQL Server | getdate() | サーバーローカル |
| PostgreSQL | CURRENT_TIMESTAMP | サーバーローカル |
| MySQL | CURRENT_TIMESTAMP | サーバーローカル |
DANGER
サーバーの OS タイムゾーンを変更すると、既存データとの整合性が崩れます。運用中の変更は避けてください。
やむを得ずサーバーのタイムゾーンを変えるときは、CodeDefiner の ConvertTime /h 時差 で DB の日時列をまとめてずらせます(CodeDefiner のコマンド一覧を参照)。
変換ヘルパー ToLocal / ToUniversal
日時変換の中核は Times.cs に定義された 2 つの拡張メソッドです。
| メソッド | 変換方向 | 用途 |
|---|---|---|
ToLocal(context) | サーバーローカル → ユーザー タイムゾーン | DB 値を画面表示・API レスポンスに |
ToUniversal(context) | ユーザー タイムゾーン → サーバーローカル | フォーム入力・API リクエストを DB に |
// サーバーローカル タイムゾーン → ユーザー タイムゾーン
public static DateTime ToLocal(this DateTime value, Context context)
{
var timeZoneInfo = context.TimeZoneInfo;
if (timeZoneInfo == null || timeZoneInfo.Id == TimeZoneInfo.Local.Id)
return value;
return TimeZoneInfo.ConvertTime(value, timeZoneInfo);
}
// ユーザー タイムゾーン → サーバーローカル タイムゾーン
public static DateTime ToUniversal(this DateTime value, Context context)
{
var timeZoneInfo = context.TimeZoneInfo;
if (timeZoneInfo == null || timeZoneInfo.Id == TimeZoneInfo.Local.Id)
return value;
return TimeZoneInfo.ConvertTime(value, timeZoneInfo, TimeZoneInfo.Local);
}ToUniversal は UTC への変換ではない
ToUniversal という名前ですが、UTC への変換ではありません。ユーザー タイムゾーンからサーバーローカル タイムゾーンへの変換です。
ユーザー タイムゾーンとサーバーローカル タイムゾーンが同一の場合は変換がスキップされるため、単一タイムゾーン環境では実質的に何も起きません。
Time クラスの二重保持
Time クラスは、1 つの日時に対してサーバーローカルの値と表示用の値の 2 つを保持します。
public class Time : IConvertable
{
public DateTime Value = 0.ToDateTime(); // サーバーローカル タイムゾーン(DB 保存値)
public DateTime DisplayValue = 0.ToDateTime(); // ユーザー タイムゾーン(表示用)
}DB から読み取ったときは Value を ToLocal して DisplayValue にセットし、フォーム入力時は DisplayValue を ToUniversal して Value にセットします。
フォーム入力から画面表示までの流れ
サーバーのタイムゾーンが UTC、ユーザーのタイムゾーンが JST(+9)の例です。入力と表示で対称的な変換が行われるため、同じユーザーが操作する限り日時のズレは発生しません。
図を読み込み中…
入出力経路ごとの変換方式
| 入力経路 | 変換メソッド | タイムゾーン基準の決定要因 |
|---|---|---|
| フォーム入力 | ToUniversal(context) | ログインユーザーのタイムゾーン |
| API リクエスト | ToUniversal(context) | API キー所有ユーザーのタイムゾーン(リクエスト側で指定不可) |
| CSV インポート | ToUniversal(context) | インポート実行ユーザーのタイムゾーン |
| 計算式サーバースクリプト | ToUniversal(context) | 操作ユーザーのタイムゾーン |
| 通常サーバースクリプト | ConvertTimeFromUtc(v, Local) | UTC 固定(ユーザー タイムゾーンを一切使わない) |
| 出力経路 | 変換メソッド | タイムゾーン基準 |
|---|---|---|
| 画面表示 | ToLocal(context) | ユーザー タイムゾーン |
| API レスポンス | ToLocal(context) | ユーザー タイムゾーン |
| 計算式サーバースクリプト | ToLocal(context) → 文字列化 | ユーザー タイムゾーン |
| 通常サーバースクリプト | 変換なし(生の DateTime) | なし |
図を読み込み中…
サーバースクリプトでの日時
サーバースクリプトは JavaScript エンジンとして ClearScript(V8)を使用しています。.NET の DateTime を JavaScript に渡すと、V8 は DateTimeKind に関係なく UTC の Date オブジェクトとして扱います。この制約により、通常のサーバースクリプトの日時変換はフォーム入力や API とは根本的に異なる方式になっています。
通常サーバースクリプトの入出力
入力(DB → JavaScript): Values() メソッドを通じて、サーバーローカル タイムゾーンの生の DateTime がそのまま渡されます。V8 はこれを UTC の Date として扱います。
// 通常サーバスクリプト
ReadNameValue(
columnName: nameof(model.CreatedTime),
value: model.CreatedTime?.Value, // 生の DateTime(サーバーローカル タイムゾーン)
mine: mine),出力(JavaScript → DB): Date() メソッドで書き戻されます。ConvertTimeFromUtc は入力値を常に UTC として扱うため、サーバーローカルの値であっても UTC として解釈して変換します。
private static DateTime Date(ExpandoObject data, string name)
{
var value = Value(data, name);
return value is DateTime dateTime
? TimeZoneInfo.ConvertTimeFromUtc(dateTime, TimeZoneInfo.Local)
: Types.ToDateTime(0);
}サーバーが UTC 以外だと日時がズレる
サーバーのタイムゾーンが JST(+9)の環境で、日付をそのまま書き戻す場合です。
図を読み込み中…
サーバーのタイムゾーンが UTC の場合は ConvertTimeFromUtc(UTC, UTC) が恒等変換になるためズレは発生しません。サーバーのタイムゾーンが UTC 以外の場合に問題が顕在化します。
日付を別の項目にコピーする場合も同様です。model.DateA = model.StartTime; とすると、StartTime が 2024/01/02 00:00 なのに、書き戻された DateA は 2024/01/02 09:00 になります(二重シフト)。
文字列で代入すると値が消える
組み込み日付項目に文字列を代入すると、値が消失します。Date() メソッドが value is DateTime をチェックし、文字列の場合は Types.ToDateTime(0) を返すためです。
// NG: 値が消失する
model.StartTime = "2024/01/01";計算式サーバースクリプトは安全
計算式サーバースクリプトでは、通常のフォーム入力と同じ変換方式が使われます。入力時に ToClientTimeZone(context)(= ToLocal(context))でユーザー タイムゾーンの文字列として渡し、書き戻しは SetByFormData で ToUniversal(context) するため、日時のズレは発生しません。
| 項目 | 通常サーバースクリプト | 計算式サーバースクリプト |
|---|---|---|
| 入力の型 | DateTime(サーバーローカル タイムゾーン) | string(ユーザー タイムゾーン) |
| 出力の変換 | ConvertTimeFromUtc(v, Local) | ToUniversal(context) |
| 対称性 | 非対称(入力のタイムゾーンと出力の変換基準が異なる) | 対称(入出力が同じタイムゾーン基準) |
$NOW() / $TODAY() と utilities.Today()
| 関数 | 使える場所 | 動作 |
|---|---|---|
$NOW() | 計算式 | UTC + ユーザー タイムゾーンのオフセットで、ユーザー タイムゾーンの日時文字列を返す |
$TODAY() | 計算式 | 同上で時刻部分を切り捨てた日付文字列を返す |
utilities.Today() | 通常サーバースクリプト | 下記のとおり |
WARNING
$NOW() / $TODAY() は BaseUtcOffset を使用しています。夏時間(DST)を持つタイムゾーンでは不正確になる可能性があります。
public DateTime Today()
{
return DateTime.Now.ToLocal(context: Context).Date.ToUniversal(context: Context);
}DateTime.Nowでサーバーローカルの現在時刻を取得.ToLocal(context)でユーザー タイムゾーンに変換.Dateで日付部分のみ取得(00:00:00).ToUniversal(context)でサーバーローカルに戻す
この値をサーバースクリプト内で項目に代入すると、書き戻し時に Date() メソッドの ConvertTimeFromUtc が適用される点に注意が必要です。
model.XXX と model.Body.XXX の違い
サーバースクリプト内で日付を設定する方法は 2 つあり、拡張日付項目では変換処理が異なります。
| 項目 | パス | 変換フロー |
|---|---|---|
組み込み日付項目(StartTime 等) | model.StartTime(ExpandoObject) | Date(data, name) → ConvertTimeFromUtc → サーバーローカル |
| 組み込み日付項目 | model.Body.StartTime(API モデル) | TrySetMember → Date(value) → ConvertTimeFromUtc → サーバーローカル |
拡張日付項目(DateA 等) | model.DateA(ExpandoObject) | Date() → ConvertTimeFromUtc → 文字列 → SetValue(toUniversal: false) |
| 拡張日付項目 | model.Body.DateA(API モデル) | value.ToStr() → SetValue(toUniversal: false)(タイムゾーン変換なし) |
組み込み日付項目はどちらのパスでも同じ変換ですが、拡張日付項目では同じ値を代入してもパスによって異なるタイムゾーン処理が適用されます。
CompletionTime の特殊処理
CompletionTime(期限日)は、表示フォーマットが Ymd の場合に内部で +1 日のオフセットを持ちます(後述の「日付のみの項目(Ymd 形式)」を参照)。計算式サーバースクリプトでは入力時に -1 日、書き戻し時に +1 日で正しく調整されますが、通常サーバースクリプトではこの調整が行われないため、CompletionTime を直接操作する際は注意が必要です。
安全パターンと危険パターン
| 区分 | パターン | 理由 |
|---|---|---|
| 危険 | model.StartTime = new Date(2024, 0, 1) | Date() で UTC → サーバーローカル変換。サーバーのタイムゾーンが UTC 以外ならズレる |
| 危険 | model.DateA = model.StartTime | 日付コピーで二重シフトが発生 |
| 危険 | model.StartTime = "2024/01/01" | is DateTime チェック失敗で値が消失する |
| 安全 | 計算式サーバースクリプトを使う | ToLocal / ToUniversal で対称変換 |
| 安全 | サーバーのタイムゾーンを UTC で運用する | ConvertTimeFromUtc(UTC, UTC) が恒等変換になる |
API と CSV での日時
API は API キーのユーザーのタイムゾーンが基準
API の日時処理では、API キーに紐づくユーザーのタイムゾーンが基準になります。リクエスト側でタイムゾーンを指定する手段はなく、日時にタイムゾーン情報(Z や +09:00 など)を付与しても無視されます。
図を読み込み中…
作成・更新時は SetUser() で API キーからユーザーのタイムゾーンを解決し、SetByApi() で ToUniversal(context) します。レスポンスは ToLocal(context) で API キーの所有ユーザーのタイムゾーンで返ります。たとえば所有ユーザーが JST、サーバーが UTC の場合、"2024/01/15 09:00" は 2024/01/15 00:00 として保存され、取得時には "2024/01/15 09:00" で返ります。
入力と出力が対称なので、同じ API キーで操作する限り日時のズレは発生しません。異なるタイムゾーンのユーザーの API キーで取得すると別の時刻で返ります(JST で登録した 1/15 09:00 は、PST のユーザーには 1/14 16:00 で返る)。これは同じ瞬間を指すので日時としては正しい変換ですが、日付のみの項目では日付が変わってしまうことがあります。
CSV インポートは実行ユーザーのタイムゾーンが基準
CSV インポートも、インポートを実行したユーザーのタイムゾーンで日時が解釈されます。
else if (TypeName == "datetime")
{
return value?.ToDateTime(format: RecordingFormat)
.ToUniversal(context: context).ToString()
?? string.Empty;
}| CSV の値 | ユーザーのタイムゾーン | DB 値(サーバーのタイムゾーン = UTC) |
|---|---|---|
2024/01/15 09:00 | JST (+9) | 2024/01/15 00:00 |
2024/01/15 09:00 | PST (-8) | 2024/01/15 17:00 |
WARNING
CSV インポートを行う際は、CSV 内の日時がどのタイムゾーンを想定しているかを意識し、インポート実行ユーザーのタイムゾーン設定を確認してください。
バックグラウンドサーバースクリプトのスケジュール
スケジュール評価では次の優先順位でタイムゾーンが決まります。
| 優先順位 | 設定値 | 説明 |
|---|---|---|
| 1 | schedule.ScheduleTimeZoneId | スケジュール個別設定 |
| 2 | TimeZoneDefault | Service.json のシステムデフォルト |
| 3 | UTC | いずれも未設定時のフォールバック |
var timeZone = TimeZoneInfo.FindSystemTimeZoneById(
!schedule.ScheduleTimeZoneId.IsNullOrEmpty()
? schedule.ScheduleTimeZoneId
: !Parameters.Service.TimeZoneDefault.IsNullOrEmpty()
? Parameters.Service.TimeZoneDefault
: TimeZoneInfo.Utc.Id);フロントエンドへの伝達
画面表示の際は、ユーザー タイムゾーンのオフセットが hidden 要素 TimeZoneOffset(例: "+09:00"、context.TimeZoneInfoOffset())として HTML に埋め込まれ、フロントエンドの JavaScript で日付ピッカーなどの表示補正に使われます(HtmlTemplates.cs)。
タイムゾーン混在環境で起きる問題
同じタイムゾーンのユーザーだけなら問題ありませんが、異なるタイムゾーンのユーザーが混在すると、変換の非対称性が日付ズレとして現れます。
日付のみの項目(Ymd 形式)
EditorFormat が Ymd(日付のみ)の項目は、DB に保存する際に「表示日 + 1 日」の値を格納します(AddDifferenceOfDates)。たとえば期限日 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;
}
}タイムゾーン変換とこの ±1 日が組み合わさると、日付がズレます(サーバーが UTC の例)。
図を読み込み中…
WARNING
この例はサーバーのタイムゾーンが UTC の場合です。サーバーが JST であれば JST ユーザーの ToUniversal は恒等変換になりますが、サーバーローカルと異なるタイムゾーンのユーザーは常に影響を受けます。
根本原因は、「日付のみ」でタイムゾーンに依存しないはずの値が DateTime 型で管理され、特定の時刻を持ってしまう点です。AddDifferenceOfDates は単純な ±1 日であり、タイムゾーンのオフセット差を吸収しません。
期限切れ判定(Overdue)
Overdue() はサーバーローカル時刻同士の比較だけで判定し、ユーザー タイムゾーンを考慮しません。
public bool Overdue()
{
return Status.Incomplete() && Value < 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")で終日判定するため、変換で時刻部分が残ると時間指定イベントとして表示される。例: Ymd の CompletionTime の DB 値が 2026-02-25 15:00(UTC)なら、JST ユーザーは 02-25 00:00 で正しいが、PST ユーザーは 02-24 07:00 になる(HtmlCalendar.cs の ConvertIfCompletionTime) |
| ガントチャート | バーの描画用は AddDifferenceOfDates 前、表示ラベル用は後の値を使うため、日付がまたぐとバーの長さとラベルが 1 日ズレることがある(GanttElement.cs) |
期限が近いフィルター(NearCompletionTime) | DateTime.Now.ToLocal(context).Date でユーザーの「今日」を算出するが、サーバーローカルで格納された DB 値と直接比較される(View.cs) |
| 日付フィルター(「今日」「今月」等) | 基準日は DateTime.Now.ToLocal(context) でユーザー タイムゾーン、DB 値はサーバーローカル(DateColumnExtensions.cs) |
| CSV エクスポート | ToLocal 済みの DisplayValue を出力するため、実行者のタイムゾーンが反映される。サーバー JST で JST ユーザーが登録した 2026-02-24(DB 値 2026-02-25 00:00)を PST ユーザーが出力すると 2026/02/23 になる |
| リマインダー | 開始日時の初期値は DateTime.Now.ToLocal(context).Date.AddDays(1)(設定したユーザーの翌日 0:00)。送信の処理の context はログインを経ないため TimeZoneDefault のまま。送るかどうかの判定(ScheduledTime <= DateTime.Now.ToLocal(context))も次回の計算も TimeZoneDefault の時計で行うので、開始日時は「TimeZoneDefault で何時か」として扱われ、TimeZoneDefault が UTC のままだと日本時間の利用者には 9 時間遅れて届く。対象レコードの「今日 + Range 日」も TimeZoneDefault の今日をサーバーローカルの DB 値と直接比べる。Ymd の項目で DB 値が 0:00 ちょうどでない場合も判定がずれることがある(通知のカスタムフォーマットとリマインダーの内部動作) |
遅延フィルター(Delay) | 進捗率の期待値を SQL(ProgressRateDelay)で CompletionTime の DB 値から計算するため、CompletionTime のズレをそのまま引き継ぐ(View.cs) |
日付項目の既定値(DefaultInput) | Ymd の項目は DateTime.Now.ToLocal(context).Date に日数を足して ToUniversal した値になり、入力したユーザーのタイムゾーンで DB 値が変わる。Ymd 以外は DateTime.Now(サーバーローカル)に日数を足すだけ(Column.cs#L1082-L1089) |
期限が近い判定(CompletionTime.Near()) | ユーザーの「今日」と閲覧者のタイムゾーンで変換済みの DisplayValue を比べるため、閲覧者のタイムゾーン内で閉じており影響は小さい(CompletionTime.cs#L109-L118) |
| バーンダウンチャート(影響小) | 「今日の線」がユーザーとサーバーのタイムゾーン差で数時間ズレる |
DataChange 日付自動設定(影響小) | ユーザー タイムゾーンによる「今日」が DB 保存値に影響 |
サーバースクリプト Today()(影響小) | ユーザー タイムゾーンにより異なる値が返る |
| クロス集計(影響小) | タイムゾーン差 + AddDifferenceOfDates の二重補正による集計軸のズレ |
図を読み込み中…
根本原因
- 日付のみの値に時刻情報が付随する:
Ymd形式でもDateTime型で管理されるため、タイムゾーン変換を経由すると時刻成分が混入する。AddDifferenceOfDatesはこれを考慮しない単純な ±1 日 ToUniversal/ToLocalがサーバーローカル基準: UTC を経由しないため、変換結果がサーバーのタイムゾーン設定に依存する- 判定ロジックがタイムゾーンを考慮しない:
Overdue()やSiteMenuの件数はDateTime.Nowや DB サーバーの現在時刻を直接使う - 日付フィルターの不整合: 条件はユーザー タイムゾーンで生成されるが、比較対象の DB 値は別のユーザーのタイムゾーンで保存された可能性がある
対策
全ユーザーがサーバーと同じタイムゾーンを使う環境(単一タイムゾーン環境)では、ToLocal / ToUniversal が恒等変換になるため、上記の混在環境の問題は発生しません。
| 対策 | 内容 |
|---|---|
| サーバーのタイムゾーンを UTC にする | タイムゾーン変換の影響を最小限にでき、通常サーバースクリプトの ConvertTimeFromUtc も恒等変換になる |
日時の項目(Ymdhm / Ymdhms)を使う | AddDifferenceOfDates の ±1 日が適用されないため、日付ズレが軽減される |
| 日時を扱うサーバースクリプトは計算式を使う | 対称変換で日時がズレない |
| API キーのタイムゾーンを統一する | 外部連携で複数の API キーを使う場合の日時の混乱を防ぐ |
| API キーのユーザーのタイムゾーンをサーバーに合わせる | ToUniversal / ToLocal がスキップされ、送信した日時がそのまま DB に格納される |
| CSV のインポート/エクスポートは同じタイムゾーンのユーザーで行う | タイムゾーン差による日付変動を回避 |
Overdue 判定のズレを認識する | タイムゾーン差がある環境では期限切れの表示が正確でない可能性を利用者に周知する |