🐨

[Go]string↔️intのパース処理

2024/09/24に公開

モチベーション

Golangを学習中にパース処理に少し悩んだのでメモを残します。

対象読者

以下の理由で悩んだので同じようなバックグラウンドの人のためになれば幸いです。

  • Golangにおけるstring→intのパース操作を習得していなかった
    (strconvライブラリのようなライブラリの存在を知らなかった)
  • Golangで特定の文字列を削除する方法(メソッド)を把握できていなかった

課題

12時間形式(HH:MM:SS:AM or HH:MM:SS:PM)の時間文字列を24時間形式(HH:MM:SS)に変換する。
また、AM/PMの文字列は削除する。
例えば"07:05:45PM"は"19:05:45"となる。

アイデア

課題の文面を見た時に思いついた直観的な解き方は以下のようなステップです

  1. 与えられる文字列の最後の2文字(AM/PM)を判定する
  2. 最初の2文字(HH)を操作する
  3. 最後の2文字(AM/PM)を削除する
  4. 出力する

深く考えると以下2点をどう処理するかで悩みました。

  • PMの場合に最初の2文字に12を足す
  • 最後の2文字を削除する
    デフォルトでtoInt()みたいなパース用のメソッドがないし、Rustでいうところのstring.remove()の代替メソッドが頭の中にインストールされていませんでした。

解法1:strconvを使う

ちょっと調べるとstrconv packageを使うことで解決できることを認識しました。この解法が一番まともだと思います。
まずstring(s)のスライスから最後の2文字を取得します。
ampm := s[len(s)-2:]
次に最初の2文字を同様に取得します。
hourStr := s[:2]
このようにsliceとして扱うことができるのは便利ですね。
そしてstrconvのAtoi()メソッドを使ってstringからintに変換します。
Cでもこの同じ名前のメソッドがあったのを思い出しました。

	hour, _ := strconv.Atoi(hourStr)

	// PMの場合、12を加える。ただし、12 PMはそのまま
	if ampm == "PM" && hour != 12 {
		hour += 12
	}

	// AMの場合、12 AMは0にする
	if ampm == "AM" && hour == 12 {
		hour = 0
	}

intの数値を加工したので、stringに戻します。この時にItoa()メソッドが使えます。

	// 新しい時間部分を2桁の文字列に変換
	newHourStr := ""
	if hour >= 10 {
		newHourStr = strconv.Itoa(hour)
	} else {
		newHourStr = "0" + strconv.Itoa(hour)
	}

全体のプログラムは以下のようになります。

package main

import (
	"errors"
	"fmt"
	"strconv"
	"strings"
)

func convertTo24HourFormatStrconv(s string) (string, error) {
	// AMまたはPMを取得(最後の2文字)
	ampm := s[len(s)-2:]

	// 最初の2文字を取得
	hourStr := s[:2]

	hour, _ := strconv.Atoi(hourStr)

	// PMの場合、12を加える。ただし、12 PMはそのまま
	if ampm == "PM" && hour != 12 {
		hour += 12
	}

	// AMの場合、12 AMは0にする
	if ampm == "AM" && hour == 12 {
		hour = 0
	}

	// 新しい時間部分を2桁の文字列に変換
	newHourStr := ""
	if hour >= 10 {
		newHourStr = strconv.Itoa(hour)
	} else {
		newHourStr = "0" + strconv.Itoa(hour)
	}

	// 残りの部分(:mm:ss)を取得
	remaining := s[2 : len(s)-2]

	// 新しい時間文字列を組み立てる
	time24 := newHourStr + remaining

	return time24, nil
}

func main() {
	// テストケース
	testTimes := []string{
		"07:05:45PM",
		"12:00:00AM",
		"12:00:00PM",
		"01:15:30AM",
		"11:59:59PM",
		"07:05:45PM",
		"07:05:45AM",
		"12:30:00PM",
		"12:30:00AM",
		"00:00:00AM", // 不正な時間(例として)
		"09:15:20PM",
		"10:45:55AM",
	}

	for _, time12 := range testTimes {
		time24, err := convertTo24HourFormatStrconv(time12)
		if err != nil {
			fmt.Printf("エラー: %v (入力: %s)\n", err, time12)
			continue
		}
		fmt.Printf("12時間形式: %s -> 24時間形式: %s\n", time12, time24)
	}
}

このパターンではAM/PMの文字列を削除するというより必要な要素だけ取り出しています。
シンプルに削除するのであればstrings.Replace()で文字列から特定の文字を削除することができます。この後の解法でもstrings.Replace()を使わず必要な要素に対して処理を行う方針で考えます。

解法2:文字列操作とASCIIコードを利用する

strconvを使わずパース処理を標準ライブラリだけでやる方法を考えました。
まずstting→intの処理ですが最初の2文字を取得し各文字をASCIIコード(byte)を利用して数値に変換します。

	hour := 0
	for i := 0; i < 2; i++ {
		char := hourChars[i]
		if char < '0' || char > '9' {
			return "", errors.New("時間部分に数字以外の文字が含まれています")
		}
		digit := int(char - '0')
		hour = hour*10 + digit
	}

	// PMの場合、12を加える。ただし、12 PMはそのまま
	if ampm == "PM" && hour != 12 {
		hour += 12
	}

	// AMの場合、12 AMは0にする
	if ampm == "AM" && hour == 12 {
		hour = 0
	}

文字列の再構築は新しい時間部分と残りの部分を結合して、新しい時間文字列を生成します。

	// 新しい時間部分を2桁の文字列に変換
	newHourStr := ""
	if hour >= 10 {
		// 10以上の場合
		tens := byte('0' + (hour / 10))
		ones := byte('0' + (hour % 10))
		newHourStr = string([]byte{tens, ones})
	} else {
		// 0〜9の場合
		newHourStr = "0" + string(byte('0'+hour))
	}

プログラム全体はこのようになります。そこまで冗長にならずに収まりました。

package main

import (
	"errors"
	"fmt"
)

func convertTo24HourFormatNoStrconv(s string) (string, error) {
	// AMまたはPMを取得(最後の2文字)
	ampm := s[len(s)-2:]

	// 最初の2文字を取得(時間部分)
	hourChars := s[:2]

	// 文字から数値に変換
	hour := 0
	for i := 0; i < 2; i++ {
		char := hourChars[i]
		if char < '0' || char > '9' {
			return "", errors.New("時間部分に数字以外の文字が含まれています")
		}
		digit := int(char - '0')
		hour = hour*10 + digit
	}

	// PMの場合、12を加える。ただし、12 PMはそのまま
	if ampm == "PM" && hour != 12 {
		hour += 12
	}

	// AMの場合、12 AMは0にする
	if ampm == "AM" && hour == 12 {
		hour = 0
	}

	// 新しい時間部分を2桁の文字列に変換
	newHourStr := ""
	if hour >= 10 {
		// 10以上の場合
		tens := byte('0' + (hour / 10))
		ones := byte('0' + (hour % 10))
		newHourStr = string([]byte{tens, ones})
	} else {
		// 0〜9の場合
		newHourStr = "0" + string(byte('0'+hour))
	}

	// 残りの部分(:mm:ss)を取得
	remaining := s[2 : len(s)-2]

	// 新しい時間文字列を組み立てる
	time24 := newHourStr + remaining

	return time24, nil
}
// main()は省略
}

まとめ

strconvを使ったパース処理は基本だと思うので早めに触れることができてよかったです。
文字列を切り出す処理の別解として正規表現を使った方法がチラつきましたがsliceが便利なので助かりました。また、解法2はプリミティブな実装パターンなのでGolangをきちんと理解するためにも考えてよかったと思います。

Discussion