✨ Add tools.ParseDate
This commit is contained in:
parent
c01fb53a0e
commit
70b82761c2
8 changed files with 1067 additions and 8 deletions
|
|
@ -43,7 +43,7 @@ func Execute() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
cobra.OnInitialize(initConfig)
|
//cobra.OnInitialize(initConfig)
|
||||||
|
|
||||||
// Here you will define your flags and configuration settings.
|
// Here you will define your flags and configuration settings.
|
||||||
// Cobra supports persistent flags, which, if defined here,
|
// Cobra supports persistent flags, which, if defined here,
|
||||||
|
|
@ -56,9 +56,3 @@ func init() {
|
||||||
// when this action is called directly.
|
// when this action is called directly.
|
||||||
// rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
// rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
}
|
}
|
||||||
|
|
||||||
// initConfig reads in config file and ENV variables if set.
|
|
||||||
func initConfig() {
|
|
||||||
// @todo
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
|
||||||
73
cmd/test.go
Normal file
73
cmd/test.go
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
/*
|
||||||
|
Copyright © 2024 Dan Jones <danjones@goodevilgenius.org>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"codeberg.org/danjones000/my-log/config"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
// "github.com/BurntSushi/toml"
|
||||||
|
dp "github.com/markusmobius/go-dateparser"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testCmd represents the test command
|
||||||
|
var testCmd = &cobra.Command{
|
||||||
|
Use: "test",
|
||||||
|
Short: "A brief description of your command",
|
||||||
|
//Long: ``,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
c, err := config.Load()
|
||||||
|
fmt.Println("error", err)
|
||||||
|
fmt.Printf("%+v\n", c)
|
||||||
|
//ne := "output.stdout.config.json = true"
|
||||||
|
//toml.Decode(ne, &c)
|
||||||
|
st, _ := c.Outputs.Stdout()
|
||||||
|
fmt.Printf("%+v\n", st)
|
||||||
|
//nc := "\n[input]\nrecurse = false\n[output.stdout.config]\njson = true"
|
||||||
|
//toml.Decode(nc, &c)
|
||||||
|
//fmt.Printf("%+v\n", c)
|
||||||
|
d, err := dp.Parse(nil, "now")
|
||||||
|
fmt.Println(d.Time, d.Period)
|
||||||
|
d, err = dp.Parse(nil, "today")
|
||||||
|
fmt.Println(d.Time, d.Period)
|
||||||
|
d, err = dp.Parse(nil, "2 days, 3 minutes ago")
|
||||||
|
fmt.Println(d.Time, d.Period)
|
||||||
|
d, err = dp.Parse(nil, "in 2 decades, 5 days, 3 minutes")
|
||||||
|
fmt.Println(d.Time, d.Period)
|
||||||
|
d, err = dp.Parse(nil, "3 years ago")
|
||||||
|
fmt.Println(d.Time, d.Period)
|
||||||
|
d, err = dp.Parse(nil, "1707711800")
|
||||||
|
fmt.Println(d.Time, d.Period)
|
||||||
|
d, err = dp.Parse(nil, "@1707711800")
|
||||||
|
fmt.Println(d.Time, d.Period)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
rootCmd.AddCommand(testCmd)
|
||||||
|
|
||||||
|
// Here you will define your flags and configuration settings.
|
||||||
|
|
||||||
|
// Cobra supports Persistent Flags which will work for this command
|
||||||
|
// and all subcommands, e.g.:
|
||||||
|
// testCmd.PersistentFlags().String("foo", "", "A help for foo")
|
||||||
|
|
||||||
|
// Cobra supports local flags which will only run when this command
|
||||||
|
// is called directly, e.g.:
|
||||||
|
// testCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||||
|
}
|
||||||
|
|
@ -3,7 +3,6 @@ package config
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
//fp "path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
|
||||||
863
cover.html
Normal file
863
cover.html
Normal file
|
|
@ -0,0 +1,863 @@
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<title>config: Go Coverage Report</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
background: black;
|
||||||
|
color: rgb(80, 80, 80);
|
||||||
|
}
|
||||||
|
body, pre, #legend span {
|
||||||
|
font-family: Menlo, monospace;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#topbar {
|
||||||
|
background: black;
|
||||||
|
position: fixed;
|
||||||
|
top: 0; left: 0; right: 0;
|
||||||
|
height: 42px;
|
||||||
|
border-bottom: 1px solid rgb(80, 80, 80);
|
||||||
|
}
|
||||||
|
#content {
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
#nav, #legend {
|
||||||
|
float: left;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
#legend {
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
#nav {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
#legend span {
|
||||||
|
margin: 0 5px;
|
||||||
|
}
|
||||||
|
.cov0 { color: rgb(192, 0, 0) }
|
||||||
|
.cov1 { color: rgb(128, 128, 128) }
|
||||||
|
.cov2 { color: rgb(116, 140, 131) }
|
||||||
|
.cov3 { color: rgb(104, 152, 134) }
|
||||||
|
.cov4 { color: rgb(92, 164, 137) }
|
||||||
|
.cov5 { color: rgb(80, 176, 140) }
|
||||||
|
.cov6 { color: rgb(68, 188, 143) }
|
||||||
|
.cov7 { color: rgb(56, 200, 146) }
|
||||||
|
.cov8 { color: rgb(44, 212, 149) }
|
||||||
|
.cov9 { color: rgb(32, 224, 152) }
|
||||||
|
.cov10 { color: rgb(20, 236, 155) }
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="topbar">
|
||||||
|
<div id="nav">
|
||||||
|
<select id="files">
|
||||||
|
|
||||||
|
<option value="file0">codeberg.org/danjones000/my-log/config/default.go (100.0%)</option>
|
||||||
|
|
||||||
|
<option value="file1">codeberg.org/danjones000/my-log/config/load.go (97.4%)</option>
|
||||||
|
|
||||||
|
<option value="file2">codeberg.org/danjones000/my-log/files/append.go (94.7%)</option>
|
||||||
|
|
||||||
|
<option value="file3">codeberg.org/danjones000/my-log/models/entry.go (100.0%)</option>
|
||||||
|
|
||||||
|
<option value="file4">codeberg.org/danjones000/my-log/models/errors.go (100.0%)</option>
|
||||||
|
|
||||||
|
<option value="file5">codeberg.org/danjones000/my-log/models/log.go (100.0%)</option>
|
||||||
|
|
||||||
|
<option value="file6">codeberg.org/danjones000/my-log/models/meta.go (100.0%)</option>
|
||||||
|
|
||||||
|
<option value="file7">codeberg.org/danjones000/my-log/tools/parse.go (100.0%)</option>
|
||||||
|
|
||||||
|
<option value="file8">codeberg.org/danjones000/my-log/tools/parse_date.go (86.7%)</option>
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div id="legend">
|
||||||
|
<span>not tracked</span>
|
||||||
|
|
||||||
|
<span class="cov0">no coverage</span>
|
||||||
|
<span class="cov1">low coverage</span>
|
||||||
|
<span class="cov2">*</span>
|
||||||
|
<span class="cov3">*</span>
|
||||||
|
<span class="cov4">*</span>
|
||||||
|
<span class="cov5">*</span>
|
||||||
|
<span class="cov6">*</span>
|
||||||
|
<span class="cov7">*</span>
|
||||||
|
<span class="cov8">*</span>
|
||||||
|
<span class="cov9">*</span>
|
||||||
|
<span class="cov10">high coverage</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="content">
|
||||||
|
|
||||||
|
<pre class="file" id="file0" style="display: none">package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
fp "path/filepath"
|
||||||
|
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ConfigStr = `# Configuration for my-log
|
||||||
|
|
||||||
|
[input]
|
||||||
|
# Path to where the log files are stored
|
||||||
|
path = "%s"
|
||||||
|
# File extension for log files
|
||||||
|
ext = "txt"
|
||||||
|
# Whether to look in sub-folders
|
||||||
|
recurse = true
|
||||||
|
|
||||||
|
# config for output types
|
||||||
|
[output]
|
||||||
|
|
||||||
|
# This one just prints the logs to stdout when run
|
||||||
|
[output.stdout]
|
||||||
|
enabled = true
|
||||||
|
[output.stdout.config]
|
||||||
|
# Whether to output as JSON. Maybe useful to pipe elsewhere.
|
||||||
|
json = false
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
func DefaultStr() string <span class="cov10" title="8">{
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
inDir := fp.Join(home, "my-log")
|
||||||
|
return fmt.Sprintf(ConfigStr, inDir)
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
func DefaultConfig() (Config, error) <span class="cov10" title="8">{
|
||||||
|
s := DefaultStr()
|
||||||
|
c := Config{}
|
||||||
|
_, err := toml.Decode(s, &c)
|
||||||
|
return c, err
|
||||||
|
}</span>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre class="file" id="file1" style="display: none">package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
fp "path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/danjones000/my-log/tools"
|
||||||
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/caarlos0/env/v10"
|
||||||
|
mapst "github.com/mitchellh/mapstructure"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ConfigPath string
|
||||||
|
var Overrides = map[string]string{}
|
||||||
|
|
||||||
|
func init() <span class="cov1" title="1">{
|
||||||
|
conf, _ := os.UserConfigDir()
|
||||||
|
ConfigPath = fp.Join(conf, "my-log", "config.toml")
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
func Load() (Config, error) <span class="cov10" title="6">{
|
||||||
|
c, _ := DefaultConfig()
|
||||||
|
_, err := os.Stat(ConfigPath)
|
||||||
|
if !os.IsNotExist(err) </span><span class="cov4" title="2">{
|
||||||
|
_, err = toml.DecodeFile(ConfigPath, &c)
|
||||||
|
if err != nil </span><span class="cov1" title="1">{
|
||||||
|
return c, err
|
||||||
|
}</span>
|
||||||
|
}
|
||||||
|
<span class="cov9" title="5">env.Parse(&c)
|
||||||
|
c.Outputs["stdout"] = loadStdout(c.Outputs["stdout"])
|
||||||
|
|
||||||
|
l := ""
|
||||||
|
for k, v := range Overrides </span><span class="cov7" title="4">{
|
||||||
|
val := tools.ParseString(v)
|
||||||
|
if val == nil </span><span class="cov1" title="1">{
|
||||||
|
continue</span>
|
||||||
|
}
|
||||||
|
<span class="cov6" title="3">if _, isJson := val.(json.RawMessage); isJson </span><span class="cov4" title="2">{
|
||||||
|
continue</span>
|
||||||
|
}
|
||||||
|
<span class="cov1" title="1">valout := fmt.Sprintf("%v", val)
|
||||||
|
if vals, isString := val.(string); isString </span><span class="cov1" title="1">{
|
||||||
|
valout = fmt.Sprintf(`"%s"`, vals)
|
||||||
|
}</span>
|
||||||
|
<span class="cov1" title="1">if valt, isTime := val.(time.Time); isTime </span><span class="cov0" title="0">{
|
||||||
|
valout = valt.Format(time.RFC3339)
|
||||||
|
}</span>
|
||||||
|
<span class="cov1" title="1">l = l + "\n" + fmt.Sprintf("%s = %s", k, valout)</span>
|
||||||
|
}
|
||||||
|
<span class="cov9" title="5">_, err = toml.Decode(l, &c)
|
||||||
|
return c, err</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadStdout(stdout Output) Output <span class="cov9" title="5">{
|
||||||
|
st := stdoutEnabled{stdout.Enabled}
|
||||||
|
env.Parse(&st)
|
||||||
|
stdout.Enabled = st.Enabled
|
||||||
|
var std Stdout
|
||||||
|
mapst.Decode(stdout.Config, &std)
|
||||||
|
env.Parse(&std)
|
||||||
|
mapst.Decode(std, &stdout.Config)
|
||||||
|
return stdout
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
func (oo Outputs) Stdout() (s Stdout, enabled bool) <span class="cov4" title="2">{
|
||||||
|
o, ok := oo["stdout"]
|
||||||
|
if !ok </span><span class="cov1" title="1">{
|
||||||
|
return s, false
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<span class="cov1" title="1">enabled = o.Enabled
|
||||||
|
mapst.Decode(o.Config, &s)
|
||||||
|
|
||||||
|
return</span>
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre class="file" id="file2" style="display: none">package files
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
fp "path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"codeberg.org/danjones000/my-log/config"
|
||||||
|
"codeberg.org/danjones000/my-log/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Append(l models.Log) error <span class="cov10" title="4">{
|
||||||
|
conf, err := config.Load()
|
||||||
|
if err != nil </span><span class="cov1" title="1">{
|
||||||
|
return err
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<span class="cov8" title="3">filename := fmt.Sprintf("%s.%s", strings.ReplaceAll(l.Name, ".", string(os.PathSeparator)), conf.Input.Ext)
|
||||||
|
path := fp.Join(conf.Input.Path, filename)
|
||||||
|
dir := fp.Dir(path)
|
||||||
|
err = os.MkdirAll(dir, 0750)
|
||||||
|
if err != nil </span><span class="cov1" title="1">{
|
||||||
|
return err
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<span class="cov5" title="2">f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640)
|
||||||
|
if err != nil </span><span class="cov1" title="1">{
|
||||||
|
return err
|
||||||
|
}</span>
|
||||||
|
<span class="cov1" title="1">defer f.Close()
|
||||||
|
|
||||||
|
for _, e := range l.Entries </span><span class="cov1" title="1">{
|
||||||
|
by, err := e.MarshalText()
|
||||||
|
if err != nil </span><span class="cov0" title="0">{
|
||||||
|
continue</span>
|
||||||
|
}
|
||||||
|
<span class="cov1" title="1">f.Write(by)</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="cov1" title="1">return nil</span>
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre class="file" id="file3" style="display: none">package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DateFormat = "January 02, 2006 at 03:04:05PM -0700"
|
||||||
|
|
||||||
|
type Entry struct {
|
||||||
|
Title string
|
||||||
|
Date time.Time
|
||||||
|
Fields []Meta
|
||||||
|
skipMissing bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func PartialEntry() Entry <span class="cov1" title="1">{
|
||||||
|
return Entry{skipMissing: true}
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
type metaRes struct {
|
||||||
|
out []byte
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entry) getFieldMarshalChan() chan metaRes <span class="cov3" title="4">{
|
||||||
|
size := len(e.Fields)
|
||||||
|
ch := make(chan metaRes, size)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
for i := 0; i < size; i++ </span><span class="cov4" title="5">{
|
||||||
|
wg.Add(1)
|
||||||
|
go func(m Meta) </span><span class="cov4" title="5">{
|
||||||
|
defer wg.Done()
|
||||||
|
if m.Key == "json" </span><span class="cov1" title="1">{
|
||||||
|
if j, ok := m.Value.(json.RawMessage); ok </span><span class="cov1" title="1">{
|
||||||
|
sub := Entry{skipMissing: true}
|
||||||
|
json.Unmarshal(j, &sub)
|
||||||
|
for _, subM := range sub.Fields </span><span class="cov3" title="3">{
|
||||||
|
o, er := subM.MarshalText()
|
||||||
|
ch <- metaRes{o, er}
|
||||||
|
}</span>
|
||||||
|
}
|
||||||
|
} else<span class="cov3" title="4"> {
|
||||||
|
o, er := m.MarshalText()
|
||||||
|
ch <- metaRes{o, er}
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
}(e.Fields[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="cov3" title="4">go func() </span><span class="cov3" title="4">{
|
||||||
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
}</span>()
|
||||||
|
<span class="cov3" title="4">return ch</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entry) MarshalText() ([]byte, error) <span class="cov4" title="6">{
|
||||||
|
e.Title = strings.TrimSpace(e.Title)
|
||||||
|
if e.Title == "" </span><span class="cov1" title="1">{
|
||||||
|
return []byte{}, ErrorMissingTitle
|
||||||
|
}</span>
|
||||||
|
<span class="cov4" title="5">if e.Date == (time.Time{}) </span><span class="cov1" title="1">{
|
||||||
|
return []byte{}, ErrorMissingDate
|
||||||
|
}</span>
|
||||||
|
<span class="cov3" title="4">ch := e.getFieldMarshalChan()
|
||||||
|
buff := &bytes.Buffer{}
|
||||||
|
buff.WriteString("\n@begin ")
|
||||||
|
buff.WriteString(e.Date.Format(DateFormat))
|
||||||
|
buff.WriteString(" - ")
|
||||||
|
buff.WriteString(e.Title)
|
||||||
|
|
||||||
|
for res := range ch </span><span class="cov5" title="7">{
|
||||||
|
if res.err == nil && len(res.out) > 0 </span><span class="cov5" title="7">{
|
||||||
|
buff.WriteString("\n")
|
||||||
|
buff.Write(res.out)
|
||||||
|
}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="cov3" title="4">buff.WriteString(" @end")
|
||||||
|
|
||||||
|
return buff.Bytes(), nil</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Entry) UnmarshalText(in []byte) error <span class="cov7" title="18">{
|
||||||
|
re := regexp.MustCompile("(?s)^@begin (.+) - (.+?)[ \n]@")
|
||||||
|
match := re.FindSubmatch(in)
|
||||||
|
if len(match) == 0 </span><span class="cov2" title="2">{
|
||||||
|
return newParsingError(errors.New("Failed to find title and date"))
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<span class="cov6" title="16">ch := m.getFieldUnarshalChan(in)
|
||||||
|
|
||||||
|
title := bytes.TrimSpace(match[2])
|
||||||
|
if len(title) == 0 </span><span class="cov1" title="1">{
|
||||||
|
return ErrorMissingTitle
|
||||||
|
}</span>
|
||||||
|
<span class="cov6" title="15">m.Title = string(title)
|
||||||
|
date := string(bytes.TrimSpace(match[1]))
|
||||||
|
if date == "" </span><span class="cov1" title="1">{
|
||||||
|
return ErrorMissingDate
|
||||||
|
}</span>
|
||||||
|
<span class="cov6" title="14">d, e := time.Parse(time.RFC3339, date)
|
||||||
|
if e != nil </span><span class="cov6" title="11">{
|
||||||
|
d, e = time.Parse(DateFormat, date)
|
||||||
|
if e != nil </span><span class="cov1" title="1">{
|
||||||
|
return newParsingError(e)
|
||||||
|
}</span>
|
||||||
|
}
|
||||||
|
<span class="cov6" title="13">m.Date = d
|
||||||
|
|
||||||
|
for meta := range ch </span><span class="cov6" title="11">{
|
||||||
|
m.Fields = append(m.Fields, meta)
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<span class="cov6" title="13">return nil</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanEntry(data []byte, atEOF bool) (advance int, token []byte, err error) <span class="cov10" title="65">{
|
||||||
|
if atEOF && len(data) == 0 </span><span class="cov7" title="17">{
|
||||||
|
return 0, nil, nil
|
||||||
|
}</span>
|
||||||
|
<span class="cov9" title="48">if i := bytes.Index(data, []byte{10, 64}); i > 0 </span><span class="cov6" title="14">{
|
||||||
|
return i + 1, data[0:i], nil
|
||||||
|
}</span>
|
||||||
|
<span class="cov8" title="34">if atEOF </span><span class="cov7" title="17">{
|
||||||
|
end := []byte{32, 64, 101, 110, 100}
|
||||||
|
token = data
|
||||||
|
if i := bytes.Index(data, end); i >= 0 </span><span class="cov6" title="15">{
|
||||||
|
token = data[0:i]
|
||||||
|
}</span>
|
||||||
|
<span class="cov7" title="17">return len(data), token, nil</span>
|
||||||
|
}
|
||||||
|
// Request more data.
|
||||||
|
<span class="cov7" title="17">return 0, nil, nil</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) getFieldUnarshalChan(in []byte) chan Meta <span class="cov6" title="16">{
|
||||||
|
size := len(in) / 3 // rough estimation
|
||||||
|
ch := make(chan Meta, size)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
read := bytes.NewReader(in)
|
||||||
|
scan := bufio.NewScanner(read)
|
||||||
|
scan.Split(scanEntry)
|
||||||
|
scan.Scan() // throw out first line
|
||||||
|
|
||||||
|
for scan.Scan() </span><span class="cov6" title="12">{
|
||||||
|
wg.Add(1)
|
||||||
|
go func(field []byte) </span><span class="cov6" title="12">{
|
||||||
|
defer wg.Done()
|
||||||
|
m := new(Meta)
|
||||||
|
err := m.UnmarshalText(field)
|
||||||
|
if err == nil </span><span class="cov5" title="10">{
|
||||||
|
if m.Key == "json" </span><span class="cov1" title="1">{
|
||||||
|
if j, ok := m.Value.(json.RawMessage); ok </span><span class="cov1" title="1">{
|
||||||
|
sub := Entry{skipMissing: true}
|
||||||
|
json.Unmarshal(j, &sub)
|
||||||
|
for _, subM := range sub.Fields </span><span class="cov2" title="2">{
|
||||||
|
ch <- subM
|
||||||
|
}</span>
|
||||||
|
}
|
||||||
|
} else<span class="cov5" title="9"> {
|
||||||
|
ch <- *m
|
||||||
|
}</span>
|
||||||
|
}
|
||||||
|
}(scan.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="cov6" title="16">go func() </span><span class="cov6" title="16">{
|
||||||
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
}</span>()
|
||||||
|
<span class="cov6" title="16">return ch</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Entry) MarshalJSON() ([]byte, error) <span class="cov6" title="11">{
|
||||||
|
if e.Title == "" </span><span class="cov1" title="1">{
|
||||||
|
return []byte{}, ErrorMissingTitle
|
||||||
|
}</span>
|
||||||
|
<span class="cov5" title="10">if e.Date == (time.Time{}) </span><span class="cov1" title="1">{
|
||||||
|
return []byte{}, ErrorMissingDate
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<span class="cov5" title="9">out := map[string]any{}
|
||||||
|
out["title"] = e.Title
|
||||||
|
out["date"] = e.Date.Format(time.RFC3339)
|
||||||
|
for _, f := range e.Fields </span><span class="cov5" title="10">{
|
||||||
|
if _, ok := out[f.Key]; !ok </span><span class="cov5" title="7">{
|
||||||
|
if f.Key == "json" </span><span class="cov1" title="1">{
|
||||||
|
ob := map[string]any{}
|
||||||
|
if j, ok := f.Value.(json.RawMessage); ok </span><span class="cov1" title="1">{
|
||||||
|
json.Unmarshal(j, &ob)
|
||||||
|
}</span>
|
||||||
|
// If we couldn't get valid data from there, this will just be empty
|
||||||
|
<span class="cov1" title="1">for k, v := range ob </span><span class="cov3" title="3">{
|
||||||
|
if k != "title" && k != "date" </span><span class="cov3" title="3">{
|
||||||
|
out[k] = v
|
||||||
|
}</span>
|
||||||
|
}
|
||||||
|
} else<span class="cov4" title="6"> {
|
||||||
|
out[f.Key] = f.Value
|
||||||
|
if vt, ok := f.Value.(time.Time); ok </span><span class="cov1" title="1">{
|
||||||
|
out[f.Key] = vt.Format(time.RFC3339)
|
||||||
|
}</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
<span class="cov5" title="9">return json.Marshal(out)</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) unmarshalJsonChanHelper(m map[string]any, ch chan Meta, wg *sync.WaitGroup) <span class="cov6" title="11">{
|
||||||
|
for k, v := range m </span><span class="cov8" title="32">{
|
||||||
|
wg.Add(1)
|
||||||
|
go func(key string, value any) </span><span class="cov8" title="32">{
|
||||||
|
defer wg.Done()
|
||||||
|
if key != "json" </span><span class="cov8" title="30">{
|
||||||
|
ch <- Meta{key, value}
|
||||||
|
return
|
||||||
|
}</span>
|
||||||
|
<span class="cov2" title="2">subM := map[string]any{}
|
||||||
|
if s, ok := value.(string); ok </span><span class="cov1" title="1">{
|
||||||
|
dec := json.NewDecoder(strings.NewReader(s))
|
||||||
|
dec.UseNumber()
|
||||||
|
dec.Decode(&subM)
|
||||||
|
}</span> else<span class="cov1" title="1"> {
|
||||||
|
subM = value.(map[string]any)
|
||||||
|
}</span>
|
||||||
|
<span class="cov2" title="2">e.unmarshalJsonChanHelper(subM, ch, wg)</span>
|
||||||
|
}(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) getUnmarshalJsonChan(m map[string]any) chan Meta <span class="cov5" title="9">{
|
||||||
|
ch := make(chan Meta, len(m))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
e.unmarshalJsonChanHelper(m, ch, &wg)
|
||||||
|
go func() </span><span class="cov5" title="9">{
|
||||||
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
}</span>()
|
||||||
|
|
||||||
|
<span class="cov5" title="9">return ch</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Entry) UnmarshalJSON(in []byte) error <span class="cov6" title="15">{
|
||||||
|
out := map[string]any{}
|
||||||
|
dec := json.NewDecoder(bytes.NewReader(in))
|
||||||
|
dec.UseNumber()
|
||||||
|
err := dec.Decode(&out)
|
||||||
|
if err != nil </span><span class="cov1" title="1">{
|
||||||
|
return newParsingError(err)
|
||||||
|
}</span>
|
||||||
|
<span class="cov6" title="14">title, ok := out["title"].(string)
|
||||||
|
if (!ok || title == "") && !e.skipMissing </span><span class="cov2" title="2">{
|
||||||
|
return ErrorMissingTitle
|
||||||
|
}</span>
|
||||||
|
<span class="cov6" title="12">e.Title = title
|
||||||
|
dates, ok := out["date"].(string)
|
||||||
|
if (!ok || dates == "") && !e.skipMissing </span><span class="cov2" title="2">{
|
||||||
|
return ErrorMissingDate
|
||||||
|
}</span>
|
||||||
|
<span class="cov5" title="10">date, err := time.Parse(time.RFC3339, dates)
|
||||||
|
if err != nil && !e.skipMissing </span><span class="cov1" title="1">{
|
||||||
|
return newParsingError(err)
|
||||||
|
}</span>
|
||||||
|
<span class="cov5" title="9">e.Date = date
|
||||||
|
ch := e.getUnmarshalJsonChan(out)
|
||||||
|
for m := range ch </span><span class="cov8" title="30">{
|
||||||
|
if m.Key == "title" || m.Key == "date" </span><span class="cov6" title="12">{
|
||||||
|
continue</span>
|
||||||
|
} else<span class="cov7" title="18"> if vs, ok := m.Value.(string); ok </span><span class="cov5" title="7">{
|
||||||
|
if vd, err := time.Parse(time.RFC3339, vs); err == nil </span><span class="cov1" title="1">{
|
||||||
|
m.Value = vd
|
||||||
|
}</span> else<span class="cov4" title="6"> {
|
||||||
|
m.Value = vs
|
||||||
|
}</span>
|
||||||
|
} else<span class="cov6" title="11"> if n, ok := m.Value.(json.Number); ok </span><span class="cov4" title="6">{
|
||||||
|
it, _ := n.Int64()
|
||||||
|
fl, _ := n.Float64()
|
||||||
|
if float64(it) == fl </span><span class="cov4" title="5">{
|
||||||
|
m.Value = it
|
||||||
|
}</span> else<span class="cov1" title="1"> {
|
||||||
|
m.Value = fl
|
||||||
|
}</span>
|
||||||
|
}
|
||||||
|
<span class="cov7" title="18">e.Fields = append(e.Fields, m)</span>
|
||||||
|
}
|
||||||
|
<span class="cov5" title="9">return nil</span>
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre class="file" id="file4" style="display: none">package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrorMissingTitle = errors.New("Missing title")
|
||||||
|
var ErrorMissingDate = errors.New("Missing date")
|
||||||
|
var ErrorParsing = errors.New("Parsing Error")
|
||||||
|
|
||||||
|
func newParsingError(err error) error <span class="cov10" title="13">{
|
||||||
|
return fmt.Errorf("%w: %w", ErrorParsing, err)
|
||||||
|
}</span>
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre class="file" id="file5" style="display: none">package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"regexp"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var reg = regexp.MustCompile("(?sm)^@begin .+?(^| )@end")
|
||||||
|
|
||||||
|
type Log struct {
|
||||||
|
Name string
|
||||||
|
Entries []Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Log) UnmarshalText(in []byte) error <span class="cov5" title="4">{
|
||||||
|
ch := l.getLogUnarshalChan(in)
|
||||||
|
for entry := range ch </span><span class="cov7" title="6">{
|
||||||
|
l.Entries = append(l.Entries, entry)
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<span class="cov5" title="4">return nil</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanLog(data []byte, atEOF bool) (advance int, token []byte, err error) <span class="cov10" title="14">{
|
||||||
|
if atEOF && len(data) == 0 </span><span class="cov1" title="1">{
|
||||||
|
// done
|
||||||
|
return 0, nil, nil
|
||||||
|
}</span>
|
||||||
|
<span class="cov9" title="13">m := reg.FindIndex(data)
|
||||||
|
if len(m) == 0 && atEOF </span><span class="cov4" title="3">{
|
||||||
|
// all trash
|
||||||
|
return len(data), nil, nil
|
||||||
|
}</span> else<span class="cov8" title="10"> if len(m) == 0 && !atEOF </span><span class="cov4" title="3">{
|
||||||
|
// get more
|
||||||
|
return 0, nil, nil
|
||||||
|
}</span>
|
||||||
|
<span class="cov7" title="7">return m[1], data[m[0]:m[1]], nil</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Log) getLogUnarshalChan(in []byte) chan Entry <span class="cov5" title="4">{
|
||||||
|
size := len(in) / 10 // rough estimation
|
||||||
|
ch := make(chan Entry, size)
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
|
||||||
|
read := bytes.NewReader(in)
|
||||||
|
scan := bufio.NewScanner(read)
|
||||||
|
scan.Split(scanLog)
|
||||||
|
|
||||||
|
for scan.Scan() </span><span class="cov7" title="7">{
|
||||||
|
wg.Add(1)
|
||||||
|
go func(field []byte) </span><span class="cov7" title="7">{
|
||||||
|
defer wg.Done()
|
||||||
|
f := new(Entry)
|
||||||
|
err := f.UnmarshalText(field)
|
||||||
|
if err != nil </span><span class="cov1" title="1">{
|
||||||
|
return
|
||||||
|
}</span>
|
||||||
|
<span class="cov7" title="6">ch <- *f</span>
|
||||||
|
}(scan.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="cov5" title="4">go func() </span><span class="cov5" title="4">{
|
||||||
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
}</span>()
|
||||||
|
<span class="cov5" title="4">return ch</span>
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre class="file" id="file6" style="display: none">package models
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"codeberg.org/danjones000/my-log/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Meta struct {
|
||||||
|
Key string
|
||||||
|
Value any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Meta) MarshalText() ([]byte, error) <span class="cov8" title="23">{
|
||||||
|
if regexp.MustCompile(`\s`).MatchString(m.Key) </span><span class="cov1" title="1">{
|
||||||
|
return []byte{}, fmt.Errorf("whitespace is not allowed in key: %s", m.Key)
|
||||||
|
}</span>
|
||||||
|
<span class="cov8" title="22">buff := &bytes.Buffer{}
|
||||||
|
buff.WriteRune('@')
|
||||||
|
buff.WriteString(m.Key)
|
||||||
|
buff.WriteRune(' ')
|
||||||
|
switch v := m.Value.(type) </span>{
|
||||||
|
default:<span class="cov1" title="1">
|
||||||
|
return nil, fmt.Errorf("Unknown type %T", v)</span>
|
||||||
|
case nil:<span class="cov1" title="1">
|
||||||
|
return []byte{}, nil</span>
|
||||||
|
case string:<span class="cov3" title="3">
|
||||||
|
buff.WriteString(v)</span>
|
||||||
|
case int:<span class="cov3" title="3">
|
||||||
|
buff.WriteString(strconv.Itoa(v))</span>
|
||||||
|
case int64:<span class="cov2" title="2">
|
||||||
|
buff.WriteString(strconv.FormatInt(v, 10))</span>
|
||||||
|
case float64:<span class="cov1" title="1">
|
||||||
|
buff.WriteString(strconv.FormatFloat(v, 'f', -1, 64))</span>
|
||||||
|
case json.Number:<span class="cov1" title="1">
|
||||||
|
buff.WriteString(v.String())</span>
|
||||||
|
case json.RawMessage:<span class="cov2" title="2">
|
||||||
|
buff.Write(v)</span>
|
||||||
|
case []byte:<span class="cov1" title="1">
|
||||||
|
buff.Write(v)</span>
|
||||||
|
case byte:<span class="cov1" title="1">
|
||||||
|
buff.WriteByte(v)</span>
|
||||||
|
case rune:<span class="cov1" title="1">
|
||||||
|
buff.WriteString(string(v))</span>
|
||||||
|
case bool:<span class="cov4" title="4">
|
||||||
|
buff.WriteString(strconv.FormatBool(v))</span>
|
||||||
|
case time.Time:<span class="cov1" title="1">
|
||||||
|
buff.WriteString(v.Format(time.RFC3339))</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
<span class="cov8" title="20">return buff.Bytes(), nil</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Meta) UnmarshalText(in []byte) error <span class="cov10" title="39">{
|
||||||
|
if len(in) == 0 </span><span class="cov2" title="2">{
|
||||||
|
return newParsingError(errors.New("Unable to Unmarshal empty string"))
|
||||||
|
}</span>
|
||||||
|
<span class="cov9" title="37">re := regexp.MustCompile("(?s)^@([^ ]+) (.*)( @end)?$")
|
||||||
|
match := re.FindSubmatch(in)
|
||||||
|
if len(match) == 0 </span><span class="cov3" title="3">{
|
||||||
|
return newParsingError(fmt.Errorf("Failed to match %s", in))
|
||||||
|
}</span>
|
||||||
|
<span class="cov9" title="34">m.Key = string(match[1])
|
||||||
|
return m.processMeta(match[2])</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Meta) processMeta(in []byte) error <span class="cov9" title="34">{
|
||||||
|
if len(in) == 0 </span><span class="cov1" title="1">{
|
||||||
|
return newParsingError(errors.New("No value found"))
|
||||||
|
}</span>
|
||||||
|
<span class="cov9" title="33">v := tools.ParseBytes(in)
|
||||||
|
if v == "" </span><span class="cov2" title="2">{
|
||||||
|
return newParsingError(errors.New("No value found"))
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<span class="cov9" title="31">m.Value = v
|
||||||
|
return nil</span>
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre class="file" id="file7" style="display: none">package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseBytes(in []byte) any <span class="cov8" title="20">{
|
||||||
|
return ParseString(string(in))
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
func ParseString(in string) any <span class="cov10" title="40">{
|
||||||
|
s := strings.TrimSpace(in)
|
||||||
|
if s == "" </span><span class="cov5" title="6">{
|
||||||
|
return s
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<span class="cov9" title="34">yesno := regexp.MustCompile("^(y|Y|yes|Yes|YES|n|N|no|No|NO|true|True|TRUE|false|False|FALSE|on|On|ON|off|Off|OFF)$")
|
||||||
|
yes := regexp.MustCompile("^(y|Y|yes|Yes|YES|true|True|TRUE|on|On|ON)$")
|
||||||
|
null := regexp.MustCompile("^(~|null|Null|NULL|none|None|NONE|nil|Nil|NIL)$")
|
||||||
|
var j json.RawMessage
|
||||||
|
if null.MatchString(s) </span><span class="cov6" title="10">{
|
||||||
|
return nil
|
||||||
|
}</span> else<span class="cov8" title="24"> if yesno.MatchString(s) </span><span class="cov7" title="12">{
|
||||||
|
if yes.MatchString(s) </span><span class="cov5" title="6">{
|
||||||
|
return true
|
||||||
|
}</span> else<span class="cov5" title="6"> {
|
||||||
|
return false
|
||||||
|
}</span>
|
||||||
|
} else<span class="cov7" title="12"> if i, err := strconv.Atoi(s); err == nil </span><span class="cov2" title="2">{
|
||||||
|
return i
|
||||||
|
}</span> else<span class="cov6" title="10"> if f, err := strconv.ParseFloat(s, 64); err == nil </span><span class="cov2" title="2">{
|
||||||
|
return f
|
||||||
|
}</span> else<span class="cov6" title="8"> if t, err := time.Parse(time.RFC3339, s); err == nil </span><span class="cov2" title="2">{
|
||||||
|
return t
|
||||||
|
}</span> else<span class="cov5" title="6"> if err := json.Unmarshal([]byte(s), &j); err == nil </span><span class="cov4" title="4">{
|
||||||
|
return j
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<span class="cov2" title="2">return s</span>
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<pre class="file" id="file8" style="display: none">package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
dp "github.com/markusmobius/go-dateparser"
|
||||||
|
"github.com/markusmobius/go-dateparser/date"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
day = time.Hour * 24
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are somewhat arbitrary, but reasonably useful min and max times
|
||||||
|
var (
|
||||||
|
MinTime = time.Unix(-2208988800, 0) // Jan 1, 1900
|
||||||
|
MaxTime = MinTime.Add(1<<63 - 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseDate(in string) (t time.Time, err error) <span class="cov10" title="7">{
|
||||||
|
if in == "min" </span><span class="cov1" title="1">{
|
||||||
|
return MinTime, nil
|
||||||
|
}</span>
|
||||||
|
<span class="cov9" title="6">if in == "max" </span><span class="cov1" title="1">{
|
||||||
|
return MaxTime, nil
|
||||||
|
}</span>
|
||||||
|
|
||||||
|
<span class="cov8" title="5">d, err := dp.Parse(nil, in)
|
||||||
|
if err != nil </span><span class="cov1" title="1">{
|
||||||
|
return
|
||||||
|
}</span>
|
||||||
|
<span class="cov7" title="4">t = d.Time.Local()
|
||||||
|
trunc := time.Second
|
||||||
|
switch d.Period </span>{
|
||||||
|
case date.Minute:<span class="cov0" title="0">
|
||||||
|
trunc = time.Minute</span>
|
||||||
|
case date.Hour:<span class="cov0" title="0">
|
||||||
|
trunc = time.Hour</span>
|
||||||
|
case date.Day:<span class="cov6" title="3">
|
||||||
|
trunc = day</span>
|
||||||
|
// @todo Handle other cases separately
|
||||||
|
}
|
||||||
|
<span class="cov7" title="4">t = t.Truncate(trunc)
|
||||||
|
return</span>
|
||||||
|
}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var files = document.getElementById('files');
|
||||||
|
var visible;
|
||||||
|
files.addEventListener('change', onChange, false);
|
||||||
|
function select(part) {
|
||||||
|
if (visible)
|
||||||
|
visible.style.display = 'none';
|
||||||
|
visible = document.getElementById(part);
|
||||||
|
if (!visible)
|
||||||
|
return;
|
||||||
|
files.value = part;
|
||||||
|
visible.style.display = 'block';
|
||||||
|
location.hash = part;
|
||||||
|
}
|
||||||
|
function onChange() {
|
||||||
|
select(files.value);
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
}
|
||||||
|
if (location.hash != "") {
|
||||||
|
select(location.hash.substr(1));
|
||||||
|
}
|
||||||
|
if (!visible) {
|
||||||
|
select("file0");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</html>
|
||||||
12
go.mod
12
go.mod
|
|
@ -5,6 +5,7 @@ go 1.21.5
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.3.2
|
github.com/BurntSushi/toml v1.3.2
|
||||||
github.com/caarlos0/env/v10 v10.0.0
|
github.com/caarlos0/env/v10 v10.0.0
|
||||||
|
github.com/markusmobius/go-dateparser v1.2.1
|
||||||
github.com/mitchellh/mapstructure v1.5.0
|
github.com/mitchellh/mapstructure v1.5.0
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
github.com/stretchr/testify v1.8.4
|
github.com/stretchr/testify v1.8.4
|
||||||
|
|
@ -12,10 +13,21 @@ require (
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/elliotchance/pie/v2 v2.7.0 // indirect
|
||||||
|
github.com/hablullah/go-hijri v1.0.2 // indirect
|
||||||
|
github.com/hablullah/go-juliandays v1.0.0 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 // indirect
|
||||||
github.com/kr/pretty v0.3.1 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
|
github.com/magefile/mage v1.14.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
github.com/spf13/pflag v1.0.5 // indirect
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/tetratelabs/wazero v1.2.1 // indirect
|
||||||
|
github.com/wasilibs/go-re2 v1.3.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705 // indirect
|
||||||
|
golang.org/x/text v0.10.0 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
|
replace github.com/markusmobius/go-dateparser => github.com/goodevilgenius/go-dateparser v1.2.2
|
||||||
|
|
|
||||||
22
go.sum
22
go.sum
|
|
@ -6,12 +6,24 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
|
||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/elliotchance/pie/v2 v2.7.0 h1:FqoIKg4uj0G/CrLGuMS9ejnFKa92lxE1dEgBD3pShXg=
|
||||||
|
github.com/elliotchance/pie/v2 v2.7.0/go.mod h1:18t0dgGFH006g4eVdDtWfgFZPQEgl10IoEO8YWEq3Og=
|
||||||
|
github.com/goodevilgenius/go-dateparser v1.2.2 h1:Up9KokPx/h07mesQGAZQg3Xi/8yrDVn1638h3k/lRyk=
|
||||||
|
github.com/goodevilgenius/go-dateparser v1.2.2/go.mod h1:5xYsZ1h7iB3sE1BSu8bkjYpbFST7EU1/AFxcyO3mgYg=
|
||||||
|
github.com/hablullah/go-hijri v1.0.2 h1:drT/MZpSZJQXo7jftf5fthArShcaMtsal0Zf/dnmp6k=
|
||||||
|
github.com/hablullah/go-hijri v1.0.2/go.mod h1:OS5qyYLDjORXzK4O1adFw9Q5WfhOcMdAKglDkcTxgWQ=
|
||||||
|
github.com/hablullah/go-juliandays v1.0.0 h1:A8YM7wIj16SzlKT0SRJc9CD29iiaUzpBLzh5hr0/5p0=
|
||||||
|
github.com/hablullah/go-juliandays v1.0.0/go.mod h1:0JOYq4oFOuDja+oospuc61YoX+uNEn7Z6uHYTbBzdGc=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||||
|
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958 h1:qxLoi6CAcXVzjfvu+KXIXJOAsQB62LXjsfbOaErsVzE=
|
||||||
|
github.com/jalaali/go-jalaali v0.0.0-20210801064154-80525e88d958/go.mod h1:Wqfu7mjUHj9WDzSSPI5KfBclTTEnLveRUFr/ujWnTgE=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||||
|
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
|
@ -26,6 +38,16 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs=
|
||||||
|
github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ=
|
||||||
|
github.com/wasilibs/go-re2 v1.3.0 h1:LFhBNzoStM3wMie6rN2slD1cuYH2CGiHpvNL3UtcsMw=
|
||||||
|
github.com/wasilibs/go-re2 v1.3.0/go.mod h1:AafrCXVvGRJJOImMajgJ2M7rVmWyisVK7sFshbxnVrg=
|
||||||
|
github.com/wasilibs/nottinygc v0.4.0 h1:h1TJMihMC4neN6Zq+WKpLxgd9xCFMw7O9ETLwY2exJQ=
|
||||||
|
github.com/wasilibs/nottinygc v0.4.0/go.mod h1:oDcIotskuYNMpqMF23l7Z8uzD4TC0WXHK8jetlB3HIo=
|
||||||
|
golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705 h1:ba9YlqfDGTTQ5aZ2fwOoQ1hf32QySyQkR6ODGDzHlnE=
|
||||||
|
golang.org/x/exp v0.0.0-20220321173239-a90fa8a75705/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
|
||||||
|
golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
|
||||||
|
golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
|
|
||||||
45
tools/parse_date.go
Normal file
45
tools/parse_date.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
dp "github.com/markusmobius/go-dateparser"
|
||||||
|
"github.com/markusmobius/go-dateparser/date"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
day = time.Hour * 24
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are somewhat arbitrary, but reasonably useful min and max times
|
||||||
|
var (
|
||||||
|
MinTime = time.Unix(-2208988800, 0) // Jan 1, 1900
|
||||||
|
MaxTime = MinTime.Add(1<<63 - 1)
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseDate(in string) (t time.Time, err error) {
|
||||||
|
if in == "min" {
|
||||||
|
return MinTime, nil
|
||||||
|
}
|
||||||
|
if in == "max" {
|
||||||
|
return MaxTime, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d, err := dp.Parse(nil, in)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t = d.Time.Local()
|
||||||
|
trunc := time.Second
|
||||||
|
switch d.Period {
|
||||||
|
case date.Minute:
|
||||||
|
trunc = time.Minute
|
||||||
|
case date.Hour:
|
||||||
|
trunc = time.Hour
|
||||||
|
case date.Day:
|
||||||
|
trunc = day
|
||||||
|
// @todo Handle other cases separately
|
||||||
|
}
|
||||||
|
t = t.Truncate(trunc)
|
||||||
|
return
|
||||||
|
}
|
||||||
51
tools/parse_date_test.go
Normal file
51
tools/parse_date_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package tools
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestParseDate(t *testing.T) {
|
||||||
|
now := time.Now().Local()
|
||||||
|
sec := now.Truncate(time.Second)
|
||||||
|
today := now.Truncate(day)
|
||||||
|
tomorrow := today.Add(day)
|
||||||
|
yesterday := today.Add(-day)
|
||||||
|
twoMin := now.Add(2 * time.Minute).Truncate(time.Minute)
|
||||||
|
twoHour := now.Add(2 * time.Hour).Truncate(time.Hour)
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
exp time.Time
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{"now", sec, ""},
|
||||||
|
{"today", today, ""},
|
||||||
|
{"tomorrow", tomorrow, ""},
|
||||||
|
{"yesterday", yesterday, ""},
|
||||||
|
{"in two minutes", twoMin, ""},
|
||||||
|
{"in two hours", twoHour, ""},
|
||||||
|
{"min", MinTime, ""},
|
||||||
|
{"max", MaxTime, ""},
|
||||||
|
{"not a date", now, fmt.Sprintf(`failed to parse "%s": unknown format`, "not a date")},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, getDateTest(tt.name, tt.exp, tt.err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDateTest(in string, exp time.Time, err string) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
out, er := ParseDate(in)
|
||||||
|
if err != "" {
|
||||||
|
assert.ErrorContains(t, er, err)
|
||||||
|
} else {
|
||||||
|
require.NoError(t, er)
|
||||||
|
|
||||||
|
assert.Equal(t, exp, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue