フリーテキスト入力から時刻をサジェストする
こんにちは、フロントエンドエンジニアの茶木です。
前回の記事で Google Calendar ライクな時間入力フォームを作りました。
前回は、簡易実装にしていた時刻のサジェストを考えます。
要件
基本形式は HH:MM(例 12:34 ) でGoogle Calendar のものは多様な入力を許容します。
許容するフォーマット
- コロン無しのどちらも許容 (例12:34, 1234)
- 12時間表記 AM/PM の対応 (例 AM12:34, PM0:00)
さらに、入力途中でも、0がパディングされているとみなしてサジェストしてくれます。 (例13 → 13:00 例234 → 23:40)
正規表現
const MATCHER = /(AM|PM)?(\d{1,2}:?\d{0,2})/;
const [_, ampm, body] = text.match(MATCHER) || [];
if(body === undefined) return undefined;
ユーザーの入力 text
が、時刻か時刻の入力途中とみなせる形式になっているかを判別します。みなせる場合は、ampm
が “AM” か “PM” もしくは空文(24時間表記)、body
が 時分部になります。時分部は入力途中とみなせる状態でもよく、:
を含んでも含まなくても構いません。
区切りのコロン有無による出し分け
const splitted = body.split(":");
return splitted.length === 1 ?
tryFormatTime(split(body, ampm), ampm):
tryFormatTime(splitBySeparator(body), ampm);
body
部を splite
メソッドの分割数で、コロンの有無を判断します。
24時間表記に直す
function tryFormatHour(hour:number, ampm: string): number | undefined {
if (ampm) {
if (hour < 0 || hour > 12) return undefined;
return (hour % 12) + (ampm === "PM" ? 12 : 0);
}
if (hour < 0 || hour > 24) return undefined;
return hour % 24;
}
function tryFormatMinute(minute: number): number | undefined {
return minute < 0 || minute > 59 ? undefined : minute;
}
function tryFormatTime(
hm: [number, number] | undefined,
ampm: string
): [number, number] | undefined {
if (hm === undefined) return undefined;
const m = tryFormatMinute(hm[1]);
if (m === undefined) return undefined;
const h = tryFormatHour(hm[0], ampm);
if (h === undefined) return undefined;
return [h, m];
}
24時間表記に直せる場合は、時と分を返し、失敗した場合は undefined を返します。12時間表記(AM/PM)は 0時台から12時台まで、24時間表記は 0時台から24時台までの表記を許容します。
補足
AM/PM の 表記
標準的なAM/PM表記の「時」は時計の文字盤に相当すると考えるため、1時〜12時の範囲をとります。そのため、0時台は存在しないのですが、サジェスト機能では単純に 0時と12時は同じものを指すとみなします。
(例1) AM0:12 = AM12:12
(例2) PM0:00 = PM12:00
24時間表記
24時間表記では、AM/PM表記とは逆に0時台が存在し、0:00〜23:59 までが 標準の範囲となり、24時台が存在しないのですが、ここでもサジェスト機能の利便性を考慮して24時台の入力があれば 0時とみなします。
参考: 24 Hour Clock Converter: How to Convert AM/PM to 24 Hour Time
split と splitBySeparator は後述します。
区切りのコロンありによる時・分の分割
: で split して、数値に変換します。
また、分に関しては、0〜5までは入力途中とみなして、00, 10, 20, 30, 40, 50分と扱い、6以上であれば、06分のように一桁のまま扱います。
function splitBySeparator(body: string): [number, number] {
const [h, m] = body.split(":");
return [Number(h), suggestMinute(m)];
}
function suggestMinute(text: string): number {
const m = Number(text);
return m <= 5 ? m * 10 : m;
}
区切りのコロンなしによる時・分の分割
非常に複雑になります。文字数による場合分けが発生します。
function split(body: string, ampm: string) {
switch (body.length) {
case 1:
return separate(body, "H");
case 2:
return tryFormatTime(separate(body, "HH"), ampm) || separate(body, "HM");
case 3:
return tryFormatTime(separate(body, "HMM"), ampm) || separate(body, "HHM");
case 4:
return separate(body, "HHMM");
default:
return undefined;
}
}
入力が1文字の場合は、
その入力を「時」とみなします。9 → 9:00
入力が2文字の場合は、
その入力を「時」とみなします。 12 → 12:00
みなせない場合、先頭1文字を「時」、後続1文字を「分」とみなします。31 → 3:10
入力が3文字の場合は、
先頭2文字を「時」、後続1文字を「分」とみなします。123 → 12:30
みなせない場合、先頭1文字を「時」、後続1文字を「分」とみなします。 312 → 3:12
入力が4文字の場合は、
先頭2文字を「時」、後続2文字を「分」とみなします。1234 → 12:34
ロジック
function separate(text: string, rule: string): [number, number] {
const hm = rule.split("").reduce(
(acc, cur, index) => {
const c = text.substring(index, index + 1);
const [h, m] = acc;
switch (cur) {
case "H":
return [`${h}${c}`, m];
case "M":
return [h, `${m}${c}`];
default:
return acc;
}
},
["", ""]
);
return [Number(hm[0]), suggestMinute(hm[1])];
}
rule には “HM” や “HHM” のように指定して、文字列のどの位置を時・分と扱うかを指定しています。
コードまとめ
- ユーザー入力を
/(AM|PM)?(\d{1,2}:?\d{0,2})/
にマッチしているかチェック - AM/PM表記の有無をチェック
- 区切り記号「:」の有無をチェック
- 区切り記号がなければ、妥当な区切り位置で「時」と「分」を区切る
- 「分」は入力途中とみなせれば ゼロパディングをする
- 「時」と「分」が妥当であれば、サジェストとみなして提示する
おわりに
ユーザーの入力補助は、想像以上にていねいな対応が必要だと感じました。
今回は未対応ですが、10:00 a.m. といった後方表記や「午前/午後」といった多言語対応、26:00 といった翌日の指定などの指定の対応も必要かもしれません。
Gaji-Laboでは、React経験が豊富なフロントエンドエンジニアを募集しています
弊社ではReactの知見で事業作りに貢献したいフロントエンドエンジニアを募集しています。大きな制作会社や事業会社とはひと味もふた味も違うGaji-Laboを味わいに来ませんか?
もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください。お仕事お問い合わせや採用への応募、共に大歓迎です!
求人応募してみる!