diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b4fa67a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,42 @@ +name: CI + +on: + push: + branches: + - "**" + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + go: ["1.24", "1.25"] + name: Test on Go ${{ matrix.go }} + steps: + # https://github.com/actions/checkout + - uses: actions/checkout@v4 + with: + submodules: recursive + + # https://github.com/actions/setup-go + - uses: actions/setup-go@v5 + with: + go-version: ${{ matrix.go }} + + - name: Check golang version/env + run: | + set -x + go version + go env + + - run: make build + - run: make test + - run: make lint + + - run: make test-with-coverage + - run: make test-coverage-profile + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} + slug: tecowl/calendatext diff --git a/.gitignore b/.gitignore index 4170155..b7757c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1 @@ -/go.sum +coverages/ diff --git a/.golangci.bck.yml b/.golangci.bck.yml new file mode 100644 index 0000000..4920df0 --- /dev/null +++ b/.golangci.bck.yml @@ -0,0 +1,28 @@ +linters: + enable-all: true + disable: + - gosmopolitan + - nlreturn + - nolintlint + - tenv # The linter 'tenv' is deprecated (since v1.64.0) due to: Duplicate feature in another linter. Replaced by usetesting. + - testpackage + - varnamelen + - wrapcheck + - wsl + +issues: +# exclude-files: +# - example_test.go + exclude-rules: + - path: _test\.go + linters: + - depguard + - dupword + - errcheck + - forcetypeassert + - funlen +linters-settings: +# cyclop: +# skip-tests: true + # lll: + # line-length: 250 diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..a07c99b --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,44 @@ +version: "2" +linters: + default: all + disable: + - gosmopolitan + - noinlineerr + - nlreturn + - nolintlint + - testpackage + - varnamelen + - wrapcheck + - wsl + - wsl_v5 + exclusions: + generated: lax + presets: + - comments + - common-false-positives + - legacy + - std-error-handling + rules: + - linters: + - depguard + - dupword + - errcheck + - forcetypeassert + - funlen + path: _test\.go + paths: + - third_party$ + - builtin$ + - examples$ +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + exclusions: + generated: lax + paths: + - third_party$ + - builtin$ + - examples$ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..d2f842e --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +.PHONY: default +default: build lint test + +GOBIN=$(shell go env GOBIN) + +include ./Makefiles/build.mk +include ./Makefiles/lint.mk +include ./Makefiles/godoc.mk +include ./Makefiles/test.mk +include ./Makefiles/cov-unit.mk +include ./Makefiles/cov-integration.mk +include ./Makefiles/metadata.mk + +.PHONY: test +test: test-unit + +# COVERAGE_GO_PACKAGES_CSV is used in Makefiles/cov-unit.mk +COVERAGE_GO_PACKAGES_CSV=$(shell find . -type d | grep -v '.git' grep -v Makefiles | grep -v coverages | tr '\n' ',' | sed 's/,$$//') +.PHONY: coverage-go-packages +coverage-go-packages: + @echo $(COVERAGE_GO_PACKAGES_CSV) diff --git a/Makefiles/build.mk b/Makefiles/build.mk new file mode 100644 index 0000000..4b8e163 --- /dev/null +++ b/Makefiles/build.mk @@ -0,0 +1,3 @@ +.PHONY: build +build: + go build ./... diff --git a/Makefiles/cov-integration.mk b/Makefiles/cov-integration.mk new file mode 100644 index 0000000..1a473a1 --- /dev/null +++ b/Makefiles/cov-integration.mk @@ -0,0 +1,28 @@ +COVERAGE_INTEGRATED_DIR=$(COVERAGES_DIR)/integrated +$(COVERAGE_INTEGRATED_DIR): + mkdir -p $(COVERAGE_INTEGRATED_DIR) + +.PHONY: test-with-coverage +test-with-coverage: test-unit-with-coverage + +COVERAGE_DIRS_CSV=$(UNIT_COVERAGE_DIR) + +COVERAGE_PROFILE?=$(COVERAGES_DIR)/coverage.txt +$(COVERAGE_PROFILE): $(COVERAGE_INTEGRATED_DIR) + $(MAKE) test-coverage-profile + +.PHONY: test-coverage-profile +test-coverage-profile: $(COVERAGE_INTEGRATED_DIR) + go tool covdata merge \ + -i $(COVERAGE_DIRS_CSV) \ + -o $(COVERAGE_INTEGRATED_DIR) + go tool covdata percent -i=$(COVERAGE_INTEGRATED_DIR) -o $(COVERAGE_PROFILE) + +COVERAGE_HTML?=$(COVERAGES_DIR)/coverage.html +$(COVERAGE_HTML): $(COVERAGE_PROFILE) + go tool covdata html -i=$(COVERAGE_PROFILE) -o $(COVERAGE_HTML) + +.PHONY: test-coverage +test-coverage: test-coverage-profile + go tool cover -html=$(COVERAGE_PROFILE) -o $(COVERAGE_HTML) + @command -v open && open $(COVERAGE_HTML) || echo "open $(COVERAGE_HTML)" diff --git a/Makefiles/cov-unit.mk b/Makefiles/cov-unit.mk new file mode 100644 index 0000000..9822446 --- /dev/null +++ b/Makefiles/cov-unit.mk @@ -0,0 +1,19 @@ +# Directory path from COVERAGES_DIR is used in test in sub directory. So COVERAGE_DIR must be an absolute path. +COVERAGES_DIR=$(CURDIR)/coverages +$(COVERAGES_DIR): + mkdir -p $(COVERAGES_DIR) + +UNIT_COVERAGE_DIR=$(COVERAGES_DIR)/unit +$(UNIT_COVERAGE_DIR): + mkdir -p $(UNIT_COVERAGE_DIR) + +.PHONY: clean-unit-coverage +clean-unit-coverage: + rm -rf $(UNIT_COVERAGE_DIR) + +# COVERAGE_GO_PACKAGES_CSV is defined in Makefile + +# See https://app.codecov.io/github/akm/go-requestid/new +.PHONY: test-unit-with-coverage +test-unit-with-coverage: clean-unit-coverage $(UNIT_COVERAGE_DIR) + go test -cover -coverpkg=$(COVERAGE_GO_PACKAGES_CSV) ./... -args -test.gocoverdir="$(UNIT_COVERAGE_DIR)" diff --git a/Makefiles/godoc.mk b/Makefiles/godoc.mk new file mode 100644 index 0000000..2fe104a --- /dev/null +++ b/Makefiles/godoc.mk @@ -0,0 +1,12 @@ +GODOC_CLI_VERSION=latest +GODOC_CLI_MODULE=golang.org/x/tools/cmd/godoc +GODOC_CLI=$(GOBIN)/godoc +$(GODOC_CLI): + $(MAKE) godoc-cli-install +godoc-cli-install: + go install $(GODOC_CLI_MODULE)@$(GODOC_CLI_VERSION) + +.PHONY: godoc +godoc: $(GODOC_CLI) + @echo "Open http://localhost:6060/pkg/github.com/tecowl/querybm" + godoc -http=:6060 diff --git a/Makefiles/lint.mk b/Makefiles/lint.mk new file mode 100644 index 0000000..5a73331 --- /dev/null +++ b/Makefiles/lint.mk @@ -0,0 +1,15 @@ +GOLANGCI_LINT_CLI_VERSION?=latest +GOLANGCI_LINT_CLI_MODULE=github.com/golangci/golangci-lint/v2/cmd/golangci-lint +GOLANGCI_LINT_CLI=$(GOBIN)/golangci-lint +$(GOLANGCI_LINT_CLI): + $(MAKE) golangci-lint-cli-install +golangci-lint-cli-install: + go install $(GOLANGCI_LINT_CLI_MODULE)@$(GOLANGCI_LINT_CLI_VERSION) + +.PHONY: lint +lint: $(GOLANGCI_LINT_CLI) + golangci-lint run + +.PHONY: linters-enabled +linters-enabled: $(GOLANGCI_LINT_CLI) + @golangci-lint linters | awk '/^Enabled by your configuration linters:$$/{flag=1;next}/^Disabled by your configuration linters:$$/{flag=0}flag{print}' diff --git a/Makefiles/metadata.mk b/Makefiles/metadata.mk new file mode 100644 index 0000000..a78b11a --- /dev/null +++ b/Makefiles/metadata.mk @@ -0,0 +1,7 @@ +METADATA_YAML=.project.yaml +$(METADATA_YAML): metadata-gen + +METADATA_LINTERS=$(strip $(shell $(MAKE) linters-enabled --no-print-directory 2>/dev/null | grep . | wc -l)) +.PHONY: metadata-gen +metadata-gen: + @echo "linters: $(METADATA_LINTERS)" > $(METADATA_YAML) diff --git a/Makefiles/test.mk b/Makefiles/test.mk new file mode 100644 index 0000000..f1d531e --- /dev/null +++ b/Makefiles/test.mk @@ -0,0 +1,5 @@ +GO_TEST_OPTIONS?= + +.PHONY: test-unit +test-unit: + go test $(GO_TEST_OPTIONS) ./... diff --git a/calendar.go b/calendar.go index 89beab8..b7655d2 100644 --- a/calendar.go +++ b/calendar.go @@ -7,7 +7,7 @@ type Calendar struct { } func NewCalendar(start, end Date, baseEnabled bool) *Calendar { - return &Calendar{Period: NewPeriod(start, end), BaseEnabled: baseEnabled} + return &Calendar{Period: NewPeriod(start, end), BaseEnabled: baseEnabled, Patterns: Patterns{}} } func (c *Calendar) Dates() Dates { diff --git a/calendar_test.go b/calendar_test.go index ff36df4..0960b0d 100644 --- a/calendar_test.go +++ b/calendar_test.go @@ -4,9 +4,11 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestCalendarDays(t *testing.T) { + t.Parallel() // ----- 2020-08 ------ // S M T W T F S @@ -18,6 +20,7 @@ func TestCalendarDays(t *testing.T) { // 30 31 t.Run("single date", func(t *testing.T) { + t.Parallel() c := &Calendar{ Period: NewPeriod(*NewDate(2020, 8, 1), *NewDate(2020, 8, 31)), BaseEnabled: false, @@ -55,6 +58,8 @@ func TestCalendarDays(t *testing.T) { } func TestCalendarParse(t *testing.T) { + t.Parallel() + texts := map[string]string{ "full-date": ` + 平日 : 通常営業日 @@ -84,15 +89,12 @@ func TestCalendarParse(t *testing.T) { for name, text := range texts { t.Run(name, func(t *testing.T) { + t.Parallel() c := NewCalendar(*NewDate(2020, 8, 1), *NewDate(2020, 8, 31), false) err := c.ParseText(text) - if !assert.NoError(t, err) { - return - } + require.NoError(t, err) + require.Len(t, c.Patterns, 4) - if !assert.Equal(t, 4, len(c.Patterns)) { - return - } if assert.IsType(t, (*Date)(nil), c.Patterns[0].DateMatcher) { i := c.Patterns[0] m := i.DateMatcher.(*Date) diff --git a/contextual_date_parser.go b/contextual_date_parser.go index 5b5b08a..dd4005f 100644 --- a/contextual_date_parser.go +++ b/contextual_date_parser.go @@ -1,12 +1,12 @@ package calendatext import ( + "errors" + "fmt" "regexp" "strconv" "strings" "time" - - "github.com/pkg/errors" ) type ContextualDateParser struct { @@ -18,23 +18,24 @@ func NewContextualDateParser(delimiterPattern string, d *Date) *ContextualDatePa if d == nil { d = Today() } + return &ContextualDateParser{ delimiter: regexp.MustCompile("[" + delimiterPattern + "]"), current: d, } } -func (cp *ContextualDateParser) Parse(s string) (*Date, error) { - parts := cp.delimiter.Split(strings.TrimSpace(s), 3) +func (cp *ContextualDateParser) Parse(s string) (*Date, error) { // nolint:cyclop,funlen + parts := cp.delimiter.Split(strings.TrimSpace(s), 3) // nolint:mnd var y, d int var m time.Month var err error switch len(parts) { - case 1: + case 1: // nolint:mnd curr := cp.current d, err = strconv.Atoi(parts[0]) if err != nil { - return nil, err + return nil, err // nolint:wrapcheck } if d < curr.Day() { m = curr.Month() + 1 @@ -42,16 +43,16 @@ func (cp *ContextualDateParser) Parse(s string) (*Date, error) { m = curr.Month() } y = curr.Year() - case 2: + case 2: // nolint:mnd curr := cp.current v, err := strconv.Atoi(parts[0]) if err != nil { - return nil, err + return nil, err // nolint:wrapcheck } m = time.Month(v) d, err = strconv.Atoi(parts[1]) if err != nil { - return nil, err + return nil, err // nolint:wrapcheck } tmpD := NewDate(curr.Year(), m, d) if curr.After(tmpD) { @@ -59,7 +60,7 @@ func (cp *ContextualDateParser) Parse(s string) (*Date, error) { } else { y = curr.Year() } - case 3: + case 3: // nolint:mnd y, err = strconv.Atoi(parts[0]) if err != nil { return nil, err @@ -74,10 +75,13 @@ func (cp *ContextualDateParser) Parse(s string) (*Date, error) { return nil, err } default: - return nil, errors.Errorf("Something wrong to parse %v (len: %d) as Date", parts, len(parts)) + return nil, fmt.Errorf("%w: %v (len: %d)", ErrDateParse, parts, len(parts)) } r := NewDate(y, m, d) cp.current = r + return r, nil } + +var ErrDateParse = errors.New("something wrong to parse date") diff --git a/contextual_date_parser_test.go b/contextual_date_parser_test.go index 22249d2..3cc90dd 100644 --- a/contextual_date_parser_test.go +++ b/contextual_date_parser_test.go @@ -5,14 +5,17 @@ import ( "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestContextualDateParser(t *testing.T) { + t.Parallel() + cp := NewContextualDateParser("/-", NewDate(2020, 8, 14)) { d, err := cp.Parse("2020/08/15") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 2020, d.Year()) assert.Equal(t, time.Month(8), d.Month()) assert.Equal(t, 15, d.Day()) @@ -20,7 +23,7 @@ func TestContextualDateParser(t *testing.T) { { d, err := cp.Parse("08/16") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 2020, d.Year()) // 2020 が補完される assert.Equal(t, time.Month(8), d.Month()) assert.Equal(t, 16, d.Day()) @@ -28,7 +31,7 @@ func TestContextualDateParser(t *testing.T) { { d, err := cp.Parse("2020/07/16") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 2020, d.Year()) // 2020 が補完される assert.Equal(t, time.Month(7), d.Month()) assert.Equal(t, 16, d.Day()) @@ -36,7 +39,7 @@ func TestContextualDateParser(t *testing.T) { { d, err := cp.Parse("17") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 2020, d.Year()) // 2020 が補完される assert.Equal(t, time.Month(7), d.Month()) // 7 が補完される assert.Equal(t, 17, d.Day()) @@ -45,7 +48,7 @@ func TestContextualDateParser(t *testing.T) { // 同じ日ならば月が進んだりしない { d, err := cp.Parse("17") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 2020, d.Year()) // 2020 が補完される assert.Equal(t, time.Month(7), d.Month()) // 7 が補完される assert.Equal(t, 17, d.Day()) @@ -53,7 +56,7 @@ func TestContextualDateParser(t *testing.T) { { d, err := cp.Parse("16") // 17よりも前の日なので翌月と判断される - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 2020, d.Year()) // 2020 が補完される assert.Equal(t, time.Month(8), d.Month()) // 8 が補完される assert.Equal(t, 16, d.Day()) @@ -61,7 +64,7 @@ func TestContextualDateParser(t *testing.T) { { d, err := cp.Parse("08/15") // 2020/08/15 ではなく 2021/08/15 と解釈される - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 2021, d.Year()) // 2021 が補完される assert.Equal(t, time.Month(8), d.Month()) assert.Equal(t, 15, d.Day()) @@ -69,7 +72,7 @@ func TestContextualDateParser(t *testing.T) { { d, err := cp.Parse("31") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 2021, d.Year()) // 2021 が補完される assert.Equal(t, time.Month(8), d.Month()) // 8 が補完される assert.Equal(t, 31, d.Day()) @@ -77,7 +80,7 @@ func TestContextualDateParser(t *testing.T) { { d, err := cp.Parse("1") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 2021, d.Year()) // 2021 が補完される assert.Equal(t, time.Month(9), d.Month()) // 9 が補完される assert.Equal(t, 1, d.Day()) @@ -85,7 +88,7 @@ func TestContextualDateParser(t *testing.T) { { d, err := cp.Parse("12/24") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 2021, d.Year()) // 2021 が補完される assert.Equal(t, time.Month(12), d.Month()) assert.Equal(t, 24, d.Day()) @@ -93,7 +96,7 @@ func TestContextualDateParser(t *testing.T) { { d, err := cp.Parse("3") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, 2022, d.Year()) // 2022 が補完される assert.Equal(t, time.Month(1), d.Month()) // 1 が補完される assert.Equal(t, 3, d.Day()) diff --git a/date.go b/date.go index 4cdbca0..c7153e6 100644 --- a/date.go +++ b/date.go @@ -1,18 +1,19 @@ package calendatext import ( + "errors" + "fmt" "strconv" "strings" "time" - - "github.com/pkg/errors" ) // RFC3339 full-date // See documents about RFC3339 -// https://www.ietf.org/rfc/rfc3339.txt -// https://medium.com/easyread/understanding-about-rfc-3339-for-datetime-formatting-in-software-engineering-940aa5d5f68a -// https://wiki.suikawiki.org/n/RFC%203339の日付形式 +// +// https://www.ietf.org/rfc/rfc3339.txt +// https://medium.com/easyread/understanding-about-rfc-3339-for-datetime-formatting-in-software-engineering-940aa5d5f68a +// https://wiki.suikawiki.org/n/RFC%203339の日付形式 const DateFormat = "2006-01-02" type Date struct { @@ -26,16 +27,21 @@ func Today() *Date { return NewDate(t.Year(), t.Month(), t.Day()) } +var ( + ErrInvalidDateFormat = errors.New("invalid date format") + ErrInvalidNumberForDate = errors.New("invalid number for date") +) + func ParseDateWith(str string, delimiter string) (*Date, error) { - parts := strings.SplitN(str, delimiter, 3) - if len(parts) < 3 { - return nil, errors.Errorf("Invalid Date format: %q", str) + parts := strings.SplitN(str, delimiter, 3) // nolint:mnd + if len(parts) < 3 { // nolint:mnd + return nil, fmt.Errorf("%w: %q", ErrInvalidDateFormat, str) } - nums := make([]int, 3) + nums := make([]int, 3) // nolint:mnd for idx, s := range parts { v, err := strconv.Atoi(s) if err != nil { - return nil, errors.Errorf("Invalid number for date: %q", str) + return nil, fmt.Errorf("%w: %q", ErrInvalidNumberForDate, str) } nums[idx] = v } @@ -83,40 +89,20 @@ func (d Date) Weekday() Weekday { } func (d Date) MonthlyWeekNum() int { - r := d.Day() / 7 - q := d.Day() % 7 + r := d.Day() / weekdays + q := d.Day() % weekdays if q == 0 { return r - } else { - return r + 1 } + return r + 1 } -// Implement DateMatcher interface +var _ DateMatcher = (*Date)(nil) // assert Date implements DateMatcher + func (d Date) Match(other *Date) bool { return d.Equal(other) } -func (d Date) beforeAfter(other *Date, compare func(a, b int) bool, resultForSame bool) bool { - if other == nil { - return false - } - if compare(d.y, other.y) { - return true - } else if d.y == other.y { - if compare(int(d.m), int(other.m)) { - return true - } else if d.m == other.m { - if compare(d.d, other.d) { - return true - } else if d.d == other.d { - return resultForSame - } - } - } - return false -} - func (d Date) After(other *Date) bool { return d.beforeAfter(other, func(a, b int) bool { return a > b @@ -162,7 +148,7 @@ func (d Date) NextDayOf(v int) *Date { } func (d Date) NextWeekOf(v int) *Date { - return d.NextDayOf(v * 7) + return d.NextDayOf(v * weekdays) } func (d Date) PrevDayOf(v int) *Date { @@ -204,3 +190,26 @@ func (d Date) NextYearOf(v int) *Date { func (d Date) PrevYearOf(v int) *Date { return d.NextYearOf(v * -1) } + +func (d Date) beforeAfter(other *Date, compare func(a, b int) bool, resultForSame bool) bool { + if other == nil { + return false + } + if compare(d.y, other.y) { + return true + } + if d.y != other.y { + return false + } + if compare(int(d.m), int(other.m)) { + return true + } + if d.m == other.m { + if compare(d.d, other.d) { + return true + } else if d.d == other.d { + return resultForSame + } + } + return false +} diff --git a/date_matcher.go b/date_matcher.go index 5b41be5..ebbaf12 100644 --- a/date_matcher.go +++ b/date_matcher.go @@ -1,5 +1,5 @@ package calendatext type DateMatcher interface { - Match(*Date) bool + Match(d *Date) bool } diff --git a/date_test.go b/date_test.go index fc187c9..72fdd93 100644 --- a/date_test.go +++ b/date_test.go @@ -7,6 +7,7 @@ import ( ) func TestNewDate(t *testing.T) { + t.Parallel() assert.Equal(t, "2019-11-30", NewDate(2020, 0, 0).String()) assert.Equal(t, "2020-08-01", NewDate(2020, 8, 1).String()) assert.Equal(t, "2019-03-01", NewDate(2019, 2, 29).String()) @@ -15,6 +16,7 @@ func TestNewDate(t *testing.T) { } func TestNextDay(t *testing.T) { + t.Parallel() d := NewDate(2020, 8, 12) assert.Equal(t, "2020-07-29", d.PrevWeekOf(2).String()) assert.Equal(t, "2020-08-05", d.PrevWeek().String()) @@ -28,6 +30,7 @@ func TestNextDay(t *testing.T) { } func TestNextMonth(t *testing.T) { + t.Parallel() d := NewDate(2020, 8, 12) assert.Equal(t, "2019-11-12", d.PrevMonthOf(9).String()) assert.Equal(t, "2019-12-12", d.PrevMonthOf(8).String()) @@ -39,6 +42,7 @@ func TestNextMonth(t *testing.T) { } func TestNextYear(t *testing.T) { + t.Parallel() d := NewDate(2020, 8, 12) assert.Equal(t, "2018-08-12", d.PrevYearOf(2).String()) assert.Equal(t, "2019-08-12", d.PrevYear().String()) @@ -47,9 +51,11 @@ func TestNextYear(t *testing.T) { } func TestCompare(t *testing.T) { + t.Parallel() t.Run("compare with myself", func(t *testing.T) { + t.Parallel() d := NewDate(2020, 8, 12) - assert.True(t, d.Equal(d)) + assert.True(t, d.Equal(d.Clone())) assert.True(t, d.BeforeEqual(d)) assert.True(t, d.AfterEqual(d)) assert.False(t, d.Before(d)) @@ -58,6 +64,7 @@ func TestCompare(t *testing.T) { subTestForNext := func(d0, d1 *Date) func(t *testing.T) { return func(t *testing.T) { + t.Helper() assert.False(t, d0.Equal(d1)) assert.True(t, d0.BeforeEqual(d1)) assert.True(t, d0.Before(d1)) @@ -68,6 +75,7 @@ func TestCompare(t *testing.T) { subTestForPrev := func(d0, d1 *Date) func(t *testing.T) { return func(t *testing.T) { + t.Helper() assert.False(t, d0.Equal(d1)) assert.False(t, d0.BeforeEqual(d1)) assert.False(t, d0.Before(d1)) @@ -77,15 +85,16 @@ func TestCompare(t *testing.T) { } d := NewDate(2020, 8, 12) - t.Run("compare with next day", subTestForNext(d, d.NextDay())) - t.Run("compare with next month", subTestForNext(d, d.NextMonth())) - t.Run("compare with next year", subTestForNext(d, d.NextYear())) - t.Run("compare with prev day", subTestForPrev(d, d.PrevDay())) - t.Run("compare with prev month", subTestForPrev(d, d.PrevMonth())) - t.Run("compare with prev year", subTestForPrev(d, d.PrevYear())) + t.Run("compare with next day", subTestForNext(d, d.NextDay())) // nolint:paralleltest + t.Run("compare with next month", subTestForNext(d, d.NextMonth())) // nolint:paralleltest + t.Run("compare with next year", subTestForNext(d, d.NextYear())) // nolint:paralleltest + t.Run("compare with prev day", subTestForPrev(d, d.PrevDay())) // nolint:paralleltest + t.Run("compare with prev month", subTestForPrev(d, d.PrevMonth())) // nolint:paralleltest + t.Run("compare with prev year", subTestForPrev(d, d.PrevYear())) // nolint:paralleltest } func TestDateMonthlyWeekNum(t *testing.T) { + t.Parallel() // ----- 2020-08 ------ // S M T W T F S // 1 diff --git a/go.mod b/go.mod index 7661464..2f67d82 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,11 @@ module github.com/tecowl/calendatext -go 1.13 +go 1.24.5 + +require github.com/stretchr/testify v1.6.1 require ( - github.com/pkg/errors v0.9.1 - github.com/stretchr/testify v1.6.1 + github.com/davecgh/go-spew v1.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect ) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..afe7890 --- /dev/null +++ b/go.sum @@ -0,0 +1,11 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main_test.go b/main_test.go index eefd238..206cd63 100644 --- a/main_test.go +++ b/main_test.go @@ -1,12 +1,14 @@ package calendatext_test import ( + "testing" + "github.com/stretchr/testify/assert" "github.com/tecowl/calendatext" - "testing" ) func TestCalendar(t *testing.T) { + t.Parallel() // ----- 2020-12 ------ ----- 2021-01 ------ // S M T W T F S S M T W T F S @@ -76,5 +78,4 @@ func TestCalendar(t *testing.T) { }, cal.Dates().Strings(), ) - } diff --git a/monthly_day_test.go b/monthly_day_test.go index 49d71a6..ed66f7f 100644 --- a/monthly_day_test.go +++ b/monthly_day_test.go @@ -8,6 +8,7 @@ import ( ) func TestCalendarWithMonthlyDay(t *testing.T) { + t.Parallel() // ----- 2020-12 ------ ----- 2021-01 ------ // S M T W T F S S M T W T F S @@ -45,5 +46,4 @@ func TestCalendarWithMonthlyDay(t *testing.T) { }, cal.Dates().Strings(), ) - } diff --git a/monthly_weekday_test.go b/monthly_weekday_test.go index cd34fa1..2b1b162 100644 --- a/monthly_weekday_test.go +++ b/monthly_weekday_test.go @@ -8,6 +8,7 @@ import ( ) func TestCalendarWithMonthlyWeekday(t *testing.T) { + t.Parallel() // ----- 2020-12 ------ ----- 2021-01 ------ // S M T W T F S S M T W T F S @@ -43,5 +44,4 @@ func TestCalendarWithMonthlyWeekday(t *testing.T) { }, cal.Dates().Strings(), ) - } diff --git a/pattern.go b/pattern.go index 77028a0..05541b7 100644 --- a/pattern.go +++ b/pattern.go @@ -1,9 +1,10 @@ package calendatext type Pattern struct { + DateMatcher + Enabled bool Description string - DateMatcher } func NewPattern(enabled bool, description string, matcher DateMatcher) *Pattern { diff --git a/period.go b/period.go index d3e5898..f19c1f1 100644 --- a/period.go +++ b/period.go @@ -16,8 +16,9 @@ func (pd *Period) Include(d *Date) bool { return pd.Start.BeforeEqual(d) && pd.End.AfterEqual(d) } -// Implement DateMatcher interface -func (pd Period) Match(other *Date) bool { +var _ DateMatcher = (*Period)(nil) // assert Period implements DateMatcher + +func (pd *Period) Match(other *Date) bool { return pd.Include(other) } diff --git a/period_test.go b/period_test.go index 9c55968..2b3363f 100644 --- a/period_test.go +++ b/period_test.go @@ -7,7 +7,9 @@ import ( ) func TestPeriodInclude(t *testing.T) { + t.Parallel() t.Run("single date", func(t *testing.T) { + t.Parallel() d := NewDate(2020, 8, 6) pd := NewPeriod(*d, *d) assert.False(t, pd.Include(d.PrevYear())) @@ -22,6 +24,7 @@ func TestPeriodInclude(t *testing.T) { }) t.Run("normal", func(t *testing.T) { + t.Parallel() d1 := NewDate(2020, 8, 6) d2 := NewDate(2020, 8, 20) pd := NewPeriod(*d1, *d2) diff --git a/text_parser.go b/text_parser.go index f8b5913..1ce949d 100644 --- a/text_parser.go +++ b/text_parser.go @@ -1,11 +1,11 @@ package calendatext import ( + "errors" + "fmt" "regexp" "strconv" "strings" - - "github.com/pkg/errors" ) type BuildMatcher func(s string) (DateMatcher, error) @@ -44,24 +44,25 @@ func (tp *textParser) Run(s string) error { func (tp *textParser) parseLine(line string) (*Pattern, error) { var enabled bool - if strings.HasPrefix(line, "+") { + switch { + case strings.HasPrefix(line, "+"): enabled = true - } else if strings.HasPrefix(line, "-") { + case strings.HasPrefix(line, "-"): enabled = false - } else { - return nil, errors.Errorf("Invalid first charactor. It must be '+' or '-': %q\n", line) + default: + return nil, fmt.Errorf("%w. It must be '+' or '-': %q", ErrInvalidFirstCharacter, line) } line = line[1:] - bodies := strings.SplitN(line, ":", 2) + bodies := strings.SplitN(line, ":", 2) // nolint:mnd description := "" - if len(bodies) == 2 { + if len(bodies) == 2 { // nolint:mnd description = strings.TrimSpace(bodies[1]) } matcher, err := tp.parseMatcher(strings.TrimSpace(bodies[0])) if err != nil { - return nil, errors.WithMessagef(err, "Failed to build matcher for %s", description) + return nil, fmt.Errorf("%w for %s: %w", ErrMatcherBuild, description, err) } return &Pattern{ Enabled: enabled, @@ -70,7 +71,7 @@ func (tp *textParser) parseLine(line string) (*Pattern, error) { }, nil } -func (tp *textParser) parseMatcher(body string) (DateMatcher, error) { +func (tp *textParser) parseMatcher(body string) (DateMatcher, error) { // nolint:ireturn for _, build := range tp.matcherBuilders { m, err := build(body) if err != nil { @@ -80,7 +81,7 @@ func (tp *textParser) parseMatcher(body string) (DateMatcher, error) { return m, nil } } - return nil, errors.Errorf("No build function found for %q", body) + return nil, fmt.Errorf("%w for %q", ErrBuildFunctionNotFound, body) } var ( @@ -89,9 +90,15 @@ var ( weeklyRE = regexp.MustCompile(`\A毎週`) monthlyDayRE = regexp.MustCompile(`\A毎月[^\d]*(\d+)日`) monthlyWeekdayRE = regexp.MustCompile(`\A毎月.*第(\d)(.+)`) + + ErrInvalidFirstCharacter = errors.New("invalid first character") + ErrSomethingWrongToParse = errors.New("something wrong to parse") + ErrPeriodSplit = errors.New("failed to split string as Period") + ErrMatcherBuild = errors.New("failed to build matcher") + ErrBuildFunctionNotFound = errors.New("no build function found") ) -func newMatcherBuilders(date *Date) []BuildMatcher { +func newMatcherBuilders(date *Date) []BuildMatcher { // nolint:gocognit,cyclop,funlen delimiter := "/" contextualParser := NewContextualDateParser(delimiter, date) @@ -126,12 +133,12 @@ func newMatcherBuilders(date *Date) []BuildMatcher { if len(m) < 1 { return nil, nil } - if len(m[0]) < 2 { - return nil, errors.Errorf("something wrong to parse %q", s) + if len(m[0]) < 2 { // nolint:mnd + return nil, fmt.Errorf("%w %q", ErrSomethingWrongToParse, s) } d, err := strconv.ParseInt(m[0][1], 10, 10) if err != nil { - return nil, err + return nil, err // nolint:wrapcheck } return MonthlyDay(d), nil }, @@ -142,8 +149,8 @@ func newMatcherBuilders(date *Date) []BuildMatcher { if len(m) < 1 { return nil, nil } - if len(m[0]) < 3 { - return nil, errors.Errorf("something wrong to parse %q", s) + if len(m[0]) < 3 { // nolint:mnd + return nil, fmt.Errorf("%w %q", ErrSomethingWrongToParse, s) } n, err := strconv.Atoi(m[0][1]) if err != nil { @@ -167,9 +174,9 @@ func newMatcherBuilders(date *Date) []BuildMatcher { if !slashPeriodRE.MatchString(s) { return nil, nil } - parts := strings.SplitN(s, "-", 2) - if len(parts) < 2 { - return nil, errors.Errorf("Failed to split string as Period: %q", s) + parts := strings.SplitN(s, "-", 2) // nolint:mnd + if len(parts) < 2 { // nolint:mnd + return nil, fmt.Errorf("%w %q", ErrPeriodSplit, s) } st, err := contextualParser.Parse(strings.TrimSpace(parts[0])) diff --git a/weekday.go b/weekday.go index f8409cc..cb831b6 100644 --- a/weekday.go +++ b/weekday.go @@ -1,9 +1,9 @@ package calendatext import ( + "errors" + "fmt" "time" - - "github.com/pkg/errors" ) type Weekday time.Weekday @@ -18,6 +18,8 @@ const ( Saturday = Weekday(time.Saturday) ) +const weekdays = 7 + func (wd Weekday) Match(d *Date) bool { if d == nil { return false @@ -25,7 +27,8 @@ func (wd Weekday) Match(d *Date) bool { return time.Weekday(wd) == d.Time().Weekday() } -var WeekdayNameMap = map[Weekday]rune{ +// See https://github.com/tecowl/calendatext/issues/5 +var WeekdayNameMap = map[Weekday]rune{ // nolint:gochecknoglobals Sunday: '日', Monday: '月', Tuesday: '火', @@ -35,6 +38,8 @@ var WeekdayNameMap = map[Weekday]rune{ Saturday: '土', } +var ErrUnknownWeekdayName = errors.New("unknown weekday name") + func ParseWeekdayName(s string) (*Weekday, error) { c := ([]rune(s))[0] for d, name := range WeekdayNameMap { @@ -42,5 +47,5 @@ func ParseWeekdayName(s string) (*Weekday, error) { return &d, nil } } - return nil, errors.Errorf("Unknown Weekday name %q", s) + return nil, fmt.Errorf("%w %q", ErrUnknownWeekdayName, s) } diff --git a/weekday_test.go b/weekday_test.go index a4bf836..c860064 100644 --- a/weekday_test.go +++ b/weekday_test.go @@ -8,6 +8,7 @@ import ( ) func TestCalendarWithWeekday(t *testing.T) { + t.Parallel() // ----- 2020-12 ------ ----- 2021-01 ------ // S M T W T F S S M T W T F S @@ -63,5 +64,4 @@ func TestCalendarWithWeekday(t *testing.T) { }, cal.Dates().Strings(), ) - }