Using the latest AI model, DeepSeek-R1, we implemented a text parser to analyze ambiguous specifications effortlessly. It outperformed OpenAI’s o1-preview with ease.
相模原市で IoT 設計を受託しているファームロジックスです。
以前に何回か、LLM(生成 AI)を使って、曖昧な仕様のテキストをパーズ(parse)するコードを書いてもらったことがあります。
最近、DeepSeek-R1 という reasoning model が話題となっていますので、私も早速試してみることにしました。今までと同じお題を使ってみます。
AI へのプロンプトは前回と同じですので、そちらを御参考頂ければと思います。
結論
2024年の8月に試した GPT-4、2024年9月に試した o1-preview および o1-mini よりも、期待に近い結果が得られました。具体的に言うと、
- AI に提示されたコードが一回で動いた(pytest で全テストケースがパスした)
- 指示を正確に捉えていた(コメントは英語で、1行は 79文字以下で)
素晴らしいことです。
思考過程
DeepSeek-R1 は、回答をする前に「思考」をするモデルです。最近急に話題となったので、サーバーの負荷が重くなっているという可能性はありますが、なんと 600秒(!!)というかなりの長考をしてくれました。以下に長考過程を示します。
まず、仕様をよく読んで、必要な処理を分解する必要がありますね。主なステップは次のようになるでしょう。
1. 入力文字列から余分なテキストを読み飛ばし、タプル部分を抽出する。
2. 各タプルを時刻と水量にパースする。
3. 時刻のフォーマットを処理する(コロンの削除、クエスチョンマークの処理など)。
4. 最初と最後の灌水時刻を適切に表示形式に変換する。
5. 総水量を計算する。
まず、入力文字列の解析部分。余分なテキストはタプルの前にあるので、最初のタプルを見つける必要があります。タプルはコンマで区切られていますが、余分なコンマが最後にある可能性もあるので、それを取り除く処理も必要です。
タプルの抽出方法ですが、正規表現を使ってタプル部分を切り出すのが良さそうです。例えば、タプルは「数字または?を含む部分」と「数字または小数点を含む部分」の組み合わせで構成されます。ただし、余分なテキストを読み飛ばすために、最初のタプルが始まる位置を見つける必要があります。具体的には、文字列全体をスキャンして、タプルのパターンにマッチする部分をすべて取得します。
タプルのパターンは、例えば「\S+\s+\d+(?:\.\d+)?」のような形ですが、より正確に考えると、各タプルは「時刻部分(コロンや?を含む可能性)」と「水量(整数または小数)」の組み合わせです。正規表現で各タプルを抽出するために、コンマ区切りの各部分を分割し、それぞれを処理します。
次に、各タプルの時刻部分の処理です。時刻部分からコロンを削除し、クエスチョンマークがあれば「頃」を付けるか、時刻が不明(?のみ)の場合は「不明」と表示します。例えば、「13:22?」はコロンを削除して「1322?」になり、時刻は「13:22頃」と表示されます。「?」だけの場合は不明です。
時刻のフォーマットは、4桁の数字の場合(例えば0630→06:30)、3桁の場合はどうなるのか? 仕様では「時 分」を続けて書くので、例えば930は09:30と解釈します。つまり、4桁未満の場合、例えば3桁の場合、頭の数字が時で残りが分? それとも、930は9時30分。そう、930→0930→09:30になる。つまり、パディングして4桁にする必要があります。ただし、クエスチョンマークがついている場合は、それを取り除いて処理します。
例えば、入力が「930?」の場合、クエスチョンマークを取り除いて「930」となり、それを09:30頃として表示する必要があります。
時刻部分の処理ステップ:
1. コロンを削除。
2. クエスチョンマークがあるかチェック。あればフラグを立てて削除。
3. 残った文字列が空(つまり元が「?」の場合)→不明。
4. 残った文字列を数字のみにして、4桁になるようにパディング。例えば、930→0930→09:30。
ただし、数字の長さが3の場合、頭が1桁の時(例:930→9時30分)? 例えば、930→0930となるように左にゼロを詰める?
例:
入力が「1234」→12:34
入力が「930」→09:30
入力が「9」→これはどうなる? 仕様では時刻は「時分」を続けるので、恐らく入力のミスですが、プログラム的には可能な限り解釈する必要があるかもしれません。しかし、テストケースには存在しないので、今回のテストケースでは4桁または3桁(930→0930)のように処理する必要があるかもしれません。しかし、例えば「9」の場合は「00:09」? それとも「09:00」? ここで混乱が生じる可能性があります。仕様の例では、930→09:30となっています。つまり、930は時が9で分が30。なので、同じように、例えば「123」→01:23となるのか、12:03となるのか。ここは、入力が時分を続けたものと仮定して、4桁に満たない場合は左詰めで、前を時、後ろを分とする。例えば、3桁の場合は、最初の1桁を時、後の2桁を分とする。例えば、930→9(時)30(分)→09:30(時は2桁表示)。同様に、123→1時23分→01:23。また、2桁の場合は? 例えば、12→00:12? それとも1時2分? ここは仕様の例に基づいて考える必要があります。テストケースには存在しないので、プログラムがどう動くべきか。おそらく、時刻は4桁の数字で、入力が3桁の場合、先頭に0を追加して4桁にする。例えば、930→0930→09:30。123→0123→01:23。同様に、入力が2桁の場合は、00XXとなる? 例えば、12→0012→00:12。しかし、これは現実的ではないかもしれませんが、仕様に従う必要があります。
クエスチョンマークの処理は、まず時刻の文字列から?を取り除き、その有無で「頃」を付けるかどうかを判断します。
次に、各タプルをパースする処理:
タプルは「灌水時刻 水量」という形式。スペースで区切られています。ただし、タプルの分割時に、時刻部分と水量部分を分ける必要があります。例えば、タプルの文字列が「0820? 22」の場合、時刻は「0820?」、水量は22となります。正規表現で分割するか、split()を使うか。split()で分割する場合、スペースで分割すれば、最初の要素が時刻、残りが水量? ただし、水量は小数点を含む可能性があります(例:22.3)。split()でスペースで分割すると、時刻部分と水量部分に分かれるはずです。
しかし、タプルの要素が例えば「13:11? 80」の場合、split()でスペースで区切ると、「13:11?」と「80」になります。そして、時刻の処理でコロンを削除して「1311?」とし、その後?を処理します。
なので、タプルをsplit()で分割する際、split(' ', 1)とすれば、最初のスペースで分割できるかもしれません。しかし、タプルの中にスペースが複数ある場合、例えば「08 20? 22」のような場合はどうなるのか? しかし、仕様ではタプルは「灌水した時刻 水の量」であり、時刻部分にスペースは含まれないと考えられます。なぜなら、入力の例ではすべて時刻が連続した文字列(0630など)で、スペースを含んでいないからです。したがって、タプルをsplit()で分割する際、時刻と水量は必ず1つのスペースで区切られていると仮定できます。
ただし、入力のタプルが「? 23」の場合、split()すると時刻は「?」、水量は23となります。これは問題ありません。
次に、タプルの抽出方法。入力文字列から、余分なテキストを読み飛ばして、タプルのリストを取得します。例えば、入力1の場合、[suzuki]の後の部分からタプルが始まります。正規表現を使って、タプルにマッチする部分を全て抽出する必要があります。
タプルのパターンは、時刻部分と水量部分の組み合わせです。時刻部分は、数字、コロン、?を含む可能性があり、水量は数字と小数点を含みます。正規表現を考えると、各タプルは次のようなパターンで表せます:
(\S+)\s+(\d+\.?\d*)
ただし、これは正確ではないかもしれません。例えば、時刻部分が「?」の場合、\S+にマッチします。水量が22.3の場合、小数点を含むので、\d+\.?\d* では不十分かもしれません。正確には、水量はfloatなので、正規表現は「\d+\.\d+」または「\d+」のどちらかです。なので、正規表現の水量部分は「\d+(?:\.\d+)?」と表現できます。
タプル全体の正規表現パターンは、次のようにできます:
r'(\S+)\s+(\d+(?:\.\d+)?)'
そして、入力文字列全体からこのパターンにマッチする部分をすべて探す。ただし、入力文字列のどこからタプルが始まるかを判断する必要があります。余分なテキストはタプルの前にある可能性があるので、タプルが始まる最初の位置を見つける必要があります。
しかし、このアプローチでは、入力文字列全体をコンマで分割して、各要素をタプルとして処理する方法の方が良いかもしれません。なぜなら、入力はタプルがコンマで区切られているからです。
手順として:
1. 入力文字列をコンマで分割し、各部分をトリム(前後のスペースを削除)する。
2. 各分割された要素を処理し、タプルとして有効なものだけを抽出する。
3. ただし、余分なテキストが最初のタプルの前にある場合、最初のいくつかの要素はタプルとして有効でない可能性がある。そこで、タプルとして有効な要素が出現する最初の位置から開始する必要がある。
例えば、入力1の場合、split(',')すると、各要素は以下のようになる:
['[suzuki] 0630 36', ' 1118 20', ' 1722 18']
これらの要素をトリムすると:
'[suzuki] 0630 36', '1118 20', '1722 18'
このうち、最初の要素は余分なテキストを含んでいる。最初の要素をさらに処理して、タプルを抽出する必要があります。しかし、この方法では、最初の要素に余分なテキストとタプルが混在しているため、正規表現でタプル部分を抽出する必要があります。
代わりに、入力文字列全体から、タプルのパターンに一致する部分をすべて抽出する方法を考えます。タプルのパターンは、「時刻部分 水量」であり、それがコンマで区切られている。正規表現で、タプルのパターンを繰り返しマッチさせることができるか?
例えば、正規表現パターン:
r'(\S+)\s+(\d+(?:\.\d+)?)(?=\s*,|$)'
このパターンは、各タプルの「時刻 水量」部分をキャプチャし、その後ろにコンマや文字列の終わりが来ることを確認する(lookahead)。しかし、この方法では、入力文字列のどこからタプルが始まるかを特定できません。余分なテキストがタプルの前にあっても、その中に「時刻 水量」のようなパターンが存在する可能性があり、誤って抽出してしまうかもしれません。
別のアプローチとして、入力文字列全体をコンマで分割し、各分割後の要素をチェックして、それがタプルとして有効かどうかを判断し、有効なタプルだけを集める方法です。例えば、各分割後の要素をトリムし、その要素が「時刻部分 水量」の形式になっているかをチェックします。もし最初のいくつかの要素がタプルとして無効(例えば余分なテキスト)であれば、無視し、最初の有効なタプル以降を処理します。
しかし、この方法でも、余分なテキストがタプルの途中に含まれる可能性がある場合、例えば、余分なテキストがコンマを含む場合、分割が正しくできない可能性があります。
この問題を解決するために、入力文字列の処理方法を再考する必要があります。
仕様によると、「余分なテキストを読み飛ばした後の残りは、コンマで区切られた複数のタプルから構成されます」とあります。つまり、余分なテキストはタプルの前にあるだけで、タプルの間には存在しません。したがって、入力文字列をコンマで分割した後、各要素を処理し、最初の有効なタプルが出現するまでの要素を読み飛ばし、その後はすべてタプルとして処理するという方法が使えるかもしれません。
例えば、入力1のケースでは、コンマで分割した最初の要素は[suzuki] 0630 36ですが、この要素の中にタプルが含まれています。つまり、余分なテキスト([suzuki])の後ろにタプルのデータ(0630 36)が続いています。したがって、各分割後の要素に対して、要素内にタプルが存在するかをチェックする必要があります。
これは複雑なので、別の方法を考える必要があります。例えば、入力文字列全体から、タプル部分を正規表現で抽出する方法です。タプルは「任意の文字(時刻)+スペース+数字(水量)」という形式です。そして、これらのタプルはコンマで区切られ、余分なテキストが前に付いている場合があります。
正規表現で、タプル部分を全て抽出するパターンを作成します。例えば、次のような正規表現:
r'\b(?:\S+\s+\d+(?:\.\d+)?)(?=(?:\s*,)?\s*(?:\S+\s+\d+(?:\.\d+)?|$))'
これは複雑すぎるかもしれません。代わりに、コンマで区切られた各要素を順番にチェックし、各要素がタプルとして有効かどうかを判断します。タプルとして有効な要素は、「時刻部分 水量」という形式で、時刻部分は任意の文字(ただし、スペースを含まない)、水量は数字または小数です。
各分割後の要素を処理する際に、その要素がタプルとして有効かどうかを判断する方法:
要素をトリム(前後のスペースを削除)した後、正規表現で「^\S+\s+\d+(?:\.\d+)?$」にマッチするかどうか。もしマッチすれば、それは有効なタプルです。ただし、余分なテキストが最初のいくつかの要素に混在している可能性があります。例えば、最初の要素が「[suzuki] 0630 36」の場合、トリムすると「[suzuki] 0630 36」で、これは「\S+\s+\d+」にマッチしない(スペースが複数ある)。そのため、この要素の中からタプルを抽出する必要があります。
この問題を解決するために、各要素をさらに解析して、タプルの部分を抽出する必要があります。つまり、各要素をスペースで分割し、最後の2つが「時刻」と「水量」であることを確認する。例えば、要素が「[suzuki] 0630 36」の場合、スペースで分割すると['[suzuki]', '0630', '36']となり、最後の2つがタプルの時刻と水量です。同様に、要素が「? 23」の場合、分割すると['?', '23']となり、時刻と水量です。
このアプローチでは、各要素をスペースで分割し、最後の2つの要素が時刻と水量であると仮定します。ただし、余分なテキストが要素の中に含まれる場合は、その部分を無視して最後の2つを取り出す必要があります。
この処理を行うため、各要素をスペースで分割し、時刻と水量を抽出するコードが必要です。例えば、要素をスペースでsplitしたリストの最後の2要素を取得します。
例えば、要素が「aaa bbb ccc 0820? 22」の場合、splitすると['aaa', 'bbb', 'ccc', '0820?', '22']となり、最後の2つを時刻と水量として取り出します。
しかし、この方法では、要素の途中に余分なテキストが含まれていても、最後の2つが常に時刻と水量であると仮定します。これは仕様で余分なテキストがタプルの前にあるとされているため、タプル部分は要素の最後の2つになると考えられます。したがって、各要素をスペースで分割し、リストの最後の2つの要素を時刻と水量として抽出します。
ただし、要素が「0820? 22」の場合、splitすると['0820?', '22']となり、正しく抽出されます。要素が「? 22.3」なら、splitすると['?', '22.3']となり、OKです。
この方法で、各要素から時刻と水量を抽出できます。しかし、要素が空の場合(例えば、入力の最後に余分なコンマがある場合)は無視します。
まとめると、処理の手順は次のようになります:
1. 入力文字列をコンマで分割し、各要素をトリムする。
2. 各要素について、スペースでsplitし、最後の2つの要素を時刻と水量として取り出す。
3. それ以外の場合は無視する。
4. 抽出した時刻と水量のペアをタプルとしてリストに追加する。
5. 最後のタプルの後に空の要素(余分なコンマ)がある場合は無視する。
この方法で、余分なテキストを処理できます。例えば、最初の要素に余分なテキストが含まれていても、最後の2つの要素を抽出できるでしょう。
次に、時刻のフォーマット処理です。
時刻の文字列から、コロンを削除し、クエスチョンマークの有無をチェックします。例えば、「13:22?」→「1322?」→クエスチョンマークを削除→「1322」、フラグを立てて「頃」を追加。または、クエスチョンマークを削除した後の文字列が空の場合(「?」の場合)は時刻が不明。
処理のステップ:
時刻文字列の処理:
a. コロンをすべて削除。
b. クエスチョンマークが末尾にあるかチェック。あれば、has_question=Trueにして、クエスチョンマークを削除。
c. 残った文字列が空またはクエスチョンマークのみの場合→時刻は不明。
d. 残った文字列を数字のみにして、4桁になるようにパディング(例:930→0930)。
e. 時と分に分割。4桁の場合、最初の2桁が時、残りの2桁が分。ただし、3桁の場合、最初の1桁が時、残りの2桁が分? 例えば、930→0930→09:30。しかし、これは入力の例からそうなっているので、左詰めで4桁になるようにゼロパディングすると考える。
例えば、「1234」→「12:34」、「930」→「09:30」、「9」→「0009」→00:09? これは問題かもしれません。しかし、テストケースでは、入力が「930」の場合、09:30と解釈されることが期待されています。つまり、左側にゼロを追加して4桁にするのではなく、数字を左詰めで4桁になるようにパディングする必要があります。例えば、930→0930→09:30。123→0123→01:23。同様に、入力が「5」→0005→00:05となります。しかし、これは現実的ではないですが、仕様の例に従う必要があります。
ただし、入力例のテストケース3を見ると、「1725」が17:25として処理されています。つまり、4桁の数字はそのまま時分として解釈します。3桁の場合は、先頭に0を追加して4桁にし、同様に処理します。例えば、930→0930→09:30。
したがって、時刻文字列の処理は次のようになります:
1. コロンを削除。
2. クエスチョンマークをチェック。あれば削除し、フラグを立てる。
3. 残りの文字列が空または数字以外の場合は、時刻不明。
4. 残りの文字列が数字のみの場合、左詰めで4桁になるように0をパディング。
- 例えば、'123' → '0123' → 01:23
- '930' → '0930' → 09:30
- '9' → '0009' → 00:09(これが正しいか?)
5. 4桁の文字列を時と分に分割。前半2桁が時、後半2桁が分。
6. 時が00-23、分が00-59の範囲かどうかはチェックせず、そのまま表示する。
ただし、テストケースでは、例えば「0630」は06:30と正しく処理されます。そのため、この方法で問題ありません。
次に、時刻のフォーマットの例:
入力が「13:22?」→コロン削除→「1322?」→クエスチョンマークを削除→「1322」→4桁→13:22頃。
入力が「?」→クエスチョンマーク削除後、空→時刻不明。
入力が「? 23」の場合、時刻は「?」なので不明。
また、水量はfloatとして処理する必要があります。例えば、「22.3」は22.3リットルとして合計に加算します。
まとめると、プログラムの流れは以下のようになります:
1. 入力文字列をコンマで分割し、各要素をトリム。
2. 各要素について、スペースで分割してリストを作成。リストの最後の2要素が時刻と水量。
3. 時刻と水量のペアを抽出。
4. 時刻の処理:
a. コロンを削除。
b. クエスチョンマークをチェックし、削除。フラグ設定。
c. 残った文字列が空→時刻不明。
d. 数字のみの場合、左詰めで4桁パディング。
e. 時と分に分割し、フォーマット。
5. 最初と最後の灌水時刻を記録。
6. 総水量を計算。
7. 出力をフォーマット。
テストケースを確認しながら、これらの処理が正しく行われるかチェックする必要があります。
次に、Pythonコードの実装です。以下のようなステップでコードを書いていきます。
まず、入力文字列を処理してタプルのリストを取得する関数が必要です。
関数の例:
def parse_input(input_str):
# コンマで分割し、各要素をトリム
parts = [p.strip() for p in input_str.split(',')]
tuples = []
for part in parts:
if not part:
continue # 空の要素(余分なコンマの場合)は無視
# スペースで分割
elements = part.split()
if len(elements) < 2:
continue # 時刻と水量が存在しない場合は無視
time_part = elements[-2]
amount_str = elements[-1]
try:
amount = float(amount_str)
except ValueError:
continue # 水量が数値でない場合は無視?
tuples.append((time_part, amount))
return tuples
しかし、このコードでは、例えば「[suzuki] 0630 36」の場合、elementsは['[suzuki]', '0630', '36']となり、elements[-2]は'0630'、elements[-1]は'36'となるため、正しく抽出できます。同様に「? 23」の場合、elements[-2]は'?', elements[-1]は'23'となります。
次に、時刻の処理を行う関数:
def process_time(time_str):
# コロンを削除
time_str = time_str.replace(':', '')
has_question = False
if time_str.endswith('?'):
has_question = True
time_str = time_str[:-1]
# クエスチョンマークの処理後、空文字列の場合
if not time_str:
return '不明', False # 時刻不明
# 数字のみかチェック
if not time_str.isdigit():
return '不明', False # 数字以外が含まれる場合は不明扱い?
# 4桁になるように左詰めでゼロパディング
padded = time_str.zfill(4)
if len(padded) >4:
# 4桁を超える場合は、先頭から4桁を取る? 例えば、12345→1234→12:34?
# しかし、入力例では4桁以下を想定しているので、このケースは想定外。とりあえず切り捨てる?
padded = padded[:4]
# 時と分に分割
hour = padded[:2]
minute = padded[2:]
# フォーマット
time_formatted = f"{hour}:{minute}"
if has_question:
return f"{time_formatted}頃", True
else:
return time_formatted, True
ただし、この関数の場合、time_strが数字でない場合(例えば、文字が含まれる)は、不明を返します。例えば、time_strが「12a3」の場合、isdigit()がFalseになるため、不明となります。
しかし、入力の例では、時刻部分は数字または?のみを想定しているようです。例えば、「?」や「1234?」など。処理のステップでは、コロンと?を削除した後に残った文字列が数字かどうかをチェックし、数字でない場合は時刻不明とします。
テストケースの入力2では、「0820?」を処理すると、クエスチョンマークを削除し「0820」となり、数字なので08:20頃となります。一方、入力3の「?」は、クエスチョンマーク削除後は空文字列となり、不明となります。
次に、全体の処理:
入力からタプルを抽出し、各タプルの時刻を処理し、最初と最後の時刻を記録、総水量を計算します。
コードの例:
def generate_report(input_str):
tuples = parse_input(input_str)
if not tuples:
return "有効なデータがありません。"
total_water = sum(amount for _, amount in tuples)
first_time = None
last_time = None
for time_part, _ in tuples:
time_str, valid = process_time(time_part)
if valid:
if first_time is None:
first_time = time_str
last_time = time_str
else:
# 時刻が不明の場合
time_str = '不明'
if first_time is None:
first_time = time_str
last_time = time_str
# 最初と最後の時刻を決定
# ただし、すべてのタプルが無効な場合もある
if not tuples:
return "データなし"
if first_time is None:
first_time = '不明'
if last_time is None:
last_time = '不明'
# 総水量を小数点以下1桁まで表示(例:189.3→189.3、74→74)
total_water_str = str(total_water).rstrip('0').rstrip('.') if '.' in str(total_water) else str(total_water)
report = []
report.append(f"- 最初の灌水: {first_time}")
report.append(f"- 最後の灌水: {last_time}")
report.append(f"- トータルの灌水量: {total_water_str}リットル")
return '\n'.join(report)
しかし、上記のコードでは、各タプルの時刻を処理し、validかどうかで分岐しています。しかし、process_timeは時刻が有効な場合(例えば、数字で4桁に変換できる場合)にtime_strを返し、そうでない場合は'不明'を返す必要があります。しかし、現在のprocess_timeの戻り値は、例えば('不明', False)となります。上記のコードでは、validがFalseの場合、time_strは'不明'を代入しています。また、最初と最後の灌水時刻を設定する際、有効な時刻があればそれを設定し、無効な場合は'不明'を設定する必要があります。
しかし、このコードでは、各タプルを順に処理し、最初の有効な時刻をfirst_timeに、最後の有効な時刻をlast_timeに設定する必要があります。しかし、現在のコードでは、例えばタプルが複数あっても、各タプルの時刻が不明の場合、最初と最後の時刻が不明になります。
テストケース5の場合、すべてのタプルが「? 11」と「? 22」なので、各時刻は不明です。そのため、最初と最後の時刻は不明となります。
また、process_time関数の戻り値として、有効な時刻文字列と、有効かどうかのフラグを返しています。例えば、時刻が不明の場合は('不明', False)、有効な場合は(時刻文字列, True)となります。このフラグを元に、first_timeとlast_timeを更新する必要があります。
しかし、コードでは、validがTrueの場合のみ、first_timeとlast_timeを更新しています。しかし、例えば、タプルの時刻が不明の場合、process_timeは'不明'を返すので、その場合は、first_timeやlast_timeを'不明'に設定する必要があります。
現在のコードのforループ内の処理:
for time_part, _ in tuples:
time_str, valid = process_time(time_part)
if valid:
if first_time is None:
first_time = time_str
last_time = time_str
else:
time_str = '不明'
if first_time is None:
first_time = time_str
last_time = time_str
これは、各タプルの時刻が有効かどうかに関わらず、最初のタプルの時刻が不明の場合、first_timeを'不明'に設定し、以降のタプルの時刻が有効でも、last_timeはその時刻で上書きされます。しかし、これでは例えば、最初のタプルが不明で、次のタプルが有効な場合、最初の灌水時刻は不明、最後は有効な時刻になります。これは正しい動作です。
しかし、process_timeが有効な場合、例えば、時刻が正しく処理された場合、time_strは「06:30」や「08:20頃」となります。有効でない場合は、'不明'が返されます。
テストケース2の場合、タプルは3つあります:
タプル1: 0820? 22 → 08:20頃(valid=True)
タプル2: 1644 40 → 16:44(valid=True)
タプル3: ? 23 → 不明(valid=False)
この場合、最初のタプルの処理でfirst_timeは'08:20頃'、last_timeは同じ。タプル2の処理では、first_timeはそのままで、last_timeは'16:44'。タプル3の処理では、valid=Falseなので、time_strは'不明'。この場合、last_timeは'不明'に上書きされます。しかし、これは期待される動作ではありません。タプル3の時刻は不明ですが、灌水時刻の最後は最後のタプルの時刻が不明の場合、最後の灌水時刻は「不明」になります。しかし、テストケース2の期待される出力は最後の灌水時刻が「不明」です。しかし、実際のコードの動作では、最後のタプルの処理で、last_timeが'不明'に上書きされます。つまり、正しい動作です。
では、コードのこの部分は正しく動作するでしょうか?
例えば、タプルが複数あり、最後のタプルが不明の場合、last_timeは不明になります。これはテストケース2の通りです。
また、タプルがすべて有効な場合、最後の有効な時刻がlast_timeに設定されます。
しかし、タプルの途中に不明があっても、最後の有効な時刻がlast_timeにならないようにする必要があります。例えば、タプル1が有効、タプル2が不明、タプル3が有効の場合、last_timeはタプル3の有効な時刻になるべきです。しかし、現在のコードでは、タプル2の処理でlast_timeが'不明'になり、タプル3の処理でvalid=Trueなので、last_timeが更新されます。この場合は正しい動作です。
したがって、このコードのロジックは正しいと考えられます。
ただし、process_time関数の戻り値について、例えばtime_strが「不明」の場合、validはFalseになります。しかし、その場合、ループ内でtime_strを'不明'に設定し、first_timeとlast_timeを更新します。これは、例えば、タプルの時刻が不明の場合でも、最初の灌水時刻と最後の灌水時刻を不明とする必要があるからです。
しかし、このコードのロジックでは、各タプルを順に見ていき、validがTrueの場合のみfirst_timeとlast_timeを更新します。しかし、タプルの時刻が不明の場合、validはFalseとなり、elseブロックに入ります。その場合、time_strを'不明'にし、first_timeがまだNoneの場合、first_timeを'不明'に設定します。そして、last_timeを'不明'に設定します。しかし、これだと、例えば最初のタプルが不明で、次のタプルが有効な場合、最初の灌水時刻は不明、最後の灌水時刻は次のタプルの有効な時刻になりますが、コードでは最後のタプルの処理でlast_timeが有効な時刻に上書きされるはずです。
例えば:
タプル1: ? → 不明(valid=False)→ first_time='不明', last_time='不明'
タプル2: 1234 → 12:34(valid=True)→ first_timeは'不明'のまま(Noneではない)、last_time=12:34
しかし、コードでは、valid=Trueの場合、最初のタプルが不明の場合、first_timeはすでに'不明'なので、更新されません。しかし、last_timeは新しい時刻に更新されます。そのため、最初の灌水時刻は不明、最後は12:34となります。これは正しいです。
しかし、現在のコードでは、valid=Falseの場合、time_strを'不明'に設定し、first_timeがNoneの場合はそれを'不明'に設定し、last_timeも'不明'に設定します。その後、valid=Trueのタプルが来ると、last_timeは上書きされます。
したがって、このロジックは正しく動作します。
総水量の計算では、各タプルの水量をfloatに変換し、合計します。その際、小数点以下の表示を適切に行う必要があります。例えば、テストケース7では、22.3が含まれるので、合計は189.3リットルになります。出力では、整数の場合は小数点以下を表示せず、小数がある場合は必要なだけ表示します。
しかし、Pythonの文字列変換では、例えば、74.0は74として表示したいです。そのため、total_water_strを以下のように処理します:
total_water_str = "{0:.1f}".format(total_water).rstrip('0').rstrip('.') if total_water % 1 != 0 else str(int(total_water))
または、より簡単な方法として:
total_water_str = str(total_water).rstrip('0').rstrip('.') if '.' in str(total_water) else str(total_water)
例えば、total_waterが74.0の場合、str(total_water)は'74.0'となり、rstrip('0')→'74.'、rstrip('.')→'74'。total_waterが189.3の場合、'189.3'→そのまま。total_waterが85→'85'。
この処理をコードに組み込む必要があります。
これで、生成されるレポートが期待通りになります。
テストケース7では、入力が「yoshida 0633 64, ? 22.3, ? 50, 1700? 53」です。parse_inputで各タプルが正しく抽出され、時刻が処理されます。総水量は64 + 22.3 +50 +53 = 189.3となるため、出力は「189.3リットル」となります。
次に、テストベンチの作成です。pytestを使用して、各テストケースを関数で記述します。入力と期待される出力をテストします。
テストコードの例:
import pytest
from main import generate_report
test_cases = [
(
"[suzuki] 0630 36, 1118 20, 1722 18",
"- 最初の灌水: 06:30\n- 最後の灌水: 17:22\n- トータルの灌水量: 74リットル"
),
(
"[tanaka] 0820? 22, 1644 40, ? 23",
"- 最初の灌水: 08:20頃\n- 最後の灌水: 不明\n- トータルの灌水量: 85リットル"
),
(
"? 69, 1725 33",
"- 最初の灌水: 不明\n- 最後の灌水: 17:25\n- トータルの灌水量: 102リットル"
),
(
"[yoshida] 0633 64, ? 22, ? 50, 1700? 53",
"- 最初の灌水: 06:33\n- 最後の灌水: 17:00頃\n- トータルの灌水量: 189リットル"
),
(
"[kimura] ? 11, ? 22",
"- 最初の灌水: 不明\n- 最後の灌水: 不明\n- トータルの灌水量: 33リットル"
),
(
"[sakamoto] 13:11? 80",
"- 最初の灌水: 13:11頃\n- 最後の灌水: 13:11頃\n- トータルの灌水量: 80リットル"
),
(
"yoshida 0633 64, ? 22.3, ? 50, 1700? 53",
"- 最初の灌水: 06:33\n- 最後の灌水: 17:00頃\n- トータルの灌水量: 189.3リットル"
),
]
@pytest.mark.parametrize("input_str, expected", test_cases)
def test_generate_report(input_str, expected):
assert generate_report(input_str) == expected
これで、各テストケースをパラメータとしてテストできます。
ただし、テストケース7の入力に小数点が含まれている場合、parse_inputが正しくfloatに変換できるか確認する必要があります。現在のparse_input関数では、elements[-1]をfloatに変換しようとします。例えば、「22.3」は正しく変換されます。
コードのparse_input関数を確認:
def parse_input(input_str):
parts = [p.strip() for p in input_str.split(',')]
tuples = []
for part in parts:
if not part:
continue
elements = part.split()
if len(elements) < 2:
continue
time_part = elements[-2]
amount_str = elements[-1]
try:
amount = float(amount_str)
except ValueError:
continue # 無効な水量の場合はタプルを無視?
tuples.append((time_part, amount))
return tuples
この場合、elements[-1]が「22.3」の場合、floatに変換できます。テストケース7の総水量は64 + 22.3 +50 +53 = 189.3となり、期待通りです。
しかし、このコードでは、タプルの水量が無効な場合(例えば文字列)、そのタプルは無視されます。しかし、仕様ではそのようなケースについての記載がありません。入力は常に正しい水量を与えると仮定しているかもしれませんが、テストケースには含まれていないので、現状のコードでは無視されます。仕様に従い、入力が正しい形式であると仮定します。
最後に、コードのコメントを英語で書く必要があります。また、1行79文字以下にする必要があります。
以上を踏まえて、Pythonコードとテストコードを実装します。
回答
最終的な回答は次の通りです。
以下は、指定された仕様に基づくPythonプログラムとテストコードです。
Pythonソースコード(main.py)
import re
def parse_input(input_str):
"""Parse input string into list of time and amount tuples."""
parts = [part.strip() for part in input_str.split(",")]
tuples = []
for part in parts:
if not part:
continue
elements = part.split()
if len(elements) < 2:
continue
time_part = elements[-2]
amount_str = elements[-1]
try:
amount = float(amount_str)
except ValueError:
continue
tuples.append((time_part, amount))
return tuples
def process_time(time_str):
"""Process time string into formatted time or 'unknown'."""
time_str = time_str.replace(":", "")
has_question = False
if time_str.endswith("?"):
has_question = True
time_str = time_str[:-1]
if not time_str:
return "不明", False
if not time_str.isdigit():
return "不明", False
padded = time_str.zfill(4)
if len(padded) > 4:
padded = padded[:4]
hour = padded[:2]
minute = padded[2:]
time_formatted = f"{hour}:{minute}"
if has_question:
return f"{time_formatted}頃", True
else:
return time_formatted, True
def generate_report(input_str):
"""Generate irrigation report from input string."""
tuples = parse_input(input_str)
if not tuples:
return "有効なデータがありません。"
total_water = sum(amount for _, amount in tuples)
first_time = None
last_time = None
for time_part, _ in tuples:
time_str, valid = process_time(time_part)
if valid:
if first_time is None:
first_time = time_str
last_time = time_str
else:
current_time_str = "不明"
if first_time is None:
first_time = current_time_str
last_time = current_time_str
total_water_str = (
f"{total_water:.1f}".rstrip("0").rstrip(".")
if isinstance(total_water, float) and total_water % 1 != 0
else str(int(total_water))
)
report = [
f"- 最初の灌水: {first_time}",
f"- 最後の灌水: {last_time}",
f"- トータルの灌水量: {total_water_str}リットル",
]
return "\n".join(report)
テストコード(test_main.py)
import pytest
from main import generate_report
test_cases = [
(
"[suzuki] 0630 36, 1118 20, 1722 18",
"- 最初の灌水: 06:30\n- 最後の灌水: 17:22\n- トータルの灌水量: 74リットル",
),
(
"[tanaka] 0820? 22, 1644 40, ? 23",
"- 最初の灌水: 08:20頃\n- 最後の灌水: 不明\n- トータルの灌水量: 85リットル",
),
(
"? 69, 1725 33",
"- 最初の灌水: 不明\n- 最後の灌水: 17:25\n- トータルの灌水量: 102リットル",
),
(
"[yoshida] 0633 64, ? 22, ? 50, 1700? 53",
"- 最初の灌水: 06:33\n- 最後の灌水: 17:00頃\n- トータルの灌水量: 189リットル",
),
(
"[kimura] ? 11, ? 22",
"- 最初の灌水: 不明\n- 最後の灌水: 不明\n- トータルの灌水量: 33リットル",
),
(
"[sakamoto] 13:11? 80",
"- 最初の灌水: 13:11頃\n- 最後の灌水: 13:11頃\n- トータルの灌水量: 80リットル",
),
(
"yoshida 0633 64, ? 22.3, ? 50, 1700? 53",
"- 最初の灌水: 06:33\n- 最後の灌水: 17:00頃\n- トータルの灌水量: 189.3リットル",
),
]
@pytest.mark.parametrize("input_str, expected", test_cases)
def test_generate_report(input_str, expected):
assert generate_report(input_str) == expected
テスト結果
すべてのテストケースがパスすることを確認してください。以下のコマンドでテストを実行できます。
pytest test_main.py -v
このプログラムは、与えられた入力文字列を解析し、灌水の最初と最後の時刻、および総灌水量をレポートとして出力します。テストケースは、提供されたすべての例をカバーしています。
動かしてみました
pytest は見事にパスしました! 何も言うことはありません…
