From 959094f40bed41ad345a0816bdad8837f3f9cca6 Mon Sep 17 00:00:00 2001 From: Onur Cinar Date: Tue, 19 Dec 2023 02:52:50 +0000 Subject: [PATCH] First, Last, and File System Repository. --- asset/README.md | 22 +++++++++--- asset/file_system_repository.go | 20 +++++++++++ asset/file_system_repository_test.go | 34 ++++++++++++++++++ asset/repository.go | 5 ++- asset/testdata/empty.csv | 1 + helper/README.md | 54 ++++++++++++++++++++++++---- helper/first.go | 26 ++++++++++++++ helper/first_test.go | 35 ++++++++++++++++++ helper/last.go | 15 ++++++++ helper/ring.go | 23 ++++++------ helper/ring_test.go | 5 +++ 11 files changed, 216 insertions(+), 24 deletions(-) create mode 100644 asset/testdata/empty.csv create mode 100644 helper/first.go create mode 100644 helper/first_test.go diff --git a/asset/README.md b/asset/README.md index 6607fe9..a890f19 100644 --- a/asset/README.md +++ b/asset/README.md @@ -27,12 +27,13 @@ The information provided on this project is strictly for informational purposes - [type FileSystemRepository](<#FileSystemRepository>) - [func NewFileSystemRepository\(base string\) \*FileSystemRepository](<#NewFileSystemRepository>) - [func \(r \*FileSystemRepository\) Get\(name string\) \(\<\-chan \*Snapshot, error\)](<#FileSystemRepository.Get>) + - [func \(r \*FileSystemRepository\) LastDate\(name string\) \(time.Time, error\)](<#FileSystemRepository.LastDate>) - [type Repository](<#Repository>) - [type Snapshot](<#Snapshot>) -## type [FileSystemRepository]() +## type [FileSystemRepository]() FileSystemRepository stores and retrieves asset snapshots using the local file system. @@ -44,7 +45,7 @@ type FileSystemRepository struct { ``` -### func [NewFileSystemRepository]() +### func [NewFileSystemRepository]() ```go func NewFileSystemRepository(base string) *FileSystemRepository @@ -53,7 +54,7 @@ func NewFileSystemRepository(base string) *FileSystemRepository NewFileSystemRepository initializes a file system repository with the given base directory. -### func \(\*FileSystemRepository\) [Get]() +### func \(\*FileSystemRepository\) [Get]() ```go func (r *FileSystemRepository) Get(name string) (<-chan *Snapshot, error) @@ -61,8 +62,17 @@ func (r *FileSystemRepository) Get(name string) (<-chan *Snapshot, error) Get attempts to return a channel of snapshots fo the asset with the given name. + +### func \(\*FileSystemRepository\) [LastDate]() + +```go +func (r *FileSystemRepository) LastDate(name string) (time.Time, error) +``` + +LastDate returns the date of the last snapshot for the asset with the given name. + -## type [Repository]() +## type [Repository]() Repository serves as a centralized storage and retrieval location for asset snapshots. @@ -71,6 +81,10 @@ type Repository interface { // Get attempts to return a channel of snapshots for // the asset with the given name. Get(name string) (<-chan *Snapshot, error) + + // LastDate returns the date of the last snapshot for + // the asset with the given name. + LastDate(name string) (time.Time, error) } ``` diff --git a/asset/file_system_repository.go b/asset/file_system_repository.go index af2689f..1f9c67e 100644 --- a/asset/file_system_repository.go +++ b/asset/file_system_repository.go @@ -5,8 +5,10 @@ package asset import ( + "errors" "fmt" "path/filepath" + "time" "github.com/cinar/indicator/helper" ) @@ -33,3 +35,21 @@ func (r *FileSystemRepository) Get(name string) (<-chan *Snapshot, error) { file := filepath.Join(r.base, fmt.Sprintf("%s.csv", name)) return helper.ReadFromCsvFile[Snapshot](file, true) } + +// LastDate returns the date of the last snapshot for the asset with the given name. +func (r *FileSystemRepository) LastDate(name string) (time.Time, error) { + var last time.Time + + snapshots, err := r.Get(name) + if err != nil { + return last, err + } + + snapshot := helper.ChanToSlice(helper.Last(snapshots, 1)) + + if len(snapshot) != 1 { + return last, errors.New("empty asset") + } + + return snapshot[0].Date, nil +} diff --git a/asset/file_system_repository_test.go b/asset/file_system_repository_test.go index 2434924..d8793e6 100644 --- a/asset/file_system_repository_test.go +++ b/asset/file_system_repository_test.go @@ -6,6 +6,7 @@ package asset_test import ( "testing" + "time" "github.com/cinar/indicator/asset" "github.com/cinar/indicator/helper" @@ -30,3 +31,36 @@ func TestFileSystemRepositoryGetNonExisting(t *testing.T) { t.Fatal("expected error") } } + +func TestFileSystemRepositoryLastDate(t *testing.T) { + expeted := time.Date(2022, 12, 30, 0, 0, 0, 0, time.UTC) + + repository := asset.NewFileSystemRepository("testdata") + + actual, err := repository.LastDate("brk-b") + if err != nil { + t.Fatal(err) + } + + if actual != expeted { + t.Fatalf("actual %v expected %v", actual, expeted) + } +} + +func TestFileSystemRepositoryLastDateNonExisting(t *testing.T) { + repository := asset.NewFileSystemRepository("testdata") + + _, err := repository.LastDate("brk") + if err == nil { + t.Fatal("expected error") + } +} + +func TestFileSystemRepositoryLastDateEmpty(t *testing.T) { + repository := asset.NewFileSystemRepository("testdata") + + _, err := repository.LastDate("empty") + if err == nil { + t.Fatal("expected error") + } +} diff --git a/asset/repository.go b/asset/repository.go index 32128a5..0905b5c 100644 --- a/asset/repository.go +++ b/asset/repository.go @@ -14,7 +14,6 @@ type Repository interface { Get(name string) (<-chan *Snapshot, error) // LastDate returns the date of the last snapshot for - // the asset with the given name, if any. Returns an - // empty value if no snapshots exist. - LastDate(name string) time.Time + // the asset with the given name. + LastDate(name string) (time.Time, error) } diff --git a/asset/testdata/empty.csv b/asset/testdata/empty.csv new file mode 100644 index 0000000..0335cf0 --- /dev/null +++ b/asset/testdata/empty.csv @@ -0,0 +1 @@ +Date,Open,High,Low,Close,Adj Close,Volume diff --git a/helper/README.md b/helper/README.md index ed3683f..7c502a9 100644 --- a/helper/README.md +++ b/helper/README.md @@ -40,10 +40,12 @@ The information provided on this project is strictly for informational purposes - [func Duplicate\[T any\]\(input \<\-chan T, count int\) \[\]\<\-chan T](<#Duplicate>) - [func Field\[T, S any\]\(c \<\-chan \*S, name string\) \(\<\-chan T, error\)](<#Field>) - [func Filter\[T Number\]\(c \<\-chan T, p func\(T\) bool\) \<\-chan T](<#Filter>) +- [func First\[T any\]\(c \<\-chan T, count int\) \<\-chan T](<#First>) - [func Head\[T Number\]\(c \<\-chan T, count int\) \<\-chan T](<#Head>) - [func IncrementBy\[T Number\]\(c \<\-chan T, i T\) \<\-chan T](<#IncrementBy>) - [func KeepNegatives\[T Number\]\(c \<\-chan T\) \<\-chan T](<#KeepNegatives>) - [func KeepPositives\[T Number\]\(c \<\-chan T\) \<\-chan T](<#KeepPositives>) +- [func Last\[T any\]\(c \<\-chan T, count int\) \<\-chan T](<#Last>) - [func Map\[F, T any\]\(c \<\-chan F, f func\(F\) T\) \<\-chan T](<#Map>) - [func Multiply\[T Number\]\(ac, bc \<\-chan T\) \<\-chan T](<#Multiply>) - [func MultiplyBy\[T Number\]\(c \<\-chan T, m T\) \<\-chan T](<#MultiplyBy>) @@ -88,7 +90,9 @@ The information provided on this project is strictly for informational purposes - [func NewNumericReportColumn\[T Number\]\(name string, values \<\-chan T\) ReportColumn](<#NewNumericReportColumn>) - [type Ring](<#Ring>) - [func NewRing\[T any\]\(size int\) \*Ring\[T\]](<#NewRing>) - - [func \(r \*Ring\[T\]\) Insert\(t T\) T](<#Ring[T].Insert>) + - [func \(r \*Ring\[T\]\) Get\(\) \(T, bool\)](<#Ring[T].Get>) + - [func \(r \*Ring\[T\]\) IsEmpty\(\) bool](<#Ring[T].IsEmpty>) + - [func \(r \*Ring\[T\]\) Put\(t T\) T](<#Ring[T].Put>) ## Constants @@ -344,6 +348,15 @@ even := helper.Filter(c, func(n int) bool { }) ``` + +## func [First]() + +```go +func First[T any](c <-chan T, count int) <-chan T +``` + +First takes a channel of values and returns a new channel containing the first N values. + ## func [Head]() @@ -412,6 +425,15 @@ positives := helper.KeepPositives(c) fmt.Println(helper.ChanToSlice(positives)) // [0, 20, 4, 0] ``` + +## func [Last]() + +```go +func Last[T any](c <-chan T, count int) <-chan T +``` + +Last takes a channel of values and returns a new channel containing the last N values. + ## func [Map]() @@ -950,7 +972,7 @@ func NewNumericReportColumn[T Number](name string, values <-chan T) ReportColumn NewNumericReportColumn returns a new instance of a numeric data column for a report. -## type [Ring]() +## type [Ring]() Ring represents a ring structure that can be instantiated using the NewRing function. @@ -972,7 +994,7 @@ type Ring[T any] struct { ``` -### func [NewRing]() +### func [NewRing]() ```go func NewRing[T any](size int) *Ring[T] @@ -980,13 +1002,31 @@ func NewRing[T any](size int) *Ring[T] NewRing creates a new ring instance with the given size. - -### func \(\*Ring\[T\]\) [Insert]() + +### func \(\*Ring\[T\]\) [Get]() + +```go +func (r *Ring[T]) Get() (T, bool) +``` + +Get retrieves the available value from the ring buffer. If empty, it returns the default value \(T\) and false. + + +### func \(\*Ring\[T\]\) [IsEmpty]() + +```go +func (r *Ring[T]) IsEmpty() bool +``` + +IsEmpty checks if the current ring buffer is empty. + + +### func \(\*Ring\[T\]\) [Put]() ```go -func (r *Ring[T]) Insert(t T) T +func (r *Ring[T]) Put(t T) T ``` -Insert function inserts the specified value into the ring and returns the value that was previously stored at that index. +Put inserts the specified value into the ring and returns the value that was previously stored at that index. Generated by [gomarkdoc]() diff --git a/helper/first.go b/helper/first.go new file mode 100644 index 0000000..7798d8d --- /dev/null +++ b/helper/first.go @@ -0,0 +1,26 @@ +// Copyright (c) 2023 Onur Cinar. All Rights Reserved. +// The source code is provided under MIT License. +// https://github.com/cinar/indicator + +package helper + +// First takes a channel of values and returns a new channel containing the first N values. +func First[T any](c <-chan T, count int) <-chan T { + result := make(chan T, cap(c)) + + go func() { + defer close(result) + for i := 0; i < count; i++ { + n, ok := <-c + if !ok { + return + } + + result <- n + } + + Drain(c) + }() + + return result +} diff --git a/helper/first_test.go b/helper/first_test.go new file mode 100644 index 0000000..d698b39 --- /dev/null +++ b/helper/first_test.go @@ -0,0 +1,35 @@ +// Copyright (c) 2023 Onur Cinar. All Rights Reserved. +// The source code is provided under MIT License. +// https://github.com/cinar/indicator + +package helper_test + +import ( + "testing" + + "github.com/cinar/indicator/helper" +) + +func TestFirst(t *testing.T) { + input := helper.SliceToChan([]int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) + expected := helper.SliceToChan([]int{1, 2, 3, 4}) + + actual := helper.First(input, 4) + + err := helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} + +func TestFirstLessValues(t *testing.T) { + input := helper.SliceToChan([]int{1, 2}) + expected := helper.SliceToChan([]int{1, 2}) + + actual := helper.First(input, 4) + + err := helper.CheckEquals(actual, expected) + if err != nil { + t.Fatal(err) + } +} diff --git a/helper/last.go b/helper/last.go index 6dc5002..2d6ec04 100644 --- a/helper/last.go +++ b/helper/last.go @@ -8,5 +8,20 @@ package helper func Last[T any](c <-chan T, count int) <-chan T { result := make(chan T, cap(c)) + go func() { + defer close(result) + + ring := NewRing[T](count) + + for n := range c { + ring.Put(n) + } + + for !ring.IsEmpty() { + n, _ := ring.Get() + result <- n + } + }() + return result } diff --git a/helper/ring.go b/helper/ring.go index 3ce8c3f..12f0cd3 100644 --- a/helper/ring.go +++ b/helper/ring.go @@ -4,8 +4,6 @@ package helper -import "fmt" - // Ring represents a ring structure that can be instantiated // using the NewRing function. // @@ -21,6 +19,7 @@ type Ring[T any] struct { buffer []T begin int end int + empty bool } // NewRing creates a new ring instance with the given size. @@ -29,22 +28,22 @@ func NewRing[T any](size int) *Ring[T] { buffer: make([]T, size), begin: 0, end: 0, + empty: true, } } // Put inserts the specified value into the ring and returns the // value that was previously stored at that index. func (r *Ring[T]) Put(t T) T { + if !r.empty && (r.end == r.begin) { + r.begin = r.nextIndex(r.begin) + } + o := r.buffer[r.end] r.buffer[r.end] = t r.end = r.nextIndex(r.end) - - if r.end == r.begin { - r.begin = r.nextIndex(r.begin) - } - - fmt.Printf("b=%d e=%d buffer=%v\n", r.begin, r.end, r.buffer) + r.empty = false return o } @@ -54,19 +53,23 @@ func (r *Ring[T]) Put(t T) T { func (r *Ring[T]) Get() (T, bool) { var t T - if r.IsEmpty() { + if r.empty { return t, false } t = r.buffer[r.begin] r.begin = r.nextIndex(r.begin) + if r.begin == r.end { + r.empty = true + } + return t, true } // IsEmpty checks if the current ring buffer is empty. func (r *Ring[T]) IsEmpty() bool { - return r.end == r.begin + return r.empty } // nextIndex returns the next index in a ring buffer, wrapping diff --git a/helper/ring_test.go b/helper/ring_test.go index 3015438..2e54e9f 100644 --- a/helper/ring_test.go +++ b/helper/ring_test.go @@ -55,4 +55,9 @@ func TestRingEmpty(t *testing.T) { if !ring.IsEmpty() { t.Fatal("not empty") } + + _, ok := ring.Get() + if ok { + t.Fatal("not empty") + } }