From f6c6988bb0ca718151b2148da5d85be2be5017bb Mon Sep 17 00:00:00 2001 From: simiraaaa Date: Tue, 13 Aug 2024 11:04:54 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20timefmt.TimeFormatter=20=E3=82=92?= =?UTF-8?q?=E5=AE=9F=E8=A3=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- timefmt/formatter.go | 84 ++++ timefmt/formatter_impl.go | 216 ++++++++++ timefmt/formatter_impl_test.go | 701 +++++++++++++++++++++++++++++++++ 3 files changed, 1001 insertions(+) create mode 100644 timefmt/formatter.go create mode 100644 timefmt/formatter_impl.go create mode 100644 timefmt/formatter_impl_test.go diff --git a/timefmt/formatter.go b/timefmt/formatter.go new file mode 100644 index 0000000..5c7cf5b --- /dev/null +++ b/timefmt/formatter.go @@ -0,0 +1,84 @@ +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" + // 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) +} diff --git a/timefmt/formatter_impl.go b/timefmt/formatter_impl.go new file mode 100644 index 0000000..0cbb24f --- /dev/null +++ b/timefmt/formatter_impl.go @@ -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 + } +} diff --git a/timefmt/formatter_impl_test.go b/timefmt/formatter_impl_test.go new file mode 100644 index 0000000..dba6ba5 --- /dev/null +++ b/timefmt/formatter_impl_test.go @@ -0,0 +1,701 @@ +package timefmt + +import ( + "reflect" + "testing" + "time" +) + +var monthNamesJP = []string{"睦月", "如月", "弥生", "卯月", "皐月", "水無月", "文月", "葉月", "長月", "神無月", "霜月", "師走"} +var monthShortNamesJP = []string{"1月", "2月", "3月", "4月", "5月", "6月", "7月", "8月", "9月", "10月", "11月", "12月"} +var weekdayFullNamesJP = []string{"日曜日", "月曜日", "火曜日", "水曜日", "木曜日", "金曜日", "土曜日"} +var weekdayShortNamesJP = []string{"日曜", "月曜", "火曜", "水曜", "木曜", "金曜", "土曜"} +var weekdayMinNamesJP = []string{"日", "月", "火", "水", "木", "金", "土"} +var meridiemFuncJP = func(hours int) string { + if hours < 12 { + return "午前" + } + return "午後" +} + +func TestNew(t *testing.T) { + tests := []struct { + name string + want TimeFormatter + }{ + { + name: "New", + want: &timeFormatter{ + weekdayFullNames: defaultWeekdayFullNames, + weekdayShortNames: defaultWeekdayShortNames, + weekdayMinNames: defaultWeekdayMinNames, + monthNames: defaultMonthNames, + monthShortNames: defaultMonthShortNames, + meridiemFunc: nil, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := New(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("New() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_timeFormatter_Format(t *testing.T) { + type fields struct { + weekdayFullNames []string + weekdayShortNames []string + weekdayMinNames []string + monthNames []string + monthShortNames []string + meridiemFunc func(int) string + } + type args struct { + tm time.Time + layout string + } + loc, _ := time.LoadLocation("Asia/Tokyo") + tm := time.Date(2021, time.June, 1, 12, 34, 56, 789000000, loc) + + tests := []struct { + name string + fields fields + args args + want string + }{ + { + name: "escape", + fields: fields{}, + args: args{ + tm: tm, + layout: "YYYY/[YYYY]", + }, + want: "2021/YYYY", + }, + { + name: "ISO8601", + fields: fields{}, + args: args{ + tm: tm, + layout: "YYYY-MM-DDTHH:mm:ss.SSSZ", + }, + want: "2021-06-01T12:34:56.789+09:00", + }, + { + name: "year", + fields: fields{}, + args: args{ + tm: tm, + layout: "YYYYYY/YYYY/YY", + }, + want: "202121/2021/21", + }, + { + name: "month", + fields: fields{ + monthNames: defaultMonthNames, + monthShortNames: defaultMonthShortNames, + }, + args: args{ + tm: tm, + layout: "MMMMMMM/MMMM/MMM/MM/M", + }, + want: "JuneJun/June/Jun/06/6", + }, + { + name: "custom month", + fields: fields{ + monthNames: monthNamesJP, + monthShortNames: monthShortNamesJP, + }, + args: args{ + tm: tm, + layout: "MMMMMMM/MMMM/MMM/MM/M", + }, + want: "水無月6月/水無月/6月/06/6", + }, + { + name: "day", + fields: fields{}, + args: args{ + tm: tm, + layout: "DDD/DD/D", + }, + want: "011/01/1", + }, + { + name: "12 hours", + fields: fields{}, + args: args{ + tm: tm, + layout: "HHHhhh/HH/H/hh/h", + }, + want: "12121212/12/12/12/12", + }, + { + name: "0 hours", + fields: fields{}, + args: args{ + tm: time.Date(2021, time.June, 1, 0, 34, 56, 0, loc), + layout: "HHHhhh/HH/H/hh/h", + }, + want: "0001212/00/0/12/12", + }, + { + name: "1 hours", + fields: fields{}, + args: args{ + tm: time.Date(2021, time.June, 1, 1, 34, 56, 0, loc), + layout: "HHHhhh/HH/H/hh/h", + }, + want: "011011/01/1/01/1", + }, + { + name: "minutes", + fields: fields{}, + args: args{ + tm: time.Date(2021, time.June, 1, 1, 3, 56, 0, loc), + layout: "mmm/mm/m", + }, + want: "033/03/3", + }, + { + name: "seconds", + fields: fields{}, + args: args{ + tm: time.Date(2021, time.June, 1, 1, 3, 4, 0, loc), + layout: "sss/ss/s", + }, + want: "044/04/4", + }, + { + name: "milliseconds", + fields: fields{}, + args: args{ + tm: time.Date(2021, time.June, 1, 1, 3, 4, 789000000, loc), + layout: "SSS", + }, + want: "789", + }, + { + name: "AM", + fields: fields{}, + args: args{ + tm: time.Date(2021, time.June, 1, 0, 3, 4, 789000000, loc), + layout: "aA", + }, + want: "amAM", + }, + { + name: "PM", + fields: fields{}, + args: args{ + tm: time.Date(2021, time.June, 1, 12, 3, 4, 789000000, loc), + layout: "aA", + }, + want: "pmPM", + }, + { + name: "custom AM", + fields: fields{ + meridiemFunc: meridiemFuncJP, + }, + args: args{ + tm: time.Date(2021, time.June, 1, 0, 3, 4, 789000000, loc), + layout: "aA", + }, + want: "午前午前", + }, + { + name: "custom PM", + fields: fields{ + meridiemFunc: meridiemFuncJP, + }, + args: args{ + tm: time.Date(2021, time.June, 1, 12, 3, 4, 789000000, loc), + layout: "aA", + }, + want: "午後午後", + }, + { + name: "timezone plus", + fields: fields{}, + args: args{ + tm: tm, + layout: "ZZZ/Z/ZZ", + }, + want: "+0900+09:00/+09:00/+0900", + }, + { + name: "weekdays", + fields: fields{ + weekdayFullNames: defaultWeekdayFullNames, + weekdayShortNames: defaultWeekdayShortNames, + weekdayMinNames: defaultWeekdayMinNames, + }, + args: args{ + tm: tm, + layout: "ddddddd/dddd/ddd/dd/d", + }, + want: "TuesdayTue/Tuesday/Tue/Tu/2", + }, + { + name: "custom weekdays", + fields: fields{ + weekdayFullNames: weekdayFullNamesJP, + weekdayShortNames: weekdayShortNamesJP, + weekdayMinNames: weekdayMinNamesJP, + }, + args: args{ + tm: tm, + layout: "ddddddd/dddd/ddd/dd/d", + }, + want: "火曜日火曜/火曜日/火曜/火/2", + }, + { + name: "timezone minus", + fields: fields{}, + args: args{ + tm: time.Date(2021, time.June, 1, 12, 34, 56, 0, time.FixedZone("UTC", -9*60*60)), + layout: "ZZZ/Z/ZZ", + }, + want: "-0900-09:00/-09:00/-0900", + }, + { + name: "timezone UTC", + fields: fields{}, + args: args{ + tm: time.Date(2021, time.June, 1, 12, 34, 56, 0, time.UTC), + layout: "ZZZ/Z/ZZ", + }, + want: "ZZ/Z/Z", + }, + { + name: "timezone UTC", + fields: fields{}, + args: args{ + tm: time.Date(2021, time.June, 1, 12, 34, 56, 0, time.UTC), + layout: "ZZZ/Z/ZZ", + }, + want: "ZZ/Z/Z", + }, + { + name: "unmatched", + fields: fields{}, + args: args{ + tm: tm, + layout: "Y/YYY", + }, + want: "Y/YYY", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := &timeFormatter{ + weekdayFullNames: tt.fields.weekdayFullNames, + weekdayShortNames: tt.fields.weekdayShortNames, + weekdayMinNames: tt.fields.weekdayMinNames, + monthNames: tt.fields.monthNames, + monthShortNames: tt.fields.monthShortNames, + meridiemFunc: tt.fields.meridiemFunc, + } + if got := tr.Format(tt.args.tm, tt.args.layout); got != tt.want { + t.Errorf("timeFormatter.Format() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_timeFormatter_MonthNames(t *testing.T) { + type fields struct { + monthNames []string + } + tests := []struct { + name string + fields fields + want []string + }{ + { + name: "month names", + fields: fields{ + monthNames: defaultMonthNames, + }, + want: defaultMonthNames, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := &timeFormatter{ + monthNames: tt.fields.monthNames, + } + if got := tr.MonthNames(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("timeFormatter.MonthNames() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_timeFormatter_SetMonthNames(t *testing.T) { + type args struct { + names []string + } + tests := []struct { + name string + args args + want []string + isErr bool + }{ + { + name: "SetMonthNames:valid", + args: args{ + names: monthNamesJP, + }, + want: monthNamesJP, + }, + { + name: "SetMonthNames:invalid_length", + args: args{ + names: []string{}, + }, + want: nil, + isErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if !tt.isErr { + t.Errorf("timeFormatter.SetMonthNames() panic = %v, want %v", r, tt.isErr) + } + } + }() + tr := &timeFormatter{} + tr.SetMonthNames(tt.args.names) + if got := tr.monthNames; !reflect.DeepEqual(got, tt.want) { + t.Errorf("timeFormatter.monthNames = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_timeFormatter_MonthShortNames(t *testing.T) { + type fields struct { + monthShortNames []string + } + tests := []struct { + name string + fields fields + want []string + }{ + { + name: "month short names", + fields: fields{ + monthShortNames: defaultMonthShortNames, + }, + want: defaultMonthShortNames, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := &timeFormatter{ + monthShortNames: tt.fields.monthShortNames, + } + if got := tr.MonthShortNames(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("timeFormatter.MonthShortNames() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_timeFormatter_SetMonthShortNames(t *testing.T) { + type args struct { + names []string + } + tests := []struct { + name string + args args + want []string + isErr bool + }{ + { + name: "SetMonthShortNames:valid", + args: args{ + names: monthShortNamesJP, + }, + want: monthShortNamesJP, + }, + { + name: "SetMonthShortNames:invalid_length", + args: args{ + names: []string{}, + }, + want: nil, + isErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if !tt.isErr { + t.Errorf("timeFormatter.SetMonthShortNames() panic = %v, want %v", r, tt.isErr) + } + } + }() + tr := &timeFormatter{} + tr.SetMonthShortNames(tt.args.names) + if got := tr.monthShortNames; !reflect.DeepEqual(got, tt.want) { + t.Errorf("timeFormatter.monthShortNames = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_timeFormatter_SetWeekdayFullNames(t *testing.T) { + type args struct { + names []string + } + tests := []struct { + name string + args args + want []string + isErr bool + }{ + { + name: "SetWeekdayFullNames:valid", + args: args{ + names: weekdayFullNamesJP, + }, + want: weekdayFullNamesJP, + }, + { + name: "SetWeekdayFullNames:invalid_length", + args: args{ + names: []string{}, + }, + want: nil, + isErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if !tt.isErr { + t.Errorf("timeFormatter.SetWeekdayFullNames() panic = %v, want %v", r, tt.isErr) + } + } + }() + tr := &timeFormatter{} + tr.SetWeekdayFullNames(tt.args.names) + if got := tr.weekdayFullNames; !reflect.DeepEqual(got, tt.want) { + t.Errorf("timeFormatter.weekdayFullNames = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_timeFormatter_SetWeekdayMinNames(t *testing.T) { + type args struct { + names []string + } + tests := []struct { + name string + args args + want []string + isErr bool + }{ + { + name: "SetWeekdayMinNames:valid", + args: args{ + names: weekdayMinNamesJP, + }, + want: weekdayMinNamesJP, + }, + { + name: "SetWeekdayMinNames:invalid_length", + args: args{ + names: []string{}, + }, + want: nil, + isErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if !tt.isErr { + t.Errorf("timeFormatter.SetWeekdayMinNames() panic = %v, want %v", r, tt.isErr) + } + } + }() + tr := &timeFormatter{} + tr.SetWeekdayMinNames(tt.args.names) + if got := tr.weekdayMinNames; !reflect.DeepEqual(got, tt.want) { + t.Errorf("timeFormatter.weekdayMinNames = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_timeFormatter_SetWeekdayShortNames(t *testing.T) { + type args struct { + names []string + } + tests := []struct { + name string + args args + want []string + isErr bool + }{ + { + name: "SetWeekdayShortNames:valid", + args: args{ + names: weekdayShortNamesJP, + }, + want: weekdayShortNamesJP, + }, + { + name: "SetWeekdayShortNames:invalid_length", + args: args{ + names: []string{}, + }, + want: nil, + isErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + if !tt.isErr { + t.Errorf("timeFormatter.SetWeekdayShortNames() panic = %v, want %v", r, tt.isErr) + } + } + }() + tr := &timeFormatter{} + tr.SetWeekdayShortNames(tt.args.names) + if got := tr.weekdayShortNames; !reflect.DeepEqual(got, tt.want) { + t.Errorf("timeFormatter.weekdayShortNames = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_timeFormatter_WeekdayFullNames(t *testing.T) { + type fields struct { + weekdayFullNames []string + } + tests := []struct { + name string + fields fields + want []string + }{ + { + name: "weekday full names", + fields: fields{ + weekdayFullNames: defaultWeekdayFullNames, + }, + want: defaultWeekdayFullNames, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := &timeFormatter{ + weekdayFullNames: tt.fields.weekdayFullNames, + } + if got := tr.WeekdayFullNames(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("timeFormatter.WeekdayFullNames() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_timeFormatter_WeekdayMinNames(t *testing.T) { + type fields struct { + weekdayMinNames []string + } + tests := []struct { + name string + fields fields + want []string + }{ + { + name: "weekday min names", + fields: fields{ + weekdayMinNames: defaultWeekdayMinNames, + }, + want: defaultWeekdayMinNames, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := &timeFormatter{ + weekdayMinNames: tt.fields.weekdayMinNames, + } + if got := tr.WeekdayMinNames(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("timeFormatter.WeekdayMinNames() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_timeFormatter_WeekdayShortNames(t *testing.T) { + type fields struct { + weekdayShortNames []string + } + tests := []struct { + name string + fields fields + want []string + }{ + { + name: "weekday short names", + fields: fields{ + weekdayShortNames: defaultWeekdayShortNames, + }, + want: defaultWeekdayShortNames, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := &timeFormatter{ + weekdayShortNames: tt.fields.weekdayShortNames, + } + if got := tr.WeekdayShortNames(); !reflect.DeepEqual(got, tt.want) { + t.Errorf("timeFormatter.WeekdayShortNames() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_timeFormatter_SetMeridiemFunc(t *testing.T) { + type args struct { + f func(int) string + } + tests := []struct { + name string + args args + }{ + { + name: "set meridiem func", + args: args{ + f: meridiemFuncJP, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tr := &timeFormatter{} + tr.SetMeridiem(tt.args.f) + if got := tr.meridiemFunc; reflect.ValueOf(got).Pointer() != reflect.ValueOf(tt.args.f).Pointer() { + t.Errorf("timeFormatter.meridiemFunc = %p, want %p", got, tt.args.f) + } + }) + } +} From 539da6a2f43c7e19dd62675636f0630ed30b5f22 Mon Sep 17 00:00:00 2001 From: simiraaaa Date: Tue, 13 Aug 2024 11:23:56 +0900 Subject: [PATCH 2/2] docs: add comment --- timefmt/formatter.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/timefmt/formatter.go b/timefmt/formatter.go index 5c7cf5b..abde4a8 100644 --- a/timefmt/formatter.go +++ b/timefmt/formatter.go @@ -6,6 +6,10 @@ 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)