feat: parse and project entries
This commit is contained in:
commit
3b75b960e0
602
entry.go
Normal file
602
entry.go
Normal file
@ -0,0 +1,602 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
"maps"
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
"unicode"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2024-01-01 12:01 [ ] (A) Do this thing @daily interval:2 count:6 until:2024-12-31
|
||||||
|
// 2024-01-01 12:01 [ ] (A) Do this thing @daily interval:2 count:6 until:2024-12-31 id:1
|
||||||
|
// 2024-01-01 12:01 [x] (A) Do this thing @daily interval:2 count:6 until:2024-12-31 id:1 completed:2024-01-01
|
||||||
|
// 2024-01-01 12:01 [ ] (A) Do this thing @daily interval:2 count:6 until:2024-12-31 id:2
|
||||||
|
|
||||||
|
// # Complete the entire series.
|
||||||
|
// 2024-01-01 12:01 [x] (A) Do this thing @daily interval:2 count:6 until:2024-12-31 completed:2024-12-31
|
||||||
|
|
||||||
|
// # multi line
|
||||||
|
// 2024-01-01
|
||||||
|
// [ ] (A) Do this thing @high
|
||||||
|
// [ ] (B) Do that thing @medium
|
||||||
|
// 12:01-13:01 read emails @low
|
||||||
|
|
||||||
|
// ## Time formats ##
|
||||||
|
// HH - start hour
|
||||||
|
// HH:MM - start hour and minute
|
||||||
|
// HH-HH - start and end hour
|
||||||
|
// HH:MM-HH:MM - start and end hour and minute
|
||||||
|
|
||||||
|
// ## To Do ##
|
||||||
|
// [ ] not done
|
||||||
|
// [!] at risk ?
|
||||||
|
// [x] done
|
||||||
|
|
||||||
|
// ## Todo Priority ##
|
||||||
|
// (A) High
|
||||||
|
// (B) Medium
|
||||||
|
// (C) Low
|
||||||
|
// None
|
||||||
|
|
||||||
|
// ## Repeat frequencies ##
|
||||||
|
// @yearly
|
||||||
|
// @monthly
|
||||||
|
// @weekly
|
||||||
|
// @daily
|
||||||
|
|
||||||
|
// ## Repeat modifiers ##
|
||||||
|
// interval:N
|
||||||
|
// count:N
|
||||||
|
// until:YYYY-MM-DD
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
Title string
|
||||||
|
Note string
|
||||||
|
Projects Set[string]
|
||||||
|
Contexts Set[string]
|
||||||
|
Attributes Attributes
|
||||||
|
|
||||||
|
Slate
|
||||||
|
Task
|
||||||
|
Event
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) Parse(text string) (int, error) {
|
||||||
|
i, err := e.Slate.Parse(text)
|
||||||
|
if err != nil {
|
||||||
|
i += trimSpace(text[i:])
|
||||||
|
// return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if strings.HasPrefix(text[i:], "- ") {
|
||||||
|
i++
|
||||||
|
i += trimSpace(text[i:])
|
||||||
|
e.Note = text[i:]
|
||||||
|
i += len(text[i:])
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
di, err := e.Task.Parse(text[i:])
|
||||||
|
if err == nil {
|
||||||
|
i += di
|
||||||
|
}
|
||||||
|
|
||||||
|
di, err = e.Event.Parse(text[i:])
|
||||||
|
if err == nil {
|
||||||
|
i += di
|
||||||
|
}
|
||||||
|
|
||||||
|
text = text[i:]
|
||||||
|
if len(text) > 0 {
|
||||||
|
i += len(text)
|
||||||
|
e.Title = text
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse attributes
|
||||||
|
e.Projects = make(Set[string])
|
||||||
|
e.Contexts = make(Set[string])
|
||||||
|
e.Attributes = make(Attributes)
|
||||||
|
for _, field := range strings.Fields(e.Title) {
|
||||||
|
if strings.HasPrefix(field, "@") {
|
||||||
|
e.Contexts.Add(field[1:])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(field, "+") {
|
||||||
|
e.Projects.Add(field[1:])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if key, value, ok := strings.Cut(field, ":"); ok {
|
||||||
|
e.Attributes[key] = append(e.Attributes[key], value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case e.Contexts.Has("yearly"):
|
||||||
|
e.Slate.Frequency = Yearly
|
||||||
|
case e.Contexts.Has("monthly"):
|
||||||
|
e.Slate.Frequency = Monthly
|
||||||
|
case e.Contexts.Has("weekly"):
|
||||||
|
e.Slate.Frequency = Weekly
|
||||||
|
case e.Contexts.Has("daily"):
|
||||||
|
e.Slate.Frequency = Daily
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := e.Attributes.GetN("until", 0); ok {
|
||||||
|
e.Slate.Until, _ = time.Parse("2006-01-02", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := e.Attributes.GetN("interval", 0); ok {
|
||||||
|
e.Slate.Interval, _ = strconv.Atoi(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := e.Attributes.GetN("count", 0); ok {
|
||||||
|
e.Slate.Count, _ = strconv.Atoi(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := e.Attributes.GetN("rel", 0); ok {
|
||||||
|
e.Task.RelatedTo, _ = time.Parse("2006-01-02", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if v, ok := e.Attributes.GetN("completed", 0); ok {
|
||||||
|
e.Task.CompletedOn, _ = time.Parse("2006-01-02", v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entry) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(e.Slate.String())
|
||||||
|
b.WriteString(e.DetailString())
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entry) DetailString() string {
|
||||||
|
var b strings.Builder
|
||||||
|
if !e.Task.IsZero(){
|
||||||
|
b.WriteString(" " + e.Task.String())
|
||||||
|
}
|
||||||
|
if !e.Event.IsZero() {
|
||||||
|
b.WriteString(" " + e.Event.String())
|
||||||
|
}
|
||||||
|
if len(e.Title) > 0 {
|
||||||
|
b.WriteString(" " + e.Title)
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entry) IsZero() bool {
|
||||||
|
return e.Slate.IsZero() &&
|
||||||
|
e.Task.IsZero() &&
|
||||||
|
e.Event.IsZero() &&
|
||||||
|
len(e.Attributes) == 0 &&
|
||||||
|
len(e.Contexts) == 0 &&
|
||||||
|
len(e.Projects) == 0 &&
|
||||||
|
len(e.Title) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entry) IsMulti() bool {
|
||||||
|
return !e.Slate.IsZero() &&
|
||||||
|
e.Task.IsZero() &&
|
||||||
|
e.Event.IsZero() &&
|
||||||
|
len(e.Attributes) == 0 &&
|
||||||
|
len(e.Contexts) == 0 &&
|
||||||
|
len(e.Projects) == 0 &&
|
||||||
|
len(e.Title) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entry) Project(start time.Time, until time.Duration) iter.Seq[time.Time] {
|
||||||
|
begin := start
|
||||||
|
end := start.Add(until)
|
||||||
|
|
||||||
|
return func(yield func(time.Time) bool) {
|
||||||
|
doProject := func(start time.Time) bool {
|
||||||
|
for ts := range e.Date.Project(start) {
|
||||||
|
if ts.Before(begin) || ts.After(end) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !yield(ts) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.Frequency {
|
||||||
|
case Yearly:
|
||||||
|
start = beginningOfYear(start)
|
||||||
|
for start.Before(end) {
|
||||||
|
if !doProject(start) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
start = start.AddDate(1, 0, 0)
|
||||||
|
start = beginningOfYear(start)
|
||||||
|
}
|
||||||
|
case Monthly:
|
||||||
|
start = beginningOfMonth(start)
|
||||||
|
for start.Before(end) {
|
||||||
|
if !doProject(start) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
start = start.AddDate(0, 1, 0)
|
||||||
|
}
|
||||||
|
case Weekly:
|
||||||
|
start = beginningOfWeek(start)
|
||||||
|
for start.Before(end) {
|
||||||
|
if !doProject(start) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
start = start.AddDate(0, 0, 7)
|
||||||
|
}
|
||||||
|
|
||||||
|
case Daily:
|
||||||
|
for start.Before(end) {
|
||||||
|
if !doProject(start) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
start = start.AddDate(0, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
start = time.Date(e.Date.Year, 1,1,0,0, 0, 0, time.UTC)
|
||||||
|
doProject(start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entry) Less(c Entry) bool { return false }
|
||||||
|
|
||||||
|
type Set[T comparable] map[T]struct{}
|
||||||
|
|
||||||
|
func NewSet[T comparable](vals ...T) Set[T] {
|
||||||
|
s := make(Set[T])
|
||||||
|
s.Add(vals...)
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Set[T]) Add(keys ...T) {
|
||||||
|
for _, key := range keys {
|
||||||
|
(*s)[key] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Set[T]) Has(key T) bool {
|
||||||
|
_, ok := s[key]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
type Attributes map[string][]string
|
||||||
|
|
||||||
|
func (a Attributes) GetN(key string, n int) (string, bool) {
|
||||||
|
if v, ok := a[key]; ok {
|
||||||
|
if len(v) > n {
|
||||||
|
return v[n], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
type Task struct {
|
||||||
|
Done bool
|
||||||
|
Priority Priority
|
||||||
|
|
||||||
|
RelatedTo time.Time
|
||||||
|
CompletedOn time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Task) Parse(text string) (int, error) {
|
||||||
|
if len(text) < 3 {
|
||||||
|
return 0, fmt.Errorf("invalid task")
|
||||||
|
}
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(text, "[ ]"):
|
||||||
|
i += 3
|
||||||
|
case strings.HasPrefix(text, "[x]"):
|
||||||
|
i += 3
|
||||||
|
t.Done = true
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("invalid task")
|
||||||
|
}
|
||||||
|
t.Priority = None
|
||||||
|
|
||||||
|
i += trimSpace(text[i:])
|
||||||
|
di, err := t.Priority.Parse(text[i:])
|
||||||
|
if err == nil {
|
||||||
|
i += di
|
||||||
|
}
|
||||||
|
i += trimSpace(text[i:])
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String
|
||||||
|
func (t Task) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteRune('[')
|
||||||
|
if t.Done {
|
||||||
|
b.WriteRune('x')
|
||||||
|
} else {
|
||||||
|
b.WriteRune(' ')
|
||||||
|
}
|
||||||
|
b.WriteRune(']')
|
||||||
|
b.WriteString(t.Priority.String())
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Task) IsZero() bool {
|
||||||
|
if t == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return t.Priority == 0 &&
|
||||||
|
t.CompletedOn.IsZero() &&
|
||||||
|
t.RelatedTo.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
StartsAt Time
|
||||||
|
EndsAt Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Event) Parse(text string) (int, error) {
|
||||||
|
if len(text) == 0 {
|
||||||
|
return 0, fmt.Errorf("invalid event")
|
||||||
|
}
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
di, v, err := parseDigits(text)
|
||||||
|
if err == nil {
|
||||||
|
e.StartsAt.Valid = true
|
||||||
|
e.StartsAt.Hour = v
|
||||||
|
i += di
|
||||||
|
} else {
|
||||||
|
return 0, fmt.Errorf("invalid event: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(text[i:]) > 0 && text[i] == ':' {
|
||||||
|
i++
|
||||||
|
di, v, err = parseDigits(text[i:])
|
||||||
|
if err == nil {
|
||||||
|
e.StartsAt.Minute = v
|
||||||
|
i += di
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(text[i:]) > 0 && text[i] == '-' {
|
||||||
|
i++
|
||||||
|
di, v, err = parseDigits(text[i:])
|
||||||
|
if err == nil {
|
||||||
|
e.EndsAt.Valid = true
|
||||||
|
e.EndsAt.Hour = v
|
||||||
|
i += di
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(text[i:]) > 0 && text[i] == ':' {
|
||||||
|
i++
|
||||||
|
di, v, err = parseDigits(text[i:])
|
||||||
|
if err == nil {
|
||||||
|
e.EndsAt.Minute = v
|
||||||
|
i += di
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i += trimSpace(text[i:])
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// String
|
||||||
|
func (e Event) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
if !e.StartsAt.Valid {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
b.WriteString(e.StartsAt.String())
|
||||||
|
if e.EndsAt.Valid {
|
||||||
|
b.WriteRune('-')
|
||||||
|
b.WriteString(e.EndsAt.String())
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Event) IsZero() bool {
|
||||||
|
if e == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return !e.StartsAt.Valid &&
|
||||||
|
!e.EndsAt.Valid
|
||||||
|
}
|
||||||
|
|
||||||
|
type Priority rune
|
||||||
|
|
||||||
|
const (
|
||||||
|
High Priority = 'A'
|
||||||
|
Medium Priority = 'B'
|
||||||
|
Low Priority = 'C'
|
||||||
|
None Priority = ' '
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Priority) Parse(text string) (int, error) {
|
||||||
|
if len(text) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(text, "(A)"):
|
||||||
|
i += 3
|
||||||
|
*p = High
|
||||||
|
case strings.HasPrefix(text, "(B)"):
|
||||||
|
i += 3
|
||||||
|
*p = Medium
|
||||||
|
case strings.HasPrefix(text, "(C)"):
|
||||||
|
i += 3
|
||||||
|
*p = Low
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("invalid priority")
|
||||||
|
}
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Priority) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
switch p {
|
||||||
|
case High, Medium, Low:
|
||||||
|
b.WriteRune(' ')
|
||||||
|
b.WriteRune('(')
|
||||||
|
b.WriteRune(rune(p))
|
||||||
|
b.WriteRune(')')
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimSpace(text string) int {
|
||||||
|
i := 0
|
||||||
|
for _, c := range text {
|
||||||
|
if unicode.IsSpace(c) {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDigits(text string) (int, int, error) {
|
||||||
|
i := 0
|
||||||
|
for _, c := range text {
|
||||||
|
if unicode.IsDigit(c) {
|
||||||
|
i++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
v, err := strconv.Atoi(text[:i])
|
||||||
|
return i, v, err
|
||||||
|
}
|
||||||
|
|
||||||
|
type Time struct {
|
||||||
|
Hour int
|
||||||
|
Minute int
|
||||||
|
Valid bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Time) Duration() time.Duration {
|
||||||
|
return time.Duration(t.Hour)*time.Hour + time.Duration(t.Minute)*time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Time) String() string {
|
||||||
|
var b strings.Builder
|
||||||
|
b.WriteString(strconv.Itoa(t.Hour))
|
||||||
|
if t.Minute > 0 {
|
||||||
|
b.WriteRune(':')
|
||||||
|
b.WriteString(strconv.Itoa(t.Minute))
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Entries map[string][]Entry
|
||||||
|
|
||||||
|
func NewEntries(entries ...Entry) Entries {
|
||||||
|
var e Entries
|
||||||
|
e.add(entries...)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
func (e *Entries) add(entries ...Entry) {
|
||||||
|
if *e == nil {
|
||||||
|
*e = make(map[string][]Entry, len(entries))
|
||||||
|
}
|
||||||
|
for _, entry := range entries {
|
||||||
|
(*e)[entry.Slate.Date.String()] = append((*e)[entry.Slate.Date.String()], entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entries) Iter() iter.Seq[Entry] {
|
||||||
|
keys := slices.Collect(maps.Keys(e))
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
return func(yield func(Entry) bool) {
|
||||||
|
for _, k := range keys {
|
||||||
|
for _, i := range (e)[k] {
|
||||||
|
if !yield(i) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entries) Parse(text string) (int, error) {
|
||||||
|
if len(text) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastDate Date
|
||||||
|
var lis []Entry
|
||||||
|
i := 0
|
||||||
|
|
||||||
|
for _, line := range strings.Split(text, "\n") {
|
||||||
|
i++
|
||||||
|
var e Entry
|
||||||
|
di, err := e.Parse(line)
|
||||||
|
i += di
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if e.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if e.Date.IsZero() {
|
||||||
|
e.Date = lastDate
|
||||||
|
}
|
||||||
|
if e.IsMulti() {
|
||||||
|
lastDate = e.Date
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lis = append(lis, e)
|
||||||
|
}
|
||||||
|
i--
|
||||||
|
e.add(lis...)
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entries) String() string {
|
||||||
|
keys := slices.Collect(maps.Keys(e))
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
var b strings.Builder
|
||||||
|
for _, key := range keys {
|
||||||
|
lis := e[key]
|
||||||
|
|
||||||
|
if len(lis) > 1 {
|
||||||
|
b.WriteString(key)
|
||||||
|
b.WriteString("\n")
|
||||||
|
|
||||||
|
for _, e := range lis {
|
||||||
|
b.WriteString("\t")
|
||||||
|
b.WriteString(e.DetailString())
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for _, e := range lis {
|
||||||
|
b.WriteString(e.String())
|
||||||
|
b.WriteString("\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return b.String()
|
||||||
|
}
|
276
entry_test.go
Normal file
276
entry_test.go
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
package main_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"iter"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/matryer/is"
|
||||||
|
|
||||||
|
cal "go.sour.is/cal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEntryParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
entry cal.Entry
|
||||||
|
string string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cal.Entry{
|
||||||
|
Slate: cal.Slate{
|
||||||
|
Date: cal.Date{Year: 2024, Month: 5, Day: 14},
|
||||||
|
},
|
||||||
|
Title: "make food",
|
||||||
|
Task: cal.Task{
|
||||||
|
Priority: cal.Priority('A'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2024-05-14 [ ] (A) make food",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Entry{
|
||||||
|
Slate: cal.Slate{
|
||||||
|
Date: cal.Date{Year: 2024, Month: 5, Day: 14},
|
||||||
|
},
|
||||||
|
Title: "make food",
|
||||||
|
Event: cal.Event{
|
||||||
|
StartsAt: cal.Time{Valid: true, Hour: 12},
|
||||||
|
EndsAt: cal.Time{Valid: true, Hour: 13},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2024-05-14 12:00-13:00 make food",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Entry{
|
||||||
|
Slate: cal.Slate{
|
||||||
|
Date: cal.Date{Year: 2024, Month: 5, Day: 14},
|
||||||
|
},
|
||||||
|
Title: "make food",
|
||||||
|
Task: cal.Task{
|
||||||
|
Priority: cal.Priority('A'),
|
||||||
|
},
|
||||||
|
Event: cal.Event{
|
||||||
|
StartsAt: cal.Time{Valid: true, Hour: 12},
|
||||||
|
EndsAt: cal.Time{Valid: true, Hour: 13},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2024-05-14 [ ] (A) 12:00-13:00 make food",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Entry{
|
||||||
|
Slate: cal.Slate{
|
||||||
|
Date: cal.Date{
|
||||||
|
Year: 2024,
|
||||||
|
Month: 5,
|
||||||
|
|
||||||
|
DayOfWeek: []cal.Day{
|
||||||
|
cal.NewDay(cal.Wednesday, 0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2024-05 Wed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Entry{
|
||||||
|
|
||||||
|
Title: "make food",
|
||||||
|
Task: cal.Task{
|
||||||
|
Priority: cal.Priority('A'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"[ ] (A) make food",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Entry{
|
||||||
|
|
||||||
|
Title: "make food @dinner +mealprep due:2024-05-14",
|
||||||
|
Task: cal.Task{
|
||||||
|
Priority: cal.Priority('A'),
|
||||||
|
},
|
||||||
|
Contexts: cal.NewSet("dinner"),
|
||||||
|
Projects: cal.NewSet("mealprep"),
|
||||||
|
Attributes: map[string][]string{
|
||||||
|
"due": {"2024-05-14"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"[ ] (A) make food @dinner +mealprep due:2024-05-14",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Entry{
|
||||||
|
Note: "this is a note",
|
||||||
|
},
|
||||||
|
"- this is a note",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Entry{
|
||||||
|
Note: "this is a note",
|
||||||
|
Slate: cal.Slate{
|
||||||
|
Date: cal.Date{
|
||||||
|
Year: 2024,
|
||||||
|
Month: 5,
|
||||||
|
|
||||||
|
DayOfWeek: []cal.Day{
|
||||||
|
cal.NewDay(cal.Wednesday, 0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2024-05 Wed - this is a note",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.string, func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
var entry cal.Entry
|
||||||
|
|
||||||
|
i, err := entry.Parse(tt.string)
|
||||||
|
is.NoErr(err)
|
||||||
|
compareEntry(t, entry, tt.entry)
|
||||||
|
|
||||||
|
is.Equal(i, len(tt.string))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareEntry(t *testing.T, a, b cal.Entry) {
|
||||||
|
t.Helper()
|
||||||
|
is := is.New(t)
|
||||||
|
is.Equal(a.Slate, b.Slate)
|
||||||
|
is.Equal(a.Title, b.Title)
|
||||||
|
if !a.Task.IsZero() {
|
||||||
|
is.Equal(a.Task, b.Task)
|
||||||
|
} else {
|
||||||
|
is.True(b.Task.IsZero())
|
||||||
|
}
|
||||||
|
if !a.Event.IsZero() {
|
||||||
|
is.True(!b.Event.IsZero())
|
||||||
|
is.Equal(a.Event, b.Event)
|
||||||
|
} else {
|
||||||
|
is.True(b.Event.IsZero())
|
||||||
|
}
|
||||||
|
if len(a.Contexts) > 0 {
|
||||||
|
is.Equal(a.Contexts, b.Contexts)
|
||||||
|
}
|
||||||
|
if len(a.Projects) > 0 {
|
||||||
|
is.Equal(a.Projects, b.Projects)
|
||||||
|
}
|
||||||
|
if len(a.Attributes) > 0 {
|
||||||
|
is.Equal(a.Attributes, b.Attributes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntriesParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
string string
|
||||||
|
entries cal.Entries
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
string: `2024-05-14 [ ] (A) make food
|
||||||
|
2024-05 Wed
|
||||||
|
[ ] one
|
||||||
|
[x] 5 two
|
||||||
|
[ ] (A) three
|
||||||
|
2025 Jan1 8-9 four`,
|
||||||
|
entries: cal.NewEntries(
|
||||||
|
cal.Entry{
|
||||||
|
Slate: cal.Slate{
|
||||||
|
Date: cal.Date{Year: 2024, Month: 5, Day: 14},
|
||||||
|
},
|
||||||
|
Title: "make food",
|
||||||
|
Task: cal.Task{
|
||||||
|
Priority: cal.Priority('A'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cal.Entry{
|
||||||
|
Slate: cal.Slate{
|
||||||
|
Date: cal.Date{Year: 2024, Month: 5, DayOfWeek: []cal.Day{
|
||||||
|
cal.NewDay(cal.Wednesday, 0),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
Title: "one",
|
||||||
|
Task: cal.Task{},
|
||||||
|
},
|
||||||
|
cal.Entry{
|
||||||
|
Slate: cal.Slate{
|
||||||
|
Date: cal.Date{Year: 2024, Month: 5, DayOfWeek: []cal.Day{
|
||||||
|
cal.NewDay(cal.Wednesday, 0),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
Title: "two",
|
||||||
|
Task: cal.Task{
|
||||||
|
Done: true,
|
||||||
|
},
|
||||||
|
Event: cal.Event{
|
||||||
|
StartsAt: cal.Time{Valid: true, Hour: 5},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cal.Entry{
|
||||||
|
Slate: cal.Slate{
|
||||||
|
Date: cal.Date{Year: 2024, Month: 5, DayOfWeek: []cal.Day{
|
||||||
|
cal.NewDay(cal.Wednesday, 0),
|
||||||
|
}},
|
||||||
|
},
|
||||||
|
Title: "three",
|
||||||
|
Task: cal.Task{
|
||||||
|
Priority: cal.Priority('A'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cal.Entry{
|
||||||
|
Slate: cal.Slate{
|
||||||
|
Date: cal.Date{Year: 2025, MonthOfYear: []cal.Month{cal.NewMonth(cal.January, 1)}},
|
||||||
|
},
|
||||||
|
Title: "four",
|
||||||
|
Event: cal.Event{
|
||||||
|
StartsAt: cal.Time{Valid: true, Hour: 8},
|
||||||
|
EndsAt: cal.Time{Valid: true, Hour: 9},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.string[:15], func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
var entries cal.Entries
|
||||||
|
|
||||||
|
i, err := entries.Parse(tt.string)
|
||||||
|
|
||||||
|
ttEntries := slices.Collect(tt.entries.Iter())
|
||||||
|
for i, entry := range enumerate(entries.Iter()) {
|
||||||
|
compareEntry(t, entry, ttEntries[i])
|
||||||
|
}
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(i, len(tt.string))
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:embed todo.txt
|
||||||
|
var todotxt string
|
||||||
|
|
||||||
|
func TestFileParse(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
var entries cal.Entries
|
||||||
|
i, err := entries.Parse(todotxt)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(i, len(todotxt))
|
||||||
|
_ = entries.String()
|
||||||
|
}
|
||||||
|
func enumerate[T any](it iter.Seq[T]) iter.Seq2[int, T] {
|
||||||
|
return func(yield func(int, T) bool) {
|
||||||
|
i := 0
|
||||||
|
for v := range it {
|
||||||
|
if !yield(i, v) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
5
go.mod
Normal file
5
go.mod
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
module go.sour.is/cal
|
||||||
|
|
||||||
|
go 1.23.3
|
||||||
|
|
||||||
|
require github.com/matryer/is v1.4.1
|
2
go.sum
Normal file
2
go.sum
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||||
|
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
43
main.go
Normal file
43
main.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed todo.txt
|
||||||
|
var todotxt []byte
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
fmt.Println("hello!")
|
||||||
|
|
||||||
|
// text, _ := io.ReadAll(os.Stdin)
|
||||||
|
|
||||||
|
var lis Entries
|
||||||
|
lis.Parse(string(todotxt))
|
||||||
|
|
||||||
|
type entry struct {
|
||||||
|
time time.Time
|
||||||
|
e Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
// now := time.Date(time.Now().Year(), 1, 1, 0, 0, 0, 0, time.UTC)
|
||||||
|
pq := PriorityQueue(func(a, b *entry) bool {
|
||||||
|
return a.time.Before(b.time)
|
||||||
|
})
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
for e := range lis.Iter() {
|
||||||
|
for p := range e.Project(now, 365*24*time.Hour) {
|
||||||
|
pq.Insert(&entry{p, e})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for !pq.IsEmpty() {
|
||||||
|
entry := pq.ExtractMin()
|
||||||
|
e, p := entry.e, entry.time
|
||||||
|
|
||||||
|
fmt.Println(p.Format("2006-01-02"), e.DetailString())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
40
pqueue.go
Normal file
40
pqueue.go
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "sort"
|
||||||
|
|
||||||
|
type priorityQueue[T any] struct {
|
||||||
|
elems []*T
|
||||||
|
less func(a, b *T) bool
|
||||||
|
maxDepth int
|
||||||
|
totalEnqueue int
|
||||||
|
totalDequeue int
|
||||||
|
}
|
||||||
|
|
||||||
|
// PriorityQueue implements a simple slice based queue.
|
||||||
|
// less is the function for sorting. reverse a and b to reverse the sort.
|
||||||
|
// T is the item
|
||||||
|
// U is a slice of T
|
||||||
|
func PriorityQueue[T any](less func(a, b *T) bool) *priorityQueue[T] {
|
||||||
|
return &priorityQueue[T]{less: less}
|
||||||
|
}
|
||||||
|
func (pq *priorityQueue[T]) Insert(elem *T) {
|
||||||
|
pq.totalEnqueue++
|
||||||
|
|
||||||
|
pq.elems = append(pq.elems, elem)
|
||||||
|
pq.maxDepth = max(pq.maxDepth, len(pq.elems))
|
||||||
|
}
|
||||||
|
func (pq *priorityQueue[T]) IsEmpty() bool {
|
||||||
|
return len(pq.elems) == 0
|
||||||
|
}
|
||||||
|
func (pq *priorityQueue[T]) ExtractMin() *T {
|
||||||
|
pq.totalDequeue++
|
||||||
|
|
||||||
|
var elem *T
|
||||||
|
if pq.IsEmpty() {
|
||||||
|
return elem
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(pq.elems, func(i, j int) bool { return pq.less(pq.elems[j], pq.elems[i]) })
|
||||||
|
pq.elems, elem = pq.elems[:len(pq.elems)-1], pq.elems[len(pq.elems)-1]
|
||||||
|
return elem
|
||||||
|
}
|
637
slate_test.go
Normal file
637
slate_test.go
Normal file
@ -0,0 +1,637 @@
|
|||||||
|
package main_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"iter"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/matryer/is"
|
||||||
|
|
||||||
|
cal "go.sour.is/cal"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDay(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
dayOfWeek cal.Day
|
||||||
|
weekOfMonth int
|
||||||
|
string string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"test monday 0",
|
||||||
|
cal.Monday,
|
||||||
|
0,
|
||||||
|
"Mon",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"test monday 1",
|
||||||
|
cal.Monday,
|
||||||
|
1,
|
||||||
|
"1Mon",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"test monday 5",
|
||||||
|
cal.Monday,
|
||||||
|
5,
|
||||||
|
"5Mon",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"test monday -1",
|
||||||
|
cal.Monday,
|
||||||
|
-1,
|
||||||
|
"-1Mon",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"test monday -5",
|
||||||
|
cal.Monday,
|
||||||
|
-5,
|
||||||
|
"-5Mon",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
day := cal.NewDay(tt.dayOfWeek, tt.weekOfMonth)
|
||||||
|
|
||||||
|
is.Equal(day.Day(), tt.dayOfWeek)
|
||||||
|
is.Equal(day.WeekOfMonth(), tt.weekOfMonth)
|
||||||
|
is.Equal(day.String(), tt.string)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonth(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
month cal.Month
|
||||||
|
dayOfMonth int
|
||||||
|
string string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cal.January,
|
||||||
|
0,
|
||||||
|
"Jan",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.February,
|
||||||
|
1,
|
||||||
|
"Feb1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.March,
|
||||||
|
31,
|
||||||
|
"Mar31",
|
||||||
|
},
|
||||||
|
{month: cal.April, string: "Apr"},
|
||||||
|
{month: cal.May, string: "May"},
|
||||||
|
{month: cal.June, string: "Jun"},
|
||||||
|
{month: cal.July, string: "Jul"},
|
||||||
|
{month: cal.August, string: "Aug"},
|
||||||
|
{month: cal.September, string: "Sep"},
|
||||||
|
{month: cal.October, string: "Oct"},
|
||||||
|
{month: cal.November, string: "Nov"},
|
||||||
|
{month: cal.December, string: "Dec"},
|
||||||
|
}
|
||||||
|
for i, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d %s %d %s", i, tt.month, tt.dayOfMonth, tt.string), func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
month := cal.NewMonth(tt.month, tt.dayOfMonth)
|
||||||
|
|
||||||
|
is.Equal(month.Month(), tt.month)
|
||||||
|
is.Equal(month.DayOfMonth(), tt.dayOfMonth)
|
||||||
|
is.Equal(month.String(), tt.string)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWeek(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
week int
|
||||||
|
string string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
"test week 1",
|
||||||
|
1,
|
||||||
|
"w01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"test week 52",
|
||||||
|
52,
|
||||||
|
"w52",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
week := cal.NewWeek(tt.week)
|
||||||
|
|
||||||
|
is.Equal(week.WeekOfYear(), tt.week)
|
||||||
|
is.Equal(week.String(), tt.string)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDate(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
date cal.Date
|
||||||
|
string string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cal.Date{
|
||||||
|
Year: 2022,
|
||||||
|
},
|
||||||
|
"2022",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Date{
|
||||||
|
Year: 2022,
|
||||||
|
Month: 2,
|
||||||
|
},
|
||||||
|
"2022-02",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Date{
|
||||||
|
Year: 2022,
|
||||||
|
Month: 1,
|
||||||
|
Day: 1,
|
||||||
|
},
|
||||||
|
"2022-01-01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Date{
|
||||||
|
Year: 2022,
|
||||||
|
WeekOfYear: []cal.Week{
|
||||||
|
cal.NewWeek(1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2022 w01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Date{
|
||||||
|
Year: 2022,
|
||||||
|
WeekOfYear: []cal.Week{
|
||||||
|
cal.NewWeek(1),
|
||||||
|
cal.NewWeek(4),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2022 w01, w04",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Date{
|
||||||
|
Year: 2022,
|
||||||
|
MonthOfYear: []cal.Month{
|
||||||
|
cal.NewMonth(cal.January, 17),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2022 Jan17",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Date{
|
||||||
|
Year: 2022,
|
||||||
|
MonthOfYear: []cal.Month{
|
||||||
|
cal.NewMonth(cal.January, 1),
|
||||||
|
cal.NewMonth(cal.December, -1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2022 Jan1, Dec-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Date{
|
||||||
|
Year: 2022,
|
||||||
|
MonthOfYear: []cal.Month{
|
||||||
|
cal.January,
|
||||||
|
},
|
||||||
|
DayOfWeek: []cal.Day{
|
||||||
|
cal.NewDay(cal.Monday, 2),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2022 Jan 2Mon",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Date{
|
||||||
|
Year: 2022,
|
||||||
|
WeekOfYear: []cal.Week{
|
||||||
|
cal.NewWeek(6),
|
||||||
|
},
|
||||||
|
DayOfWeek: []cal.Day{
|
||||||
|
cal.Tuesday,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2022 w06 Tue",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Date{
|
||||||
|
Year: 2022,
|
||||||
|
Month: 8,
|
||||||
|
DayOfWeek: []cal.Day{
|
||||||
|
cal.Wednesday,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2022-08 Wed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Date{
|
||||||
|
Year: 2022,
|
||||||
|
Month: 8,
|
||||||
|
DayOfWeek: []cal.Day{
|
||||||
|
cal.Thursday,
|
||||||
|
cal.Friday,
|
||||||
|
cal.Saturday,
|
||||||
|
cal.Sunday,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"2022-08 Thu, Fri, Sat, Sun",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Date{},
|
||||||
|
"",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d %s", i, tt.string), func(t *testing.T) {
|
||||||
|
_ = i
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
s := tt.date.String()
|
||||||
|
|
||||||
|
is.Equal(s, tt.string)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMonthParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
month cal.Month
|
||||||
|
day int
|
||||||
|
string string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cal.January,
|
||||||
|
1,
|
||||||
|
"Jan1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.March,
|
||||||
|
2,
|
||||||
|
"March2",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.February,
|
||||||
|
3,
|
||||||
|
"feb3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.April,
|
||||||
|
-3,
|
||||||
|
"april-3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.May,
|
||||||
|
0,
|
||||||
|
"MAY",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.June,
|
||||||
|
-30,
|
||||||
|
"JUN-30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.July,
|
||||||
|
4,
|
||||||
|
"jul4",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.August,
|
||||||
|
3,
|
||||||
|
"aug3",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.September,
|
||||||
|
21,
|
||||||
|
"sept21",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.October,
|
||||||
|
31,
|
||||||
|
"Oct31",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.November,
|
||||||
|
15,
|
||||||
|
"nov15",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.December,
|
||||||
|
25,
|
||||||
|
"december25",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.string, func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
var m cal.Month
|
||||||
|
|
||||||
|
i, err := m.Parse(tt.string)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(m.Month(), tt.month)
|
||||||
|
is.Equal(m.DayOfMonth(), tt.day)
|
||||||
|
is.Equal(i, len(tt.string))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDayParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
day cal.Day
|
||||||
|
week int
|
||||||
|
string string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
cal.Monday,
|
||||||
|
1,
|
||||||
|
"1monday",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Tuesday,
|
||||||
|
2,
|
||||||
|
"2Tuesday",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Wednesday,
|
||||||
|
3,
|
||||||
|
"3Wed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Thursday,
|
||||||
|
4,
|
||||||
|
"4Thur",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Friday,
|
||||||
|
-5,
|
||||||
|
"-5Friday",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Saturday,
|
||||||
|
-2,
|
||||||
|
"-2Saturday",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cal.Sunday,
|
||||||
|
-1,
|
||||||
|
"-1Sunday",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.string, func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
var d cal.Day
|
||||||
|
|
||||||
|
i, err := d.Parse(tt.string)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(d.Day(), tt.day)
|
||||||
|
is.Equal(d.WeekOfMonth(), tt.week)
|
||||||
|
is.Equal(i, len(tt.string))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWeekParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
week int
|
||||||
|
string string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
1,
|
||||||
|
"w01",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
52,
|
||||||
|
"w52",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.string, func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
var w cal.Week
|
||||||
|
|
||||||
|
i, err := w.Parse(tt.string)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(w.WeekOfYear(), tt.week)
|
||||||
|
is.Equal(i, len(tt.string))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateParse(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
date cal.Date
|
||||||
|
string string
|
||||||
|
extra string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
date: cal.Date{Year: 2024},
|
||||||
|
string: "2024",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{Year: 2024},
|
||||||
|
string: "2024 ",
|
||||||
|
extra: "make food",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{Year: 2024, WeekOfYear: []cal.Week{cal.NewWeek(5)}},
|
||||||
|
string: "2024 w05",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{Year: 2024, DayOfWeek: []cal.Day{cal.NewDay(cal.Monday, 2)}},
|
||||||
|
string: "2024 2mon",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{Year: 2024, WeekOfYear: []cal.Week{cal.NewWeek(5), cal.NewWeek(10)}},
|
||||||
|
string: "2024 w05, w10",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{Year: 2024, WeekOfYear: []cal.Week{cal.NewWeek(5)}, DayOfWeek: []cal.Day{cal.NewDay(cal.Monday, 0)}},
|
||||||
|
string: "2024 w05 mon",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{Year: 2024, WeekOfYear: []cal.Week{cal.NewWeek(5)}},
|
||||||
|
string: "2024 w05 ",
|
||||||
|
extra: "monitoring",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{
|
||||||
|
Year: 2024,
|
||||||
|
WeekOfYear: []cal.Week{cal.NewWeek(5), cal.NewWeek(17)},
|
||||||
|
DayOfWeek: []cal.Day{cal.NewDay(cal.Monday, 0), cal.NewDay(cal.Friday, 0)},
|
||||||
|
},
|
||||||
|
string: "2024 w05 mon, w17 fri",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{
|
||||||
|
Year: 2024,
|
||||||
|
MonthOfYear: []cal.Month{cal.NewMonth(cal.September, 0)},
|
||||||
|
DayOfWeek: []cal.Day{cal.NewDay(cal.Monday, 1)},
|
||||||
|
},
|
||||||
|
string: "2024 Sept 1mon",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{
|
||||||
|
Year: 2024,
|
||||||
|
MonthOfYear: []cal.Month{cal.NewMonth(cal.September, 1)},
|
||||||
|
},
|
||||||
|
string: "2024 Sept1 ",
|
||||||
|
extra: "monitoring",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{
|
||||||
|
Year: 2024,
|
||||||
|
MonthOfYear: []cal.Month{
|
||||||
|
cal.NewMonth(cal.September, 1),
|
||||||
|
cal.NewMonth(cal.October, -1),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
string: "2024 Sept1, October-1 ",
|
||||||
|
extra: "monitoring",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{
|
||||||
|
Year: 2024,
|
||||||
|
MonthOfYear: []cal.Month{cal.NewMonth(cal.September, 1)},
|
||||||
|
},
|
||||||
|
string: "2024 Sept1 ",
|
||||||
|
extra: "monitoring",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{
|
||||||
|
Year: 2024,
|
||||||
|
MonthOfYear: []cal.Month{cal.NewMonth(cal.August, 0), cal.NewMonth(cal.October, 0)},
|
||||||
|
DayOfWeek: []cal.Day{cal.NewDay(cal.Monday, 2), cal.NewDay(cal.Friday, -1)},
|
||||||
|
},
|
||||||
|
string: "2024 Aug 2mon, Oct -1fri",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
date: cal.Date{Year: 2024, Month: 5},
|
||||||
|
string: "2024-05",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{Year: 2024, Month: 5, DayOfWeek: []cal.Day{cal.NewDay(cal.Monday, 2)}},
|
||||||
|
string: "2024-05 2mon",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{Year: 2024, Month: 5, DayOfWeek: []cal.Day{
|
||||||
|
cal.NewDay(cal.Monday, 2),
|
||||||
|
cal.NewDay(cal.Thursday, 3),
|
||||||
|
}},
|
||||||
|
string: "2024-05 2mon, 3thur",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: cal.Date{Year: 2024, Month: 5, DayOfWeek: []cal.Day{
|
||||||
|
cal.NewDay(cal.Monday, 2),
|
||||||
|
cal.NewDay(cal.Thursday, 3),
|
||||||
|
}},
|
||||||
|
string: "2024-05 2mon, 3thur ",
|
||||||
|
extra: "make food",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
date: cal.Date{Year: 2024, Month: 5, Day: 14},
|
||||||
|
string: "2024-05-14",
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
date: cal.Date{Year: 2024, Month: 5, Day: 14},
|
||||||
|
string: "2024-05-14 ",
|
||||||
|
extra: "make food",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for i, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%d %s%s", i, tt.string, tt.extra), func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
var d cal.Date
|
||||||
|
|
||||||
|
i, err := d.Parse(tt.string + tt.extra)
|
||||||
|
is.NoErr(err)
|
||||||
|
is.Equal(fmt.Sprintf("%#v", d), fmt.Sprintf("%#v", tt.date))
|
||||||
|
is.Equal(d, tt.date)
|
||||||
|
is.Equal(i, len(tt.string))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDateProject(t *testing.T) {
|
||||||
|
type test struct {
|
||||||
|
date string
|
||||||
|
start string
|
||||||
|
out string
|
||||||
|
}
|
||||||
|
tests := []test{
|
||||||
|
{"2024-05-14", "2024-05-14", "2024-05-14"},
|
||||||
|
{"2024-05-14", "2024-06-24", "2024-06-24"},
|
||||||
|
{"2024-05", "2024-05-01", "2024-05-31"},
|
||||||
|
{"2024-05", "2025-02-01", "2025-02-28"},
|
||||||
|
{"2024-05 2mon", "2025-02-01", "2025-02-10"},
|
||||||
|
{"2024-05 2mon, 2fri", "2025-02-11", "2025-02-14"},
|
||||||
|
{"2024-05 -1mon, -2fri", "2025-02-01", "2025-02-24 2025-02-21"},
|
||||||
|
{"2024 feb -1mon, feb -2fri", "2025-02-01", "2025-02-24 2025-02-21"},
|
||||||
|
{"2024", "2025-02-01", "2025-12-31"},
|
||||||
|
{"2024 mar mon, mar 3mon", "2025-02-01", "2025-03-03 2025-03-17"},
|
||||||
|
{"2024 mar3, mar17", "2025-03-01", "2025-03-03 2025-03-17"},
|
||||||
|
{"2024 mar, nov", "2025-03-01", "2025-03-31 2025-11-30"},
|
||||||
|
{"2024 w03, w-2", "2025-01-01", "2025-01-18 2025-12-27"},
|
||||||
|
{"2024 w03 mon, w-1 wed, w-01 sat", "2025-01-01", "2025-01-13 2025-12-31 2026-01-03"},
|
||||||
|
}
|
||||||
|
for i, tt := range tests {
|
||||||
|
t.Run(fmt.Sprintf("%03d %s %s", i, tt.date, tt.start), func(t *testing.T) {
|
||||||
|
is := is.New(t)
|
||||||
|
|
||||||
|
var date cal.Date
|
||||||
|
_, err := date.Parse(tt.date)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
start, err := time.Parse("2006-01-02", tt.start)
|
||||||
|
is.NoErr(err)
|
||||||
|
|
||||||
|
out := collectN(len(tt.out)+1, date.Project(start))
|
||||||
|
|
||||||
|
fields := strings.Fields(tt.out)
|
||||||
|
is.Equal(len(out), len(fields))
|
||||||
|
for i, v := range fields {
|
||||||
|
is.Equal(out[i].Format("2006-01-02"), v)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectN[T any](n int, seq iter.Seq[T]) []T {
|
||||||
|
out := make([]T, 0, n)
|
||||||
|
|
||||||
|
i := 0
|
||||||
|
for v := range seq {
|
||||||
|
out = append(out, v)
|
||||||
|
i++
|
||||||
|
if i == n {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func timeDate(year, month, day int) time.Time {
|
||||||
|
return time.Date(year, time.Month(month), day, 0, 0, 0, 0, time.UTC)
|
||||||
|
}
|
19
test.puml
Normal file
19
test.puml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
@startuml test
|
||||||
|
split
|
||||||
|
-[hidden]->
|
||||||
|
split
|
||||||
|
-[hidden]->
|
||||||
|
:Delta;
|
||||||
|
split again
|
||||||
|
-[hidden]->
|
||||||
|
:Cumulative;
|
||||||
|
end split
|
||||||
|
|
||||||
|
:Derived;
|
||||||
|
|
||||||
|
split again
|
||||||
|
-[hidden]->
|
||||||
|
:foo;
|
||||||
|
end split
|
||||||
|
:member profile;
|
||||||
|
@enduml
|
Loading…
x
Reference in New Issue
Block a user