Compare commits
6 Commits
b6c6e83443
...
9a0eb89f54
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a0eb89f54 | ||
|
|
fe8d80ffc8 | ||
|
|
f41386b3b4 | ||
|
|
2141e030ef | ||
|
|
ee6ce95c0a | ||
|
|
6d174b031b |
@@ -1,32 +1,16 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
|
|
||||||
_ "embed"
|
_ "embed"
|
||||||
|
|
||||||
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
|
"gogs.inhome.blapointe.com/ana-ledger/src/ledger"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed public/*
|
|
||||||
var _staticFileDir embed.FS
|
|
||||||
var publicHandler = func() http.Handler {
|
|
||||||
if os.Getenv("DEBUG") != "" {
|
|
||||||
return http.FileServer(http.Dir("./http/public"))
|
|
||||||
}
|
|
||||||
sub, err := fs.Sub(_staticFileDir, "public")
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return http.FileServer(http.FS(sub))
|
|
||||||
}()
|
|
||||||
|
|
||||||
func Main() {
|
func Main() {
|
||||||
likeName := flag.String("like-name", ".", "regexp to match")
|
likeName := flag.String("like-name", ".", "regexp to match")
|
||||||
likeBefore := flag.String("like-before", "9", "date str to compare")
|
likeBefore := flag.String("like-before", "9", "date str to compare")
|
||||||
|
|||||||
@@ -35,12 +35,13 @@
|
|||||||
function load() {
|
function load() {
|
||||||
http("GET", "/api/transactions" /*?f="+f*/, (body, status) => {
|
http("GET", "/api/transactions" /*?f="+f*/, (body, status) => {
|
||||||
var d = JSON.parse(body)
|
var d = JSON.parse(body)
|
||||||
|
console.log("loading", d)
|
||||||
loadBalances(d.balances)
|
loadBalances(d.balances)
|
||||||
loadDeltas(d.deltas)
|
loadDeltas(d.deltas)
|
||||||
loadLastNLines(d.lastNLines)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
function loadBalances(balances) {
|
function loadBalances(balances) {
|
||||||
|
console.log("loading balances", balances)
|
||||||
var result = `<table>`
|
var result = `<table>`
|
||||||
for (var k in balances) {
|
for (var k in balances) {
|
||||||
result += `<tr style="display: flex; flex-direction: row; width: 100%; justify-content: space-between;"><td>${k}</td><td>${Math.floor(balances[k]["$"])}</td></tr>`
|
result += `<tr style="display: flex; flex-direction: row; width: 100%; justify-content: space-between;"><td>${k}</td><td>${Math.floor(balances[k]["$"])}</td></tr>`
|
||||||
@@ -69,26 +70,6 @@
|
|||||||
result += `</table>`
|
result += `</table>`
|
||||||
document.getElementById("reg").innerHTML = result
|
document.getElementById("reg").innerHTML = result
|
||||||
}
|
}
|
||||||
function setLastNLines(form) {
|
|
||||||
http("PUT", "/api/lastnlines" /*?f="+f*/, (body, status) => {
|
|
||||||
if (status == 205) {
|
|
||||||
init()
|
|
||||||
}
|
|
||||||
document.getElementById("lastNLinesStatus").innerHTML = `(${status}) ${body}`
|
|
||||||
}, JSON.stringify(form.elements["lastNLines"].value.split("\n")))
|
|
||||||
}
|
|
||||||
function loadLastNLines(lastNLines) {
|
|
||||||
var result = `<form onsubmit="setLastNLines(this); return false;" action="#">`
|
|
||||||
//result += ` <div>${f}</div>`
|
|
||||||
result += ` <textarea id="lastNLinesTextarea" name="lastNLines" style="height: 30em;">`
|
|
||||||
for (var k in lastNLines) {
|
|
||||||
result += lastNLines[k] + "\n"
|
|
||||||
}
|
|
||||||
result += ` </textarea>`
|
|
||||||
result += ` <input type="submit">`
|
|
||||||
result += `</form>`
|
|
||||||
document.getElementById("lastNLines").innerHTML = result
|
|
||||||
}
|
|
||||||
function stage(who, contributesToHouse) {
|
function stage(who, contributesToHouse) {
|
||||||
var d = new Date()
|
var d = new Date()
|
||||||
const zeroPad = (num, places) => String(num).padStart(places, '0')
|
const zeroPad = (num, places) => String(num).padStart(places, '0')
|
||||||
@@ -106,9 +87,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`@${today} ${benefactor} gave to ${beneficiary}`)
|
console.log(`@${today} ${benefactor} gave to ${beneficiary}`)
|
||||||
document.getElementById("lastNLinesTextarea").value += `\n${today} TODO moolah2`
|
|
||||||
document.getElementById("lastNLinesTextarea").value += `\n ${beneficiary} ${beneficiary_value}`
|
|
||||||
document.getElementById("lastNLinesTextarea").value += `\n ${benefactor} ${benefactor_value}`
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
</header>
|
</header>
|
||||||
@@ -135,10 +113,6 @@
|
|||||||
<input type="button" onclick="stage('AssetAccount:Bel', false)" value="Stage Bel's Charge"/>
|
<input type="button" onclick="stage('AssetAccount:Bel', false)" value="Stage Bel's Charge"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="lastNLinesStatus">
|
|
||||||
</div>
|
|
||||||
<div id="lastNLines">
|
|
||||||
</div>
|
|
||||||
</details>
|
</details>
|
||||||
<details>
|
<details>
|
||||||
<summary>Register</summary>
|
<summary>Register</summary>
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -15,6 +17,9 @@ import (
|
|||||||
"gogs.inhome.blapointe.com/ana-ledger/src/view"
|
"gogs.inhome.blapointe.com/ana-ledger/src/view"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
//go:embed public/*
|
||||||
|
var _staticFileDir embed.FS
|
||||||
|
|
||||||
type Router struct {
|
type Router struct {
|
||||||
files ledger.Files
|
files ledger.Files
|
||||||
like struct {
|
like struct {
|
||||||
@@ -42,58 +47,72 @@ func NewRouter(files ledger.Files, likeName, likeBefore, likeAfter string, group
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (router Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
func (router Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch strings.Split(r.URL.Path, "/")[1] {
|
||||||
|
case "api":
|
||||||
|
router.API(w, r)
|
||||||
|
default:
|
||||||
|
router.FS(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router Router) FS(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if os.Getenv("DEBUG") != "" {
|
||||||
|
http.FileServer(http.Dir("./http/public")).ServeHTTP(w, r)
|
||||||
|
} else {
|
||||||
|
sub, err := fs.Sub(_staticFileDir, "public")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
http.FileServer(http.FS(sub)).ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router Router) API(w http.ResponseWriter, r *http.Request) {
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/transactions":
|
||||||
|
router.APITransactions(w, r)
|
||||||
|
case "/api/amend":
|
||||||
|
router.APIAmend(w, r)
|
||||||
|
case "/api/reg":
|
||||||
|
router.APIReg(w, r)
|
||||||
|
default:
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router Router) APITransactions(w http.ResponseWriter, r *http.Request) {
|
||||||
bpis, err := router.bpis()
|
bpis, err := router.bpis()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasPrefix(r.URL.Path, "/api") {
|
deltas, err := router.files.Deltas()
|
||||||
publicHandler.ServeHTTP(w, r)
|
if err != nil {
|
||||||
return
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
reqF := router.files
|
sixMonths := time.Hour * 24 * 365 / 2
|
||||||
if queryF := r.URL.Query().Get("f"); queryF != "" {
|
|
||||||
queryF = path.Join("http", queryF)
|
|
||||||
reqF, err = ledger.NewFiles(queryF)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.URL.Path {
|
deltasForHouse := deltas.Like(ledger.LikeName(`^House`))
|
||||||
case "/api/transactions":
|
houseRelatedDeltas := deltas.Like(ledger.LikeTransactions(deltasForHouse...))
|
||||||
lastNLines, err := reqF.TempGetLastNLines(20)
|
houseRelatedBalances := houseRelatedDeltas.Balances().
|
||||||
if err != nil {
|
Group(`^[^:]*`).
|
||||||
panic(err)
|
WithBPIs(bpis).
|
||||||
}
|
NotLike(`^Withdrawal`)
|
||||||
deltas, err := reqF.Deltas()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
json.NewEncoder(w).Encode(map[string]any{
|
|
||||||
"deltas": deltas.Like(ledger.LikeAfter(time.Now().Add(-1 * time.Hour * 24 * 365 / 2).Format("2006-01"))),
|
|
||||||
"balances": deltas.Balances().Like("^AssetAccount:").WithBPIs(bpis),
|
|
||||||
"lastNLines": lastNLines,
|
|
||||||
})
|
|
||||||
return
|
|
||||||
case "/api/lastnlines":
|
|
||||||
if r.Method != http.MethodPut {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var lines []string
|
|
||||||
if err := json.NewDecoder(r.Body).Decode(&lines); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if err := reqF.TempSetLastNLines(20, lines); err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
w.WriteHeader(http.StatusResetContent)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
deltas, err := reqF.Deltas()
|
json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"deltas": deltasForHouse.
|
||||||
|
Like(ledger.LikeAfter(time.Now().Add(-1 * sixMonths / 3).Format("2006-01"))),
|
||||||
|
"balances": houseRelatedBalances,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router Router) APIAmend(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (router Router) APIReg(w http.ResponseWriter, r *http.Request) {
|
||||||
|
deltas, err := router.files.Deltas()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@@ -178,6 +197,10 @@ func (router Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
bpis, err := router.bpis()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
bpis, err = ana.BPIsWithFixedGrowthPrediction(bpis, window, currency, rate)
|
bpis, err = ana.BPIsWithFixedGrowthPrediction(bpis, window, currency, rate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -186,6 +209,10 @@ func (router Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if r.URL.Query().Get("bpi") == "true" {
|
if r.URL.Query().Get("bpi") == "true" {
|
||||||
|
bpis, err := router.bpis()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
register = register.WithBPIs(bpis)
|
register = register.WithBPIs(bpis)
|
||||||
predicted = predicted.WithBPIs(bpis)
|
predicted = predicted.WithBPIs(bpis)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,17 @@ type Balances map[string]Balance
|
|||||||
|
|
||||||
type Balance map[Currency]float64
|
type Balance map[Currency]float64
|
||||||
|
|
||||||
|
func (balances Balances) NotLike(pattern string) Balances {
|
||||||
|
result := make(Balances)
|
||||||
|
p := regexp.MustCompile(pattern)
|
||||||
|
for k, v := range balances {
|
||||||
|
if !p.MatchString(k) {
|
||||||
|
result[k] = maps.Clone(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (balances Balances) Like(pattern string) Balances {
|
func (balances Balances) Like(pattern string) Balances {
|
||||||
result := make(Balances)
|
result := make(Balances)
|
||||||
p := regexp.MustCompile(pattern)
|
p := regexp.MustCompile(pattern)
|
||||||
@@ -22,6 +33,29 @@ func (balances Balances) Like(pattern string) Balances {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (balances Balances) Group(pattern string) Balances {
|
||||||
|
result := make(Balances)
|
||||||
|
p := regexp.MustCompile(pattern)
|
||||||
|
for k, v := range balances {
|
||||||
|
k2 := p.FindString(k)
|
||||||
|
if k2 == "" {
|
||||||
|
k2 = k
|
||||||
|
}
|
||||||
|
|
||||||
|
was := result[k2]
|
||||||
|
if was == nil {
|
||||||
|
was = make(Balance)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k3, v3 := range v {
|
||||||
|
was[k3] += v3
|
||||||
|
}
|
||||||
|
|
||||||
|
result[k2] = was
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
func (balances Balances) WithBPIs(bpis BPIs) Balances {
|
func (balances Balances) WithBPIs(bpis BPIs) Balances {
|
||||||
return balances.WithBPIsAt(bpis, "9")
|
return balances.WithBPIsAt(bpis, "9")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,4 +51,38 @@ func TestBalances(t *testing.T) {
|
|||||||
t.Error("didnt sum other", b["ab"])
|
t.Error("didnt sum other", b["ab"])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("like", func(t *testing.T) {
|
||||||
|
was := make(Balances)
|
||||||
|
was.Push(Delta{Name: "a", Currency: USD, Value: 0.1})
|
||||||
|
was.Push(Delta{Name: "ab", Currency: USD, Value: 1.2})
|
||||||
|
|
||||||
|
got := was.Like(`^ab$`)
|
||||||
|
if len(got) != 1 || got["ab"][USD] != 1.2 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
|
||||||
|
got = was.NotLike(`^ab$`)
|
||||||
|
if len(got) != 1 || got["a"][USD] != 0.1 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("group", func(t *testing.T) {
|
||||||
|
was := make(Balances)
|
||||||
|
was.Push(Delta{Name: "a:1", Currency: USD, Value: 0.1})
|
||||||
|
was.Push(Delta{Name: "a:2", Currency: USD, Value: 1.2})
|
||||||
|
was.Push(Delta{Name: "b:1", Currency: USD, Value: 2.2})
|
||||||
|
|
||||||
|
got := was.Group(`^[^:]*`)
|
||||||
|
if len(got) != 2 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
if got["a"][USD] != 1.3 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
if got["b"][USD] != 2.2 {
|
||||||
|
t.Error(got)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,13 @@
|
|||||||
package ledger
|
package ledger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
|
||||||
"unicode"
|
"unicode"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,89 +61,6 @@ func (files Files) Amend(old, now Delta) error {
|
|||||||
return files.Add(transaction.payee, []Delta{old, now}...)
|
return files.Add(transaction.payee, []Delta{old, now}...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (files Files) TempGetLastNLines(n int) ([]string, error) {
|
|
||||||
p := files.paths()[0]
|
|
||||||
f, err := os.Open(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
return peekLastNLines(io.Discard, bufio.NewReader(f), n)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (files Files) TempSetLastNLines(n int, lines []string) error {
|
|
||||||
p := files.paths()[0]
|
|
||||||
|
|
||||||
newFile, err := func() (string, error) {
|
|
||||||
w, err := ioutil.TempFile(os.TempDir(), path.Base(p))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer w.Close()
|
|
||||||
|
|
||||||
r, err := os.Open(p)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
if _, err := peekLastNLines(w, bufio.NewReader(r), n); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
for i := range lines {
|
|
||||||
if len(strings.TrimSpace(lines[i])) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, err := fmt.Fprintln(w, lines[i]); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := w.Close(); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return w.Name(), nil
|
|
||||||
}()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := os.Open(newFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer r.Close()
|
|
||||||
|
|
||||||
w, err := os.Create(p)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer w.Close()
|
|
||||||
|
|
||||||
_, err = io.Copy(w, r)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func peekLastNLines(w io.Writer, r *bufio.Reader, n int) ([]string, error) {
|
|
||||||
lastNLines := make([]string, 0, n)
|
|
||||||
for {
|
|
||||||
line, err := r.ReadBytes('\n')
|
|
||||||
if len(bytes.TrimSpace(line)) > 0 {
|
|
||||||
lastNLines = append(lastNLines, string(bytes.TrimRight(line, "\n")))
|
|
||||||
for i := 0; i < len(lastNLines)-n; i++ {
|
|
||||||
fmt.Fprintln(w, lastNLines[i])
|
|
||||||
}
|
|
||||||
lastNLines = lastNLines[max(0, len(lastNLines)-n):]
|
|
||||||
}
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lastNLines, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (files Files) paths() []string {
|
func (files Files) paths() []string {
|
||||||
result := make([]string, 0, len(files))
|
result := make([]string, 0, len(files))
|
||||||
for i := range files {
|
for i := range files {
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package ledger
|
package ledger
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -450,148 +447,3 @@ func TestFilesOfDir(t *testing.T) {
|
|||||||
t.Error(paths)
|
t.Error(paths)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilesTempGetLastNLines(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
input string
|
|
||||||
n int
|
|
||||||
want []string
|
|
||||||
}{
|
|
||||||
"empty": {},
|
|
||||||
"get n lines from empty file": {
|
|
||||||
input: "",
|
|
||||||
n: 5,
|
|
||||||
want: []string{},
|
|
||||||
},
|
|
||||||
"get 0 lines from file": {
|
|
||||||
input: "#a\n#b",
|
|
||||||
n: 0,
|
|
||||||
want: []string{},
|
|
||||||
},
|
|
||||||
"get 3 lines from 2 line file": {
|
|
||||||
input: "#a\n#b",
|
|
||||||
n: 3,
|
|
||||||
want: []string{"#a", "#b"},
|
|
||||||
},
|
|
||||||
"get 2 lines from 3 line file": {
|
|
||||||
input: "#a\n#b\n#c",
|
|
||||||
n: 2,
|
|
||||||
want: []string{"#b", "#c"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, d := range cases {
|
|
||||||
c := d
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
p := path.Join(t.TempDir(), base64.URLEncoding.EncodeToString([]byte(t.Name())))
|
|
||||||
os.WriteFile(p, []byte(c.input), os.ModePerm)
|
|
||||||
files, err := NewFiles(p)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
if got, err := files.TempGetLastNLines(c.n); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
} else if fmt.Sprint(got) != fmt.Sprint(c.want) {
|
|
||||||
for i := range c.want {
|
|
||||||
t.Logf("want[%d] = %q", i, c.want[i])
|
|
||||||
}
|
|
||||||
for i := range got {
|
|
||||||
t.Logf(" got[%d] = %q", i, got[i])
|
|
||||||
}
|
|
||||||
t.Errorf("wanted\n\t%+v, got\n\t%+v", c.want, got)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilesTempSetLastNLines(t *testing.T) {
|
|
||||||
cases := map[string]struct {
|
|
||||||
given string
|
|
||||||
input []string
|
|
||||||
n int
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
"empty": {},
|
|
||||||
"append to empty": {
|
|
||||||
input: []string{"hello", "world"},
|
|
||||||
n: 100,
|
|
||||||
want: "hello\nworld\n",
|
|
||||||
},
|
|
||||||
"replace last 10 of 1 lines with 2": {
|
|
||||||
given: "ohno",
|
|
||||||
input: []string{"hello", "world"},
|
|
||||||
n: 10,
|
|
||||||
want: "hello\nworld\n",
|
|
||||||
},
|
|
||||||
"replace last 1 of 1 lines with 2": {
|
|
||||||
given: "ohno",
|
|
||||||
input: []string{"hello", "world"},
|
|
||||||
n: 1,
|
|
||||||
want: "hello\nworld\n",
|
|
||||||
},
|
|
||||||
"replace last 0 of 1 lines with 2": {
|
|
||||||
given: "ohno",
|
|
||||||
input: []string{"hello", "world"},
|
|
||||||
n: 0,
|
|
||||||
want: "ohno\nhello\nworld\n",
|
|
||||||
},
|
|
||||||
"replace last 1 of 1 lines with 0": {
|
|
||||||
given: "ohno",
|
|
||||||
input: []string{},
|
|
||||||
n: 1,
|
|
||||||
want: "",
|
|
||||||
},
|
|
||||||
"replace last 1 of 2 lines with 1": {
|
|
||||||
given: "ohno\nhaha",
|
|
||||||
input: []string{"replaced"},
|
|
||||||
n: 1,
|
|
||||||
want: "ohno\nreplaced\n",
|
|
||||||
},
|
|
||||||
"replace last 1 of 2 lines with 2": {
|
|
||||||
given: "ohno\nhaha",
|
|
||||||
input: []string{"replac", "ed"},
|
|
||||||
n: 1,
|
|
||||||
want: "ohno\nreplac\ned\n",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, d := range cases {
|
|
||||||
c := d
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
p := path.Join(t.TempDir(), base64.URLEncoding.EncodeToString([]byte(t.Name())))
|
|
||||||
realp := p + ".real"
|
|
||||||
|
|
||||||
os.WriteFile(realp, []byte(c.given), os.ModePerm)
|
|
||||||
if err := os.Symlink(realp, p); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if stat, err := os.Lstat(p); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
} else if stat.Mode().IsRegular() {
|
|
||||||
t.Error("p is already a regular file")
|
|
||||||
}
|
|
||||||
|
|
||||||
files := Files([]string{p})
|
|
||||||
if err := files.TempSetLastNLines(c.n, c.input); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
got, _ := os.ReadFile(p)
|
|
||||||
if string(got) != c.want {
|
|
||||||
t.Errorf("want\n%s, got\n%s", c.want, got)
|
|
||||||
}
|
|
||||||
|
|
||||||
realb, _ := os.ReadFile(realp)
|
|
||||||
b, _ := os.ReadFile(realp)
|
|
||||||
if !bytes.Equal(b, realb) {
|
|
||||||
t.Errorf("%s no longer links to %s", p, realp)
|
|
||||||
}
|
|
||||||
|
|
||||||
if stat, err := os.Lstat(p); err != nil {
|
|
||||||
t.Error(err)
|
|
||||||
} else if stat.Mode().IsRegular() {
|
|
||||||
t.Error("p is now a regular file")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,12 +8,21 @@ type Like func(Delta) bool
|
|||||||
|
|
||||||
type Likes []Like
|
type Likes []Like
|
||||||
|
|
||||||
func LikeTransaction(delta Delta) Like {
|
func LikeTransactions(deltas ...Delta) Like {
|
||||||
return func(d Delta) bool {
|
return func(d Delta) bool {
|
||||||
return d.Transaction == delta.Transaction
|
for i := range deltas {
|
||||||
|
if deltas[i].Transaction == d.Transaction {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func LikeTransaction(delta Delta) Like {
|
||||||
|
return LikeTransactions(delta)
|
||||||
|
}
|
||||||
|
|
||||||
func LikeBefore(date string) Like {
|
func LikeBefore(date string) Like {
|
||||||
return func(d Delta) bool {
|
return func(d Delta) bool {
|
||||||
return date >= d.Date
|
return date >= d.Date
|
||||||
|
|||||||
Reference in New Issue
Block a user