Skip to content

Commit

Permalink
Merge pull request #38 from rabee-inc/feature/time-formatter
Browse files Browse the repository at this point in the history
feat: timefmt.TimeFormatter を実装
  • Loading branch information
simiraaaa authored Aug 16, 2024
2 parents 3d9487e + 539da6a commit aeb0427
Show file tree
Hide file tree
Showing 3 changed files with 1,005 additions and 0 deletions.
88 changes: 88 additions & 0 deletions timefmt/formatter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
package timefmt

import "time"

type TimeFormatter interface {
// dayjs like format
// REF: https://day.js.org/docs/en/display/format
// ISO8601 format example: "YYYY-MM-DDTHH:mm:ss.SSSZ"
// escape character: [].
// escape example:
// - "[Today is] YYYY/MM/DD" = "Today is 2024/01/01"
// - not escape "Today is YYYY/MM/DD" = "To1amy i0 2024/01/01"
// YYYY: 4 digit year (2024)
// YY: 2 digit year (24)
// MMMM: full month name (January)
// MMM: short month name (Jan)
// MM: 2 digit month (01-12)
// M: 1 digit month (1-12)
// DD: 2 digit day (01-31)
// D: 1 digit day (1-31)
// HH: 2 digit hour (00-23)
// H: 1 digit hour (0-23)
// hh: 2 digit hour (00-12)
// h: 1 digit hour (0-12)
// mm: 2 digit minute (00-59)
// m: 1 digit minute (0-59)
// ss: 2 digit second (00-59)
// s: 1 digit second (0-59)
// SSS: 3 digit millisecond (000-999)
// A: upper case meridiem (AM/PM)
// a: lower case meridiem (am/pm)
// Z: time zone offset (+09:00/Z). if time zone is UTC, return "Z".
// ZZ: time zone offset (+0900/Z). if time zone is UTC, return "Z".
// dddd: full day of the week (Sunday)
// ddd: short day of the week (Sun)
// dd: min day of the week (Su)
// d: 1 digit day of the week (0-6)
Format(t time.Time, layout string) string

// dddd で使用される曜日の full name を設定する。
// len(names) != 7 の場合 panic。
// ex) []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
// ex) []string{"日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"}
SetWeekdayFullNames(names []string)

// dddd で使用される曜日の short name を取得する
WeekdayFullNames() []string

// ddd で使用される曜日の short name を設定する。
// len(names) != 7 の場合 panic。
// ex) []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
// ex) []string{"日", "月", "火", "水", "木", "金", "土"}
SetWeekdayShortNames(names []string)

// ddd で使用される曜日の short name を取得する。
WeekdayShortNames() []string

// dd で使用される曜日の min name を設定する。
// len(names) != 7 の場合 panic。
// ex) []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}
// ex) []string{"日", "月", "火", "水", "木", "金", "土"}
SetWeekdayMinNames(names []string)

// dd で使用される曜日の min name を取得する。
WeekdayMinNames() []string

// MMMM で使用される month の name を設定する。
// len(names) != 12 の場合 panic。
// ex) []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
// ex) []string{"1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"}
SetMonthNames(names []string)

// MMMM で使用される month の name を取得する
MonthNames() []string

// MMM で使用される month の short name を設定する。
// len(names) != 12 の場合 panic。
// ex) []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
// ex) []string{"1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"}
SetMonthShortNames(names []string)

// MMM で使用される month の short name を取得する
MonthShortNames() []string

// AM/PM を判定する関数を設定する。
// hours には 24 時間表記の時間が入る。(0〜23)
SetMeridiem(f func(hours int) string)
}
216 changes: 216 additions & 0 deletions timefmt/formatter_impl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
package timefmt

import (
"fmt"
"regexp"
"strconv"
"strings"
"time"
)

type timeFormatter struct {
weekdayFullNames []string
weekdayShortNames []string
weekdayMinNames []string
monthNames []string
monthShortNames []string
meridiemFunc func(int) string
}

var defaultWeekdayFullNames = []string{"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"}
var defaultWeekdayShortNames = []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
var defaultWeekdayMinNames = []string{"Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"}
var defaultMonthNames = []string{"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"}
var defaultMonthShortNames = []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
var defaultMeridiemFunc = func(hours int) string {
if hours < 12 {
return "am"
}
return "pm"
}

// REF: https://github.com/iamkun/dayjs/blob/dev/src/constant.js#L30C30-L30C112
var reFormat = regexp.MustCompile(`\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS`)

var weekLen = 7
var monthLen = 12

// New returns a new instance of TimeFormatter.
func New() TimeFormatter {
return &timeFormatter{
weekdayFullNames: defaultWeekdayFullNames,
weekdayShortNames: defaultWeekdayShortNames,
weekdayMinNames: defaultWeekdayMinNames,
monthNames: defaultMonthNames,
monthShortNames: defaultMonthShortNames,
meridiemFunc: nil,
}
}

func (t *timeFormatter) Format(tm time.Time, layout string) string {
return reFormat.ReplaceAllStringFunc(layout, func(s string) string {
// [] で囲まれている場合は、フォーマットせずに[]の中身を返す
if s[0] == '[' {
return s[1 : len(s)-1]
}
return t.format(tm, s)
})
}

func (t *timeFormatter) MonthNames() []string {
return t.monthNames
}

func (t *timeFormatter) MonthShortNames() []string {
return t.monthShortNames
}

func (t *timeFormatter) SetMonthNames(names []string) {
mustLen(names, monthLen)
t.monthNames = names
}

func (t *timeFormatter) SetMonthShortNames(names []string) {
mustLen(names, monthLen)
t.monthShortNames = names
}

func (t *timeFormatter) SetWeekdayFullNames(names []string) {
mustLen(names, weekLen)
t.weekdayFullNames = names
}

func (t *timeFormatter) SetWeekdayMinNames(names []string) {
mustLen(names, weekLen)
t.weekdayMinNames = names
}

func (t *timeFormatter) SetWeekdayShortNames(names []string) {
mustLen(names, weekLen)
t.weekdayShortNames = names
}

func (t *timeFormatter) WeekdayFullNames() []string {
return t.weekdayFullNames
}

func (t *timeFormatter) WeekdayMinNames() []string {
return t.weekdayMinNames
}

func (t *timeFormatter) WeekdayShortNames() []string {
return t.weekdayShortNames
}

func (t *timeFormatter) SetMeridiem(f func(hours int) string) {
t.meridiemFunc = f
}

// mustLen ... names の長さが l でない場合 panic にする
func mustLen(names []string, l int) {
if len(names) != l {
panic(fmt.Sprintf("len error: names length must be %d but %d", l, len(names)))
}
}

// chunk に応じたフォーマットを行う
func (t *timeFormatter) format(tm time.Time, chunk string) string {
switch chunk {
// year
case "YYYY":
return strconv.Itoa(tm.Year())
case "YY":
return fmt.Sprintf("%02d", tm.Year()%100)

// month
case "M":
return strconv.Itoa(int(tm.Month()))
case "MM":
return fmt.Sprintf("%02d", int(tm.Month()))
case "MMM":
return t.monthShortNames[int(tm.Month())-1]
case "MMMM":
return t.monthNames[int(tm.Month())-1]

// day
case "D":
return strconv.Itoa(tm.Day())
case "DD":
return fmt.Sprintf("%02d", tm.Day())

// hours
case "H":
return strconv.Itoa(tm.Hour())
case "HH":
return fmt.Sprintf("%02d", tm.Hour())
case "h", "hh":
h := tm.Hour() % 12
if h == 0 {
h = 12
}
if chunk == "h" {
return strconv.Itoa(h)
}
return fmt.Sprintf("%02d", h)

// minutes
case "m":
return strconv.Itoa(tm.Minute())
case "mm":
return fmt.Sprintf("%02d", tm.Minute())

// seconds
case "s":
return strconv.Itoa(tm.Second())
case "ss":
return fmt.Sprintf("%02d", tm.Second())

// milliseconds
case "SSS":
return fmt.Sprintf("%03d", tm.Nanosecond()/1e6)

// AM/PM
case "a", "A":
meridiemFunc := t.meridiemFunc
if meridiemFunc == nil {
meridiemFunc = defaultMeridiemFunc
}
meridiem := meridiemFunc(tm.Hour())
if chunk == "a" {
return strings.ToLower(meridiem)
}
return strings.ToUpper(meridiem)

// weekday
case "d":
return strconv.Itoa(int(tm.Weekday()))
case "dd":
return t.weekdayMinNames[int(tm.Weekday())]
case "ddd":
return t.weekdayShortNames[int(tm.Weekday())]
case "dddd":
return t.weekdayFullNames[int(tm.Weekday())]

// timezone
case "Z", "ZZ":
_, offset := tm.Zone()
if offset == 0 {
return "Z"
}
sign := "+"
if offset < 0 {
sign = "-"
offset = -offset
}
hour := offset / 3600
min := (offset % 3600) / 60
if chunk == "Z" {
return fmt.Sprintf("%s%02d:%02d", sign, hour, min)
}
return fmt.Sprintf("%s%02d%02d", sign, hour, min)

// match しない場合はそのまま返す
default:
return chunk
}
}
Loading

0 comments on commit aeb0427

Please sign in to comment.