feat: parse and project entries
This commit is contained in:
commit
3b75b960e0
0
README.md
Normal file
0
README.md
Normal file
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
|
1
todo.txt
Normal file
1
todo.txt
Normal file
@ -0,0 +1 @@
|
||||
2025-01 Fri [ ] Take out Trash @weekly
|
Loading…
x
Reference in New Issue
Block a user