diff --git a/card.go b/card.go index d566fd9..449ad67 100644 --- a/card.go +++ b/card.go @@ -16,7 +16,13 @@ package riff -import "time" +import ( + "encoding/json" + "time" + + "github.com/open-spaced-repetition/go-fsrs" + "github.com/siyuan-note/logging" +) // Card 描述了闪卡。 type Card interface { @@ -24,7 +30,10 @@ type Card interface { ID() string // BlockID 返回闪卡关联的内容块 ID。 - BlockID() string + // BlockID() string + + // CSID 获取卡片的 CSID + GetCSID() string // NextDues 返回每种评分对应的下次到期时间。 NextDues() map[Rating]time.Time @@ -32,23 +41,72 @@ type Card interface { // SetNextDues 设置每种评分对应的下次到期时间。 SetNextDues(map[Rating]time.Time) - // SetDue 设置到期时间。 - SetDue(time.Time) + // GetUpdate 返回闪卡的更新时间。 + GetUpdate() time.Time + + // SetUpdate 设置闪卡的更新时间。 + SetUpdate(time.Time) + + // GetState 返回闪卡状态。 + GetState() State + + // SetState 设置闪卡状态。 + SetState(State) // GetLapses 返回闪卡的遗忘次数。 GetLapses() int + // SetLapses 设置闪卡的遗忘次数。 + SetLapses(int) + // GetReps 返回闪卡的复习次数。 GetReps() int - // GetState 返回闪卡状态。 - GetState() State + // SetReps 设置闪卡的复习次数。 + SetReps(int) + + // GetSuspend 返回闪卡是否被暂停。 + GetSuspend() bool + + // SetSuspend 设置闪卡是否被暂停。 + SetSuspend(bool) + + // GetTag 返回闪卡的标签。 + GetTag() string + + // SetTag 设置闪卡的标签。 + SetTag(string) + + // GetFlag 返回闪卡的标志。 + GetFlag() string + + // SetFlag 设置闪卡的标志。 + SetFlag(string) + + // GetPriority 返回闪卡的优先级。 + GetPriority() float64 + + // SetPriority 设置闪卡的优先级。 + SetPriority(float64) + + // GetDue 返回闪卡的到期时间。 + GetDue() time.Time + + // SetDue 设置闪卡的到期时间。 + SetDue(time.Time) + + // 返回 Algo + GetAlgo() Algo + + UseAlgo(algo Algo) - // GetLastReview 返回闪卡的最后复习时间。 - GetLastReview() time.Time + // 返回 MarshalImpl + GetMarshalImpl() []uint8 - // Clone 返回闪卡的克隆。 - Clone() Card + // 对 Impl 进行 Marshal + MarshalImpl() + + UnmarshalImpl() // Impl 返回具体的闪卡实现。 Impl() interface{} @@ -57,11 +115,129 @@ type Card interface { SetImpl(c interface{}) } +func UnmarshalImpl(card Card) { + switch card.GetAlgo() { + case AlgoFSRS: + impl := fsrs.Card{} + json.Unmarshal(card.GetMarshalImpl(), &impl) + card.SetImpl(impl) + default: + return + } +} + // BaseCard 描述了基础的闪卡实现。 type BaseCard struct { - CID string - BID string - NDues map[Rating]time.Time + CID string `xorm:"pk index"` + CSID string + Update time.Time + State State //State 返回闪卡状态。 + Lapses int //Lapses 返回闪卡的遗忘次数。 + Reps int //Reps 返回闪卡的复习次数。 + Suspend bool `xorm:"index"` + Tag string + Flag string + Priority float64 + Due time.Time `xorm:"index"` + NDues map[Rating]time.Time `xorm:"-"` + Algo Algo + AlgoImpl interface{} `xorm:"-"` + AlgoImplData []uint8 `json:"-"` +} + +func NewBaseCard(cs CardSource) (card *BaseCard) { + CSID := cs.GetCSID() + card = &BaseCard{ + CSID: CSID, + CID: newID(), + State: New, + Update: time.Now(), + Due: time.Now(), + NDues: map[Rating]time.Time{}, + Priority: 0.5, + AlgoImplData: []uint8{}, + } + return +} + +func (card *BaseCard) ID() string { + return card.CID +} + +func (card *BaseCard) GetCSID() string { + return card.CSID +} + +func (c *BaseCard) GetUpdate() time.Time { + return c.Update +} + +func (c *BaseCard) SetUpdate(update time.Time) { + c.Update = update +} + +func (c *BaseCard) GetState() State { + return c.State +} + +func (c *BaseCard) SetState(state State) { + c.State = state +} + +func (c *BaseCard) GetLapses() int { + return c.Lapses +} + +func (c *BaseCard) SetLapses(lapses int) { + c.Lapses = lapses +} + +func (c *BaseCard) GetReps() int { + return c.Reps +} + +func (c *BaseCard) SetReps(reps int) { + c.Reps = reps +} + +func (c *BaseCard) GetSuspend() bool { + return c.Suspend +} + +func (c *BaseCard) SetSuspend(suspend bool) { + c.Suspend = suspend +} + +func (c *BaseCard) GetTag() string { + return c.Tag +} + +func (c *BaseCard) SetTag(tag string) { + c.Tag = tag +} + +func (c *BaseCard) GetFlag() string { + return c.Flag +} + +func (c *BaseCard) SetFlag(flag string) { + c.Flag = flag +} + +func (c *BaseCard) GetPriority() float64 { + return c.Priority +} + +func (c *BaseCard) SetPriority(priority float64) { + c.Priority = priority +} + +func (c *BaseCard) GetDue() time.Time { + return c.Due +} + +func (c *BaseCard) SetDue(due time.Time) { + c.Due = due } func (card *BaseCard) NextDues() map[Rating]time.Time { @@ -72,10 +248,42 @@ func (card *BaseCard) SetNextDues(dues map[Rating]time.Time) { card.NDues = dues } -func (card *BaseCard) ID() string { - return card.CID +func (card *BaseCard) Impl() interface{} { + return card.AlgoImpl +} +func (card *BaseCard) SetImpl(c interface{}) { + card.AlgoImpl = c +} + +func (card *BaseCard) UseAlgo(algo Algo) { + switch algo { + case AlgoFSRS: + AlgoImpl := fsrs.NewCard() + card.AlgoImpl = AlgoImpl + card.Algo = AlgoFSRS + // card.Due = AlgoImpl.Due + default: + logging.LogErrorf("unsupported Algo: %s", algo) + } +} + +func (card *BaseCard) GetMarshalImpl() []uint8 { + if card.AlgoImplData == nil || len(card.AlgoImplData) == 0 { + card.MarshalImpl() + } + + return card.AlgoImplData +} + +func (card *BaseCard) MarshalImpl() { + data, _ := json.Marshal(card.AlgoImpl) + card.AlgoImplData = data +} + +func (card *BaseCard) UnmarshalImpl() { + UnmarshalImpl(card) } -func (card *BaseCard) BlockID() string { - return card.BID +func (card *BaseCard) GetAlgo() Algo { + return card.Algo } diff --git a/card_history.go b/card_history.go new file mode 100644 index 0000000..bd10bda --- /dev/null +++ b/card_history.go @@ -0,0 +1,108 @@ +package riff + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/open-spaced-repetition/go-fsrs" +) + +type History interface { + ID() string + + // 对 Impl 进行 Marshal + MarshalImpl() + + UnmarshalImpl() (err error) +} + +type BaseHistory struct { + HID string `xorm:"pk index"` + CID string + Update time.Time `xorm:"index"` + UpdateResult int + State State + Tag string + Flag string + Suspend bool + Priority float64 + Due time.Time + Algo Algo + AlgoImpl interface{} `xorm:"-"` + AlgoImplData []uint8 `json:"-"` +} + +type ReviewLog struct { + HID string + Rate Rating + Review time.Time +} + +type ReviewHistory struct { + BaseHistory `xorm:"extends"` + ReviewLog `xorm:"extends"` +} + +func NewBaseHistory(c Card) (history *BaseHistory) { + return &BaseHistory{ + HID: newID(), + CID: c.ID(), + Update: time.Now(), + UpdateResult: CreateHistory, // 这里可以根据需要设置具体的更新结果 + State: c.GetState(), + Tag: c.GetTag(), + Flag: c.GetFlag(), + Suspend: c.GetSuspend(), + Priority: c.GetPriority(), + Due: c.GetDue(), + Algo: c.GetAlgo(), + AlgoImpl: c.Impl(), // 假设Card接口有Impl方法 + AlgoImplData: c.GetMarshalImpl(), // 假设Card接口有GetMarshalImpl方法 + } +} + +func (b *BaseHistory) ID() string { + return b.HID +} + +func (b *BaseHistory) UnmarshalImpl() (err error) { + if b.AlgoImplData == nil || len(b.AlgoImplData) == 0 { + err = errors.New("dont have AlgoImplData") + return + } + switch b.Algo { + case AlgoFSRS: + impl := fsrs.Card{} + json.Unmarshal(b.AlgoImplData, &impl) + b.AlgoImpl = impl + default: + err = fmt.Errorf("un support Algo Type : %s", string(b.Algo)) + return + } + return +} + +func (b *BaseHistory) MarshalImpl() { + if len(b.AlgoImplData) != 0 && b.AlgoImpl == nil { + return + } + data, _ := json.Marshal(b.AlgoImpl) + b.AlgoImplData = data +} + +func NewReviewLog(h History, rate Rating) (log *ReviewLog) { + log = &ReviewLog{ + HID: h.ID(), + Rate: rate, + Review: time.Now(), + } + return +} + +const ( + EditUpdate int = -2 + CreateHistory int = -1 + ReviewUpdate int = 1 +) diff --git a/card_source.go b/card_source.go new file mode 100644 index 0000000..04ef8f4 --- /dev/null +++ b/card_source.go @@ -0,0 +1,38 @@ +package riff + +type CardSource interface { + GetCSID() string + GetDIDs() []string + GetBlockIDs() []string +} + +type BaseCardSource struct { + CSID string `xorm:"pk index"` + Hash string + BlockIDs []string + DID []string + CType string + SourceContext map[string]interface{} +} + +func NewBaseCardSource(DID string) *BaseCardSource { + ID := newID() + cardSource := &BaseCardSource{ + CSID: ID, + DID: []string{DID}, + BlockIDs: []string{}, + SourceContext: map[string]interface{}{}, + } + return cardSource +} + +func (cs *BaseCardSource) GetCSID() string { + return cs.CSID +} + +func (cs *BaseCardSource) GetDIDs() []string { + return cs.DID +} +func (cs *BaseCardSource) GetBlockIDs() []string { + return cs.BlockIDs +} diff --git a/deck.go b/deck.go index ebc9372..707f79f 100644 --- a/deck.go +++ b/deck.go @@ -16,208 +16,134 @@ package riff -import ( - "errors" - "path/filepath" - "sync" - "time" - - "github.com/siyuan-note/filelock" - "github.com/siyuan-note/logging" - "github.com/vmihailenco/msgpack/v5" +import "time" + +type Deck interface { + AddCard(cardID, blockID string) // FINISH + RemoveCard(cardID string) // TODO + SetCard(card Card) // TODO + GetCard(cardID string) Card // TODO + GetCardsByBlockID(blockID string) (ret []Card) // UNKNOW + GetCardsByBlockIDs(blockIDs []string) (ret []Card) // INSTEAD OF BETTER + GetNewCardsByBlockIDs(blockIDs []string) (ret []Card) // INSTEAD OF BETTER + GetDueCardsByBlockIDs(blockIDs []string) (ret []Card) // INSTEAD OF BETTER + GetBlockIDs() (ret []string) // TODO + CountCards() int // TODO + Save() (err error) // FINISH + SaveLog(log *Log) (err error) // FINISH + Review(cardID string, rating Rating) (ret *Log) // FINISH + Dues() (ret []Card) // FINISH + GetDID() string +} + +// BaseDeck 描述了一套闪卡包。 +type BaseDeck struct { + DID string // DeckID + Name string // 名称 + Desc string // 描述 + DeckType DeckType + Created time.Time // 创建时间 + Updated time.Time `xorm:"updated"` // 更新时间 + ParentDeckID string + DeckContext map[string]interface{} + riff *Riff `json:"-"` +} + +type DeckType string + +const ( + Set DeckType = "Set" + Collection DeckType = "Collection" ) -// Deck 描述了一套闪卡包。 -type Deck struct { - ID string // ID - Name string // 名称 - Algo Algo // 间隔重复算法 - Desc string // 描述 - Created int64 // 创建时间 - Updated int64 // 更新时间 - - store Store // 底层存储 - lock *sync.Mutex -} - -// LoadDeck 从文件夹 saveDir 路径上加载 id 闪卡包。 -func LoadDeck(saveDir, id string, requestRetention float64, maximumInterval int, weights string) (deck *Deck, err error) { - created := time.Now().UnixMilli() - deck = &Deck{ - ID: id, - Name: id, - Algo: AlgoFSRS, - Created: created, - Updated: created, - lock: &sync.Mutex{}, - } - - dataPath := getDeckMsgpackPath(saveDir, id) - if filelock.IsExist(dataPath) { - var data []byte - data, err = filelock.ReadFile(dataPath) - if nil != err { - logging.LogErrorf("load deck [%s] failed: %s", deck.Name, err) - return - } - - err = msgpack.Unmarshal(data, deck) - if nil != err { - logging.LogErrorf("load deck [%s] failed: %s", deck.Name, err) - return - } +func DefaultBaseDeck() *BaseDeck { + Created := time.Now() + deck := &BaseDeck{ + DID: builtInDeck, + Name: "builtInDeck", + Desc: "built in Deck", + Created: Created, + DeckType: Collection, + DeckContext: map[string]interface{}{}, } + return deck +} - var store Store - switch deck.Algo { - case AlgoFSRS: - store = NewFSRSStore(deck.ID, saveDir, requestRetention, maximumInterval, weights) - err = store.Load() - default: - err = errors.New("not supported yet") - return - } - if nil != err { - return +func NewBaseDeck() (deck *BaseDeck) { + deck = &BaseDeck{ + DID: newID(), + Created: time.Now(), + DeckType: Collection, } - deck.store = store return } -// AddCard 新建一张闪卡。 -func (deck *Deck) AddCard(cardID, blockID string) { - deck.lock.Lock() - defer deck.lock.Unlock() - - card := deck.store.GetCard(cardID) - if nil != card { - return - } - - deck.store.AddCard(cardID, blockID) - deck.Updated = time.Now().UnixMilli() +func (bd *BaseDeck) AddCard(cardID, blockID string) { + // 空实现 } -// RemoveCard 删除一张闪卡。 -func (deck *Deck) RemoveCard(cardID string) { - deck.lock.Lock() - defer deck.lock.Unlock() - - deck.store.RemoveCard(cardID) - deck.Updated = time.Now().UnixMilli() +func (bd *BaseDeck) RemoveCard(cardID string) { + // 空实现 } -// SetCard 设置一张闪卡。 -func (deck *Deck) SetCard(card Card) { - deck.lock.Lock() - defer deck.lock.Unlock() - - deck.store.SetCard(card) +func (bd *BaseDeck) SetCard(card Card) { + // 空实现 } -// GetCard 根据闪卡 ID 获取对应的闪卡。 -func (deck *Deck) GetCard(cardID string) Card { - deck.lock.Lock() - defer deck.lock.Unlock() - return deck.store.GetCard(cardID) +func (bd *BaseDeck) GetCard(cardID string) Card { + // 空实现 + return nil } -func (deck *Deck) GetCardsByBlockID(blockID string) (ret []Card) { - deck.lock.Lock() - defer deck.lock.Unlock() - - return deck.store.GetCardsByBlockID(blockID) +func (bd *BaseDeck) GetCardsByBlockID(blockID string) (ret []Card) { + // 空实现 + return nil } -// GetCardsByBlockIDs 获取指定内容块的所有卡片。 -func (deck *Deck) GetCardsByBlockIDs(blockIDs []string) (ret []Card) { - deck.lock.Lock() - defer deck.lock.Unlock() - - return deck.store.GetCardsByBlockIDs(blockIDs) +func (bd *BaseDeck) GetCardsByBlockIDs(blockIDs []string) (ret []Card) { + // 空实现 + return nil } -func (deck *Deck) GetNewCardsByBlockIDs(blockIDs []string) (ret []Card) { - deck.lock.Lock() - defer deck.lock.Unlock() - - return deck.store.GetNewCardsByBlockIDs(blockIDs) +func (bd *BaseDeck) GetNewCardsByBlockIDs(blockIDs []string) (ret []Card) { + // 空实现 + return nil } -func (deck *Deck) GetDueCardsByBlockIDs(blockIDs []string) (ret []Card) { - deck.lock.Lock() - defer deck.lock.Unlock() - - return deck.store.GetDueCardsByBlockIDs(blockIDs) +func (bd *BaseDeck) GetDueCardsByBlockIDs(blockIDs []string) (ret []Card) { + // 空实现 + return nil } -// GetBlockIDs 获取所有内容块 ID。 -func (deck *Deck) GetBlockIDs() (ret []string) { - deck.lock.Lock() - defer deck.lock.Unlock() - - return deck.store.GetBlockIDs() +func (bd *BaseDeck) GetBlockIDs() (ret []string) { + // 空实现 + return nil } -// CountCards 获取卡包中的闪卡数量。 -func (deck *Deck) CountCards() int { - deck.lock.Lock() - defer deck.lock.Unlock() - - return deck.store.CountCards() +func (bd *BaseDeck) CountCards() int { + // 空实现 + return 0 } -// Save 保存闪卡包。 -func (deck *Deck) Save() (err error) { - deck.lock.Lock() - defer deck.lock.Unlock() - - deck.Updated = time.Now().UnixMilli() - err = deck.store.Save() - if nil != err { - logging.LogErrorf("save deck [%s] failed: %s", deck.Name, err) - return - } - - saveDir := deck.store.GetSaveDir() - dataPath := getDeckMsgpackPath(saveDir, deck.ID) - data, err := msgpack.Marshal(deck) - if nil != err { - logging.LogErrorf("save deck failed: %s", err) - return - } - if err = filelock.WriteFile(dataPath, data); nil != err { - logging.LogErrorf("save deck failed: %s", err) - return - } - return +func (bd *BaseDeck) Save() (err error) { + // 空实现 + return nil } -// SaveLog 保存闪卡包的复习日志。 -func (deck *Deck) SaveLog(log *Log) (err error) { - deck.lock.Lock() - defer deck.lock.Unlock() - - return deck.store.SaveLog(log) +func (bd *BaseDeck) SaveLog(log *Log) (err error) { + // 空实现 + return nil } -// Review 复习一张闪卡,rating 为复习评分结果。 -func (deck *Deck) Review(cardID string, rating Rating) (ret *Log) { - deck.lock.Lock() - defer deck.lock.Unlock() - - ret = deck.store.Review(cardID, rating) - deck.Updated = time.Now().UnixMilli() - return +func (bd *BaseDeck) Review(cardID string, rating Rating) (ret *Log) { + // 空实现 + return nil } -// Dues 返回所有到期的闪卡。 -func (deck *Deck) Dues() (ret []Card) { - deck.lock.Lock() - defer deck.lock.Unlock() - return deck.store.Dues() +func (bd *BaseDeck) Dues() (ret []Card) { + // 空实现 + return nil } - -func getDeckMsgpackPath(saveDir, id string) string { - return filepath.Join(saveDir, id+".deck") +func (bd *BaseDeck) GetDID() string { + return bd.DID } diff --git a/deck_test.go b/deck_test.go deleted file mode 100644 index acffcf5..0000000 --- a/deck_test.go +++ /dev/null @@ -1,83 +0,0 @@ -// Riff - Spaced repetition. -// Copyright (c) 2022-present, b3log.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 . - -package riff - -import ( - "os" - "testing" - - "github.com/open-spaced-repetition/go-fsrs" -) - -func TestDeck(t *testing.T) { - const saveDir = "testdata" - os.MkdirAll(saveDir, 0755) - defer os.RemoveAll(saveDir) - deckID := newID() - deck, err := LoadDeck(saveDir, deckID, requestRetention, maximumInterval, weights) - if nil != err { - t.Fatal(err) - } - deckName := "deck0" - if deck.Name == deckID { - deck.Name = deckName - } - - cardID, blockID := newID(), newID() - deck.AddCard(cardID, blockID) - card := deck.GetCard(cardID) - if card.ID() != cardID { - t.Fatalf("card id [%s] != [%s]", card.ID(), cardID) - } - - deck.Review(cardID, Good) - due := card.Impl().(*fsrs.Card).Due.UnixMilli() - card = deck.GetCard(cardID) - due2 := card.Impl().(*fsrs.Card).Due.UnixMilli() - if due2 != due { - t.Fatalf("card due [%v] != [%v]", due2, due) - } - - err = deck.Save() - if nil != err { - t.Fatal(err) - } - deck = nil - - deck, err = LoadDeck(saveDir, deckID, requestRetention, maximumInterval, weights) - if nil != err { - t.Fatal(err) - } - - if deckName != deck.Name { - t.Fatalf("deck name [%s] != [%s]", deck.Name, deckID) - } - - card = deck.GetCard(cardID) - if card.ID() != cardID { - t.Fatalf("card id [%s] != [%s]", card.ID(), cardID) - } - due3 := card.Impl().(*fsrs.Card).Due.UnixMilli() - if due2 != due3 { - t.Fatalf("card due [%v] != [%v]", due2, due3) - } - - count := deck.CountCards() - if 1 != count { - t.Fatalf("card count [%d] != [1]", count) - } -} diff --git a/fsrs_store.go b/fsrs_store.go deleted file mode 100644 index 9a588e4..0000000 --- a/fsrs_store.go +++ /dev/null @@ -1,362 +0,0 @@ -// Riff - Spaced repetition. -// Copyright (c) 2022-present, b3log.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 . - -package riff - -import ( - "os" - "path/filepath" - "sort" - "strconv" - "strings" - "time" - - "github.com/88250/gulu" - "github.com/open-spaced-repetition/go-fsrs" - "github.com/siyuan-note/filelock" - "github.com/siyuan-note/logging" - "github.com/vmihailenco/msgpack/v5" -) - -type FSRSStore struct { - *BaseStore - - cards map[string]*FSRSCard - params fsrs.Parameters -} - -func NewFSRSStore(id, saveDir string, requestRetention float64, maximumInterval int, weights string) *FSRSStore { - params := fsrs.DefaultParam() - params.RequestRetention = requestRetention - params.MaximumInterval = float64(maximumInterval) - params.W = [17]float64{} - for i, w := range strings.Split(weights, ",") { - w = strings.TrimSpace(w) - params.W[i], _ = strconv.ParseFloat(w, 64) - } - - return &FSRSStore{ - BaseStore: NewBaseStore(id, "fsrs", saveDir), - cards: map[string]*FSRSCard{}, - params: params, - } -} - -func (store *FSRSStore) AddCard(id, blockID string) Card { - store.lock.Lock() - defer store.lock.Unlock() - - c := fsrs.NewCard() - card := &FSRSCard{BaseCard: &BaseCard{id, blockID, nil}, C: &c} - store.cards[id] = card - return card -} - -func (store *FSRSStore) GetCard(id string) Card { - store.lock.Lock() - defer store.lock.Unlock() - - ret := store.cards[id] - if nil == ret { - return nil - } - return ret -} - -func (store *FSRSStore) SetCard(card Card) { - store.lock.Lock() - defer store.lock.Unlock() - - store.cards[card.ID()] = card.(*FSRSCard) -} - -func (store *FSRSStore) RemoveCard(id string) Card { - store.lock.Lock() - defer store.lock.Unlock() - - card := store.cards[id] - if nil == card { - return nil - } - delete(store.cards, id) - return card -} - -func (store *FSRSStore) GetCardsByBlockID(blockID string) (ret []Card) { - store.lock.Lock() - defer store.lock.Unlock() - - for _, card := range store.cards { - if card.BlockID() == blockID { - ret = append(ret, card) - } - } - return -} - -func (store *FSRSStore) GetCardsByBlockIDs(blockIDs []string) (ret []Card) { - store.lock.Lock() - defer store.lock.Unlock() - - blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs) - for _, card := range store.cards { - if gulu.Str.Contains(card.BlockID(), blockIDs) { - ret = append(ret, card) - } - } - return -} - -func (store *FSRSStore) GetNewCardsByBlockIDs(blockIDs []string) (ret []Card) { - store.lock.Lock() - defer store.lock.Unlock() - - blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs) - for _, card := range store.cards { - c := card.Impl().(*fsrs.Card) - if !c.LastReview.IsZero() { - continue - } - - if gulu.Str.Contains(card.BlockID(), blockIDs) { - ret = append(ret, card) - } - } - return -} - -func (store *FSRSStore) GetDueCardsByBlockIDs(blockIDs []string) (ret []Card) { - store.lock.Lock() - defer store.lock.Unlock() - - blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs) - now := time.Now() - for _, card := range store.cards { - c := card.Impl().(*fsrs.Card) - if now.Before(c.Due) { - continue - } - - if gulu.Str.Contains(card.BlockID(), blockIDs) { - ret = append(ret, card) - } - } - return -} - -func (store *FSRSStore) GetBlockIDs() (ret []string) { - store.lock.Lock() - defer store.lock.Unlock() - - ret = []string{} - for _, card := range store.cards { - ret = append(ret, card.BlockID()) - } - ret = gulu.Str.RemoveDuplicatedElem(ret) - sort.Strings(ret) - return -} - -func (store *FSRSStore) CountCards() int { - store.lock.Lock() - defer store.lock.Unlock() - - return len(store.cards) -} - -func (store *FSRSStore) Review(cardId string, rating Rating) (ret *Log) { - store.lock.Lock() - defer store.lock.Unlock() - - now := time.Now() - card := store.cards[cardId] - if nil == card { - logging.LogWarnf("not found card [id=%s] to review", cardId) - return - } - - schedulingInfo := store.params.Repeat(*card.C, now) - updated := schedulingInfo[fsrs.Rating(rating)].Card - card.SetImpl(&updated) - store.cards[cardId] = card - - reviewLog := schedulingInfo[fsrs.Rating(rating)].ReviewLog - ret = &Log{ - ID: newID(), - CardID: cardId, - Rating: rating, - ScheduledDays: reviewLog.ScheduledDays, - ElapsedDays: reviewLog.ElapsedDays, - Reviewed: reviewLog.Review.Unix(), - State: State(reviewLog.State), - } - return -} - -func (store *FSRSStore) Dues() (ret []Card) { - store.lock.Lock() - defer store.lock.Unlock() - - now := time.Now() - for _, card := range store.cards { - c := card.Impl().(*fsrs.Card) - if now.Before(c.Due) { - continue - } - - schedulingInfos := store.params.Repeat(*c, now) - nextDues := map[Rating]time.Time{} - for rating, schedulingInfo := range schedulingInfos { - nextDues[Rating(rating)] = schedulingInfo.Card.Due - } - card.SetNextDues(nextDues) - ret = append(ret, card) - } - return -} - -func (store *FSRSStore) Load() (err error) { - store.lock.Lock() - defer store.lock.Unlock() - - store.cards = map[string]*FSRSCard{} - p := store.getMsgPackPath() - if !filelock.IsExist(p) { - return - } - - data, err := filelock.ReadFile(p) - if nil != err { - logging.LogErrorf("load cards failed: %s", err) - } - if err = msgpack.Unmarshal(data, &store.cards); nil != err { - logging.LogErrorf("load cards failed: %s", err) - return - } - return -} - -func (store *FSRSStore) Save() (err error) { - store.lock.Lock() - defer store.lock.Unlock() - - saveDir := store.GetSaveDir() - if !gulu.File.IsDir(saveDir) { - if err = os.MkdirAll(saveDir, 0755); nil != err { - return - } - } - - p := store.getMsgPackPath() - data, err := msgpack.Marshal(store.cards) - if nil != err { - logging.LogErrorf("save cards failed: %s", err) - return - } - if err = filelock.WriteFile(p, data); nil != err { - logging.LogErrorf("save cards failed: %s", err) - return - } - return -} - -func (store *FSRSStore) SaveLog(log *Log) (err error) { - store.lock.Lock() - defer store.lock.Unlock() - - saveDir := store.GetSaveDir() - saveDir = filepath.Join(saveDir, "logs") - if !gulu.File.IsDir(saveDir) { - if err = os.MkdirAll(saveDir, 0755); nil != err { - return - } - } - - yyyyMM := time.Now().Format("200601") - p := filepath.Join(saveDir, yyyyMM+".msgpack") - logs := []*Log{} - var data []byte - if filelock.IsExist(p) { - data, err = filelock.ReadFile(p) - if nil != err { - logging.LogErrorf("load logs failed: %s", err) - return - } - - if err = msgpack.Unmarshal(data, &logs); nil != err { - logging.LogErrorf("unmarshal logs failed: %s", err) - return - } - } - logs = append(logs, log) - - if data, err = msgpack.Marshal(logs); nil != err { - logging.LogErrorf("marshal logs failed: %s", err) - return - } - if err = filelock.WriteFile(p, data); nil != err { - logging.LogErrorf("write logs failed: %s", err) - return - } - return -} - -type FSRSCard struct { - *BaseCard - C *fsrs.Card -} - -func (card *FSRSCard) Impl() interface{} { - return card.C -} - -func (card *FSRSCard) SetImpl(c interface{}) { - card.C = c.(*fsrs.Card) -} - -func (card *FSRSCard) GetLapses() int { - return int(card.C.Lapses) -} - -func (card *FSRSCard) GetReps() int { - return int(card.C.Reps) -} - -func (card *FSRSCard) GetState() State { - return State(card.C.State) -} - -func (card *FSRSCard) GetLastReview() time.Time { - return card.C.LastReview -} - -func (card *FSRSCard) Clone() Card { - data, err := gulu.JSON.MarshalJSON(card) - if nil != err { - logging.LogErrorf("marshal card failed: %s", err) - return nil - } - ret := &FSRSCard{} - if err = gulu.JSON.UnmarshalJSON(data, ret); nil != err { - logging.LogErrorf("unmarshal card failed: %s", err) - return nil - } - return ret -} - -func (card *FSRSCard) SetDue(due time.Time) { - card.C.Due = due -} diff --git a/fsrs_store_test.go b/fsrs_store_test.go deleted file mode 100644 index 6edb3ae..0000000 --- a/fsrs_store_test.go +++ /dev/null @@ -1,121 +0,0 @@ -// Riff - Spaced repetition. -// Copyright (c) 2022-present, b3log.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 . - -package riff - -import ( - "github.com/88250/gulu" - "os" - "strings" - "testing" - "time" - - "github.com/open-spaced-repetition/go-fsrs" -) - -const ( - requestRetention = 0.9 - maximumInterval = 36500 - weights = "0.40, 0.60, 2.40, 5.80, 4.93, 0.94, 0.86, 0.01, 1.49, 0.14, 0.94, 2.18, 0.05, 0.34, 1.26, 0.29, 2.61" -) - -func TestFSRSStore(t *testing.T) { - const storePath = "testdata" - os.MkdirAll(storePath, 0755) - defer os.RemoveAll(storePath) - - store := NewFSRSStore("test-store", storePath, requestRetention, maximumInterval, weights) - p := fsrs.DefaultParam() - start := time.Now() - repeatTime := start - ids := map[string]bool{} - var firstCardID, firstBlockID, lastCardID, lastBlockID string - max := 10000 - for i := 0; i < max; i++ { - id, blockID := newID(), newID() - if 0 == i { - firstCardID = id - firstBlockID = blockID - } else if max-1 == i { - lastCardID = id - lastBlockID = blockID - } - store.AddCard(id, blockID) - card := store.GetCard(id) - c := *card.Impl().(*fsrs.Card) - ids[id] = true - - for j := 0; j < 10; j++ { - schedulingInfo := p.Repeat(c, repeatTime) - c = schedulingInfo[fsrs.Hard].Card - repeatTime = c.Due - } - repeatTime = start - } - cardsLen := len(store.cards) - t.Logf("cards len [%d]", cardsLen) - if len(ids) != len(store.cards) { - t.Fatalf("cards len [%d] != ids len [%d]", len(store.cards), len(ids)) - } - - count := store.CountCards() - if cardsLen != count { - t.Fatalf("cards len [%d] != count [%d]", cardsLen, count) - } - - if err := store.Save(); nil != err { - t.Fatal(err) - } - t.Logf("saved cards [len=%d]", len(store.cards)) - - if err := store.Load(); nil != err { - t.Fatal(err) - } - t.Logf("loaded cards len [%d]", len(store.cards)) - - if cardsLen != len(store.cards) { - t.Fatal("cards len not equal") - } - - cards := store.GetCardsByBlockID(firstBlockID) - if 1 != len(cards) { - t.Fatalf("cards by block id [len=%d]", len(cards)) - } - if firstCardID != cards[0].ID() { - t.Fatalf("cards by block id [cardID=%s]", cards[0].ID()) - } - - cards = store.GetCardsByBlockID(lastBlockID) - if 1 != len(cards) { - t.Fatalf("cards by block id [len=%d]", len(cards)) - } - if lastCardID != cards[0].ID() { - t.Fatalf("cards by block id [cardID=%s]", cards[0].ID()) - } - - cards = store.GetCardsByBlockIDs([]string{firstBlockID, lastBlockID}) - if 2 != len(cards) { - t.Fatalf("cards by block ids [len=%d]", len(cards)) - } - cardIDs := []string{cards[0].ID(), cards[1].ID()} - if !gulu.Str.Contains(firstCardID, cardIDs) { - t.Fatalf("cards by block ids [cardIDs=%v]", cardIDs) - } - if !gulu.Str.Contains(lastCardID, cardIDs) { - t.Fatalf("cards by block ids [cardIDs=%v]", cardIDs) - } - t.Logf("cards by block ids [len=%d], card ids [%s]", len(cards), strings.Join(cardIDs, ", ")) -} diff --git a/go.mod b/go.mod index b0331b6..377ff7b 100644 --- a/go.mod +++ b/go.mod @@ -5,35 +5,44 @@ go 1.21 toolchain go1.21.6 require ( - github.com/88250/gulu v1.2.3-0.20231209020950-b7b6994e395c + github.com/88250/gulu v1.2.3-0.20240505150113-bc43bd50f866 + github.com/mattn/go-sqlite3 v1.14.22 github.com/open-spaced-repetition/go-fsrs v1.2.1 - github.com/siyuan-note/filelock v0.0.0-20240419132904-2fbfe64f1939 - github.com/siyuan-note/logging v0.0.0-20231208035918-61f884c854f0 - github.com/vmihailenco/msgpack/v5 v5.4.1 + github.com/siyuan-note/filelock v0.0.0-20240629145917-7545564cf0a4 + github.com/siyuan-note/logging v0.0.0-20240505035402-6430d57006a2 + github.com/syndtr/goleveldb v1.0.0 + xorm.io/xorm v1.3.9 ) require ( github.com/andybalholm/brotli v1.1.0 // indirect - github.com/cloudflare/circl v1.3.8 // indirect - github.com/getsentry/sentry-go v0.27.0 // indirect + github.com/cloudflare/circl v1.3.9 // indirect + github.com/getsentry/sentry-go v0.28.1 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/google/pprof v0.0.0-20240430035430-e4905b036c4e // indirect + github.com/goccy/go-json v0.9.11 // indirect + github.com/golang/snappy v0.0.4 // indirect + github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/imroc/req/v3 v3.43.3 // indirect - github.com/klauspost/compress v1.17.8 // indirect - github.com/onsi/ginkgo/v2 v2.17.2 // indirect + github.com/imroc/req/v3 v3.43.7 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/onsi/ginkgo/v2 v2.19.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect - github.com/quic-go/quic-go v0.43.0 // indirect - github.com/refraction-networking/utls v1.6.4 // indirect - github.com/siyuan-note/httpclient v0.0.0-20240429013218-3caa1f89f9ed // indirect - github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/quic-go/quic-go v0.45.1 // indirect + github.com/refraction-networking/utls v1.6.6 // indirect + github.com/siyuan-note/httpclient v0.0.0-20240626145026-29585d45a51c // indirect go.uber.org/mock v0.4.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect - golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect - golang.org/x/tools v0.20.0 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 // indirect + golang.org/x/mod v0.18.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + golang.org/x/tools v0.22.0 // indirect + modernc.org/ccgo/v3 v3.17.0 // indirect + modernc.org/libc v1.52.1 // indirect + xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 // indirect ) diff --git a/go.sum b/go.sum index b728182..e87ef82 100644 --- a/go.sum +++ b/go.sum @@ -1,36 +1,74 @@ -github.com/88250/gulu v1.2.3-0.20231209020950-b7b6994e395c h1:Fas3hxqP33xA9KKDV50jUmppiiOukk5bdV00Hk5VSSk= -github.com/88250/gulu v1.2.3-0.20231209020950-b7b6994e395c/go.mod h1:pTWnjt+6qUqNnP9xltswsJxgCBVu3C7eW09u48LWX0k= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= +gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= +github.com/88250/gulu v1.2.3-0.20240505150113-bc43bd50f866 h1:RFfNFS0hv6TbOuwET6xZAfGlV4hNlXiWTnfbLN1eF6k= +github.com/88250/gulu v1.2.3-0.20240505150113-bc43bd50f866/go.mod h1:MUfzyfmbPrRDZLqxc7aPrVYveatTHRfoUa5TynPS0i8= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= -github.com/cloudflare/circl v1.3.8 h1:j+V8jJt09PoeMFIu2uh5JUyEaIHTXVOHslFoLNAKqwI= -github.com/cloudflare/circl v1.3.8/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= +github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= -github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getsentry/sentry-go v0.28.1 h1:zzaSm/vHmGllRM6Tpx1492r0YDzauArdBfkJRtY6P5k= +github.com/getsentry/sentry-go v0.28.1/go.mod h1:1fQZ+7l7eeJ3wYi82q5Hg8GqAPgefRq+FP/QhafYVgg= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= +github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= +github.com/goccy/go-json v0.9.11 h1:/pAaQDLHEoCq/5FFmSKBswWmK6H0e8g4159Kc/X/nqk= +github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/pprof v0.0.0-20240430035430-e4905b036c4e h1:RsXNnXE59RTt8o3DcA+w7ICdRfR2l+Bb5aE0YMpNTO8= -github.com/google/pprof v0.0.0-20240430035430-e4905b036c4e/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0 h1:e+8XbKB6IMn8A4OAyZccO4pYfB3s7bt6azNIPE7AnPg= +github.com/google/pprof v0.0.0-20240625030939-27f56978b8b0/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/imroc/req/v3 v3.43.3 h1:WdZhpUev9THtuwEZsW2LOYacl12fm7IkB7OgACv40+k= -github.com/imroc/req/v3 v3.43.3/go.mod h1:SQIz5iYop16MJxbo8ib+4LnostGCok8NQf8ToyQc2xA= -github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= -github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/onsi/ginkgo/v2 v2.17.2 h1:7eMhcy3GimbsA3hEnVKdw/PQM9XN9krpKVXsZdph0/g= -github.com/onsi/ginkgo/v2 v2.17.2/go.mod h1:nP2DPOQoNsQmsVyv5rDA8JkXQoCs6goXIvr/PRJ1eCc= -github.com/onsi/gomega v1.33.0 h1:snPCflnZrpMsy94p4lXVEkHo12lmPnc3vY5XBbreexE= -github.com/onsi/gomega v1.33.0/go.mod h1:+925n5YtiFsLzzafLUHzVMBpvvRAzrydIBiSIxjX3wY= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/imroc/req/v3 v3.43.7 h1:dOcNb9n0X83N5/5/AOkiU+cLhzx8QFXjv5MhikazzQA= +github.com/imroc/req/v3 v3.43.7/go.mod h1:SQIz5iYop16MJxbo8ib+4LnostGCok8NQf8ToyQc2xA= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= +github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= +github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.33.1 h1:dsYjIxxSR755MDmKVsaFQTE22ChNBcuuTWgkUDSubOk= +github.com/onsi/gomega v1.33.1/go.mod h1:U4R44UsT+9eLIaYRB2a5qajjtQYn0hauxvRm16AVYg0= github.com/open-spaced-repetition/go-fsrs v1.2.1 h1:vY1hSQ3gvHtfnw8ahylcZyyqusKWDkWCd1+ca4lZoSc= github.com/open-spaced-repetition/go-fsrs v1.2.1/go.mod h1:WpbNs4TTKZChOHFO+ME0B9femUVZsepFT5mhAioszRg= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= @@ -41,43 +79,79 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/quic-go/qpack v0.4.0 h1:Cr9BXA1sQS2SmDUWjSofMPNKmvF6IiIfDRmgU0w1ZCo= github.com/quic-go/qpack v0.4.0/go.mod h1:UZVnYIfi5GRk+zI9UMaCPsmZ2xKJP7XBUvVyT1Knj9A= -github.com/quic-go/quic-go v0.43.0 h1:sjtsTKWX0dsHpuMJvLxGqoQdtgJnbAPWY+W+5vjYW/g= -github.com/quic-go/quic-go v0.43.0/go.mod h1:132kz4kL3F9vxhW3CtQJLDVwcFe5wdWeJXXijhsO57M= -github.com/refraction-networking/utls v1.6.4 h1:aeynTroaYn7y+mFtqv8D0bQ4bw0y9nJHneGxJ7lvRDM= -github.com/refraction-networking/utls v1.6.4/go.mod h1:2VL2xfiqgFAZtJKeUTlf+PSYFs3Eu7km0gCtXJ3m8zs= -github.com/siyuan-note/filelock v0.0.0-20240419132904-2fbfe64f1939 h1:RQApoGXu6E5KX2Deb05c27bfJiA/Yzc34BKx+hRnCtM= -github.com/siyuan-note/filelock v0.0.0-20240419132904-2fbfe64f1939/go.mod h1:0MqIa22SVvzvdI696uQAxvNmfOpD5gBh/i/wK4BECQo= -github.com/siyuan-note/httpclient v0.0.0-20240429013218-3caa1f89f9ed h1:QBPxwSsWnAxxnEKYn3ycvFxj61vfpD8q4zTtLH9iN9w= -github.com/siyuan-note/httpclient v0.0.0-20240429013218-3caa1f89f9ed/go.mod h1:qQsrqhPrCPJwjpHQetGcLCBh7bBCpoJ7P1uXoXJiClY= -github.com/siyuan-note/logging v0.0.0-20231208035918-61f884c854f0 h1:+XjUr9UMXsczdO2bGA72p/k9wa2ShPb8ybi7CDBJ7HQ= -github.com/siyuan-note/logging v0.0.0-20231208035918-61f884c854f0/go.mod h1:6mRFtAAvYPn3cDzqvyv+t8BVPGqpONDMMb5ywOhY1D4= +github.com/quic-go/quic-go v0.45.1 h1:tPfeYCk+uZHjmDRwHHQmvHRYL2t44ROTujLeFVBmjCA= +github.com/quic-go/quic-go v0.45.1/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI= +github.com/refraction-networking/utls v1.6.6 h1:igFsYBUJPYM8Rno9xUuDoM5GQrVEqY4llzEXOkL43Ig= +github.com/refraction-networking/utls v1.6.6/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/siyuan-note/filelock v0.0.0-20240629145917-7545564cf0a4 h1:Mk3712ngEHghxSqdyaCP2G+fI27kCTDYJLOSB22zb1c= +github.com/siyuan-note/filelock v0.0.0-20240629145917-7545564cf0a4/go.mod h1:ftS1FyVHGGlyqXgGOVux4JLv8b2fXN9uJSpTRzCvjc8= +github.com/siyuan-note/httpclient v0.0.0-20240626145026-29585d45a51c h1:E6W4x2GL+7jHsfkPEldvosWNl7hxtRHJjgGVhHHywn4= +github.com/siyuan-note/httpclient v0.0.0-20240626145026-29585d45a51c/go.mod h1:RZ0DzDpOBiKMKOTMI2OdSNGBo7Fn6za6Q+ornWF0x+Q= +github.com/siyuan-note/logging v0.0.0-20240505035402-6430d57006a2 h1:/2+tlOThVB86RxSLeW0JFw2ISUrH2ZFRg15ULGAUGAE= +github.com/siyuan-note/logging v0.0.0-20240505035402-6430d57006a2/go.mod h1:3Osd2/nwzXZFl6ZcDE4hA0HD83Wyv1fds47nVuapyOM= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= -github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= -github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ= go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f h1:99ci1mjWVBWwJiEKYY6jWa4d2nTQVIEhZIptnrVb1XY= -golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY= +golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.20.0 h1:hz/CVckiOxybQvFw6h7b/q80NTr9IUQb4s1IIzW7KNY= -golang.org/x/tools v0.20.0/go.mod h1:WvitBU7JJf6A4jOdg4S1tviW9bhUxkgeCui/0JHctQg= +golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= +golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= +lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= +modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q= +modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y= +modernc.org/ccgo/v3 v3.17.0 h1:o3OmOqx4/OFnl4Vm3G8Bgmqxnvxnh0nbxeT5p/dWChA= +modernc.org/ccgo/v3 v3.17.0/go.mod h1:Sg3fwVpmLvCUTaqEUjiBDAvshIaKDB0RXaf+zgqFu8I= +modernc.org/libc v1.52.1 h1:uau0VoiT5hnR+SpoWekCKbLqm7v6dhRL3hI+NQhgN3M= +modernc.org/libc v1.52.1/go.mod h1:HR4nVzFDSDizP620zcMCgjb1/8xk2lg5p/8yjfGv1IQ= +modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= +modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= +modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= +modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= +modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= +modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= +modernc.org/sqlite v1.20.4 h1:J8+m2trkN+KKoE7jglyHYYYiaq5xmz2HoHJIiBlRzbE= +modernc.org/sqlite v1.20.4/go.mod h1:zKcGyrICaxNTMEHSr1HQ2GUraP0j+845GYw37+EyT6A= +modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= +modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978 h1:bvLlAPW1ZMTWA32LuZMBEGHAUOcATZjzHcotf3SWweM= +xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE= +xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU= +xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw= diff --git a/review_info.go b/review_info.go new file mode 100644 index 0000000..69e212e --- /dev/null +++ b/review_info.go @@ -0,0 +1,11 @@ +package riff + +type ReviewInfo struct { + BaseCard `xorm:"extends"` + BaseCardSource `xorm:"extends"` +} + +func (ri *ReviewInfo) ToCard() (card Card) { + card = &ri.BaseCard + return +} diff --git a/riff.go b/riff.go new file mode 100644 index 0000000..d2c0438 --- /dev/null +++ b/riff.go @@ -0,0 +1,822 @@ +package riff + +import ( + "encoding/json" + "fmt" + "io/fs" + "math/rand" + "os" + "path" + "path/filepath" + "reflect" + "sync" + "time" + + "github.com/88250/gulu" + _ "github.com/mattn/go-sqlite3" + "github.com/open-spaced-repetition/go-fsrs" + + "github.com/siyuan-note/filelock" + "github.com/siyuan-note/logging" + "github.com/syndtr/goleveldb/leveldb/errors" + "xorm.io/xorm" +) + +type Riff interface { + Query() []map[string]interface{} + QueryCard() []Card + + // 底层 xorm DB的 Find 加锁包装 + Find(beans interface{}, condiBeans ...interface{}) error + SetParams(algo Algo, params interface{}) + AddDeck(deck Deck) (newDeck Deck, err error) + AddCardSource(cardSources []CardSource) (cardSourceList []CardSource, err error) + AddCard(cards []Card) (cardList []Card, err error) + Load(savePath string) (err error) + //WaitLoad会等待至从磁盘加载完成 + WaitLoad() (err error) + Save(path string) error + Due() []ReviewInfo + // Review(card Card, rating Rating, RequestRetention float64) + GetCardsByBlockIDs(blockIDs []string) (ret []ReviewInfo) + Review(cardID string, rating Rating) + CountCards() int + GetBlockIDs() (ret []string) + + //SetDeck设置Deck, 无锁 + SetDeck(deck Deck) (err error) + + //GetDeck获取Deck, 无锁 + GetDeck(did string) (deck Deck, err error) + + //SetCardSource设置CardSource, 无锁 + SetCardSource(cs CardSource) (err error) + + //GetCardSource获取CardSource, 无锁 + GetCardSource(csid string) (cs CardSource, err error) + + //SetCard设置Card, 无锁 + SetCard(card Card) (err error) + + //GetCard获取Card, 无锁 + GetCard(cardID string) (card Card, err error) +} + +type BaseRiff struct { + db *xorm.Engine + GlobalRequestRetention float64 + MaxRequestRetention float64 + MinRequestRetention float64 + lock *sync.Mutex + load *sync.Mutex + startTime time.Time + ParamsMap map[Algo]interface{} + deckMap map[string]Deck + cardSourceMap map[string]CardSource + cardMap map[string]Card +} + +func NewBaseRiff() Riff { + // orm, err := xorm.NewEngine("sqlite", ":memory:?_pragma=foreign_keys(1)") + orm, err := xorm.NewEngine("sqlite3", ":memory:?mode=memory&cache=shared&loc=auto") + orm.Exec(` + create view block_id_to_card_source as + select c_s_i_d,value + from + base_card_source , json_each(base_card_source.block_i_ds)`) + if err != nil { + return &BaseRiff{} + } + orm.Sync(new(BaseCard), new(BaseCardSource), new(BaseDeck), new(BaseHistory), new(ReviewLog)) + riff := BaseRiff{ + db: orm, + GlobalRequestRetention: 0.900, + MaxRequestRetention: 0.999, + MinRequestRetention: 0.500, + lock: &sync.Mutex{}, + load: &sync.Mutex{}, + startTime: time.Now(), + ParamsMap: map[Algo]interface{}{}, + deckMap: map[string]Deck{}, + cardSourceMap: map[string]CardSource{}, + cardMap: map[string]Card{}, + } + return &riff +} + +func (br *BaseRiff) SetParams(algo Algo, params interface{}) { + br.ParamsMap[algo] = params +} + +func (br *BaseRiff) Query() []map[string]interface{} { + // 空实现 + return nil +} + +func (br *BaseRiff) QueryCard() []Card { + // 空实现 + return nil +} + +func (br *BaseRiff) AddDeck(deck Deck) (newDeck Deck, err error) { + br.lock.Lock() + defer br.lock.Unlock() + br.deckMap[deck.GetDID()] = deck + _, err = br.db.Insert(deck) + newDeck = deck + return +} + +func (br *BaseRiff) batchCheck(table, field string, IDs []string) (existMap map[string]bool, err error) { + + br.lock.Lock() + defer br.lock.Unlock() + + const MAX_BATCH = 5000 + + existMap = map[string]bool{} + existsIDs := make([]string, 0) + IDs = gulu.Str.RemoveDuplicatedElem(IDs) + IDsLength := len(IDs) + + for i := 0; i < IDsLength; i += MAX_BATCH { + end := i + MAX_BATCH + if end > IDsLength { + end = IDsLength + } + subIDs := IDs[i:end] + err = br.db.Table(table). + In(field, subIDs). + Cols(field). + Find(&existsIDs) + } + + for _, existsID := range existsIDs { + existMap[existsID] = true + } + for _, ID := range IDs { + if !existMap[ID] { + err = errors.New(fmt.Sprintf("no exit field in %s : %s = %s", table, field, ID)) + return + } + } + + return +} + +func (br *BaseRiff) batchInsert(rowSlice interface{}) (err error) { + // 获取数据类型 + + br.lock.Lock() + defer br.lock.Unlock() + + t := reflect.TypeOf(rowSlice) + + // 检查是否是切片类型 + if t.Kind() != reflect.Slice { + fmt.Println("Not a slice") + return + } + sliceValue := reflect.Indirect(reflect.ValueOf(rowSlice)) + Len := sliceValue.Len() + session := br.db.NewSession() + defer session.Close() + session.Begin() + for i := 0; i < Len; i++ { + _, err = session.Insert(sliceValue.Index(i).Interface()) + if err != nil { + fmt.Printf("error on insert CardSource %s \n", err) + continue + } + } + err = session.Commit() + return +} + +func (br *BaseRiff) AddCardSource(cardSources []CardSource) (cardSourceList []CardSource, err error) { + + DIDs := make([]string, 0) + existsCardSourceList := make([]CardSource, 0) + for index := range cardSources { + DIDs = append(DIDs, cardSources[index].GetDIDs()...) + } + + existCSIDMap, err := br.batchCheck( + "base_deck", + "d_i_d", + DIDs, + ) + + for _, cardSource := range cardSources { + DIDs := cardSource.GetDIDs() + unExist := 0 + for _, DID := range DIDs { + if !existCSIDMap[DID] { + unExist += 1 + } + } + if unExist == 0 { + existsCardSourceList = append(existsCardSourceList, cardSource) + // 添加到 cardSourceMap + br.cardSourceMap[cardSource.GetCSID()] = cardSource + } + } + + br.batchInsert(existsCardSourceList) + + return +} + +func (br *BaseRiff) AddCard(cards []Card) (cardList []Card, err error) { + // 空实现 + // start := time.Now() + + CSIDs := make([]string, 0) + existsCardList := make([]Card, 0) + for index := range cards { + cards[index].MarshalImpl() + CSIDs = append(CSIDs, cards[index].GetCSID()) + } + + existCSIDMap, err := br.batchCheck( + "base_card_source", + "c_s_i_d", + CSIDs, + ) + + for _, card := range cards { + if existCSIDMap[card.GetCSID()] { + br.cardMap[card.ID()] = card + existsCardList = append(existsCardList, card) + } + } + + br.batchInsert(existsCardList) + + return +} + +func saveData(data interface{}, suffix SaveExt, saveDirPath string) (err error) { + byteData, err := json.Marshal(data) + if err != nil { + logging.LogErrorf("marshal logs failed: %s", err) + return + } + savePath := path.Join(saveDirPath, "siyuan"+string(suffix)) + err = filelock.WriteFile(savePath, byteData) + if err != nil { + logging.LogErrorf("write riff file failed: %s", err) + return + } + return +} + +func (br *BaseRiff) Find(beans interface{}, condiBeans ...interface{}) error { + br.lock.Lock() + defer br.lock.Unlock() + err := br.db.Find(beans, condiBeans...) + return err +} + +func (br *BaseRiff) Get(beans ...interface{}) error { + br.lock.Lock() + defer br.lock.Unlock() + _, err := br.db.Get(beans...) + return err +} + +func (br *BaseRiff) Save(path string) (err error) { + decks := make([]BaseDeck, 0) + cardSources := make([]BaseCardSource, 0) + cards := make([]BaseCard, 0) + + for _, deck := range br.deckMap { + decks = append(decks, *(deck.(*BaseDeck))) + } + for _, cardSource := range br.cardSourceMap { + cardSources = append(cardSources, *(cardSource.(*BaseCardSource))) + } + for _, card := range br.cardMap { + cards = append(cards, *(card.(*BaseCard))) + } + + if !gulu.File.IsDir(path) { + if err = os.MkdirAll(path, 0755); nil != err { + return + } + } + err = saveData(decks, DeckExt, path) + if err != nil { + fmt.Printf("err in save riff data: %s \n", err) + } + err = saveData(cardSources, CardSourceExt, path) + if err != nil { + fmt.Printf("err in save riff data: %s \n", err) + } + err = saveData(cards, CardExt, path) + if err != nil { + fmt.Printf("err in save riff data: %s \n", err) + } + + err = br.SaveHistory(path) + + return +} + +func saveHistoryData(data interface{}, suffix SaveExt, saveDirPath string, time time.Time) (err error) { + byteData, err := json.Marshal(data) + if err != nil { + logging.LogErrorf("marshal logs failed: %s", err) + return + } + yyyyMMddHHmmss := time.Format("2006-01-02-15_04_05") + savePath := path.Join(saveDirPath, yyyyMMddHHmmss+string(suffix)) + err = filelock.WriteFile(savePath, byteData) + if err != nil { + return + } + return +} + +func (br *BaseRiff) SaveHistory(path string) (err error) { + historys := make([]BaseHistory, 0) + reviewLogs := make([]ReviewLog, 0) + err = br.Find(&historys) + if err != nil { + return + } + + for i := range historys { + historys[i].UnmarshalImpl() + } + + err = br.Find(&reviewLogs) + if err != nil { + return + } + err = saveHistoryData(historys, HistoryExt, path, br.startTime) + if err != nil { + logging.LogErrorf("write history file failed: %s", err) + } + err = saveHistoryData(reviewLogs, reviewLogExt, path, br.startTime) + if err != nil { + logging.LogErrorf("write log file failed: %s", err) + } + + if err != nil { + return + } + return +} + +func (br *BaseRiff) LoadHistory(savePath string) (err error) { + if !gulu.File.IsDir(savePath) { + return errors.New("no a save path") + } + + totalHistory := make([]History, 0) + totalReviewLog := make([]ReviewLog, 0) + + filelock.Walk(savePath, func(walkPath string, info fs.FileInfo, err error) (reErr error) { + if info.IsDir() { + return + } + ext := filepath.Ext(walkPath) + data, reErr := filelock.ReadFile(walkPath) + switch SaveExt(ext) { + + case HistoryExt: + historys := make([]BaseHistory, 0) + json.Unmarshal(data, &historys) + for _, history := range historys { + copy := history + totalHistory = append(totalHistory, ©) + } + case reviewLogExt: + reviewLog := make([]ReviewLog, 0) + json.Unmarshal(data, &reviewLog) + totalReviewLog = append(totalReviewLog, reviewLog...) + } + + return + }) + br.batchInsert(totalHistory) + br.batchInsert(totalReviewLog) + return +} + +func (br *BaseRiff) Load(savePath string) (err error) { + br.load.Lock() + defer br.load.Unlock() + + if !gulu.File.IsDir(savePath) { + return errors.New("no a save path") + } + totalDecks := make([]Deck, 0) + totalCards := make([]Card, 0) + totalCardSources := make([]CardSource, 0) + + filelock.Walk(savePath, func(walkPath string, info fs.FileInfo, err error) (reErr error) { + if info.IsDir() { + return + } + ext := filepath.Ext(walkPath) + // 后期性能改进点:把读文件位置后移 + data, reErr := filelock.ReadFile(walkPath) + switch SaveExt(ext) { + + case DeckExt: + decks := make([]BaseDeck, 0) + json.Unmarshal(data, &decks) + for _, deck := range decks { + copy := deck + totalDecks = append(totalDecks, ©) + } + + case CardExt: + cards := make([]BaseCard, 0) + json.Unmarshal(data, &cards) + for _, card := range cards { + copy := card + totalCards = append(totalCards, ©) + } + + case CardSourceExt: + cardSources := make([]BaseCardSource, 0) + json.Unmarshal(data, &cardSources) + for _, cardSource := range cardSources { + copy := cardSource + totalCardSources = append(totalCardSources, ©) + } + } + + return + }) + for _, deck := range totalDecks { + br.AddDeck(deck) + } + + br.AddCardSource(totalCardSources) + br.AddCard(totalCards) + + go br.LoadHistory(savePath) + return + +} + +func (br *BaseRiff) WaitLoad() (err error) { + br.load.Lock() + defer br.load.Unlock() + return +} + +func (br *BaseRiff) Due() (ret []ReviewInfo) { + now := time.Now() + + qr := br.newReviewInfoQuery() + qr, err := qr.ByDue(now) + if err != nil { + return + } + ret, err = qr.Query() + if err != nil { + ret = make([]ReviewInfo, 0) + return + } + return +} + +func (br *BaseRiff) GetCardsByBlockIDs(blockIDs []string) (ret []ReviewInfo) { + + qr := br.newReviewInfoQuery() + qr, err := qr.ByBlockIDs(blockIDs) + + if err != nil { + fmt.Println(err) + } + + ret, err = qr.Query() + + if err != nil { + fmt.Println(err) + } + + return +} + +func (br *BaseRiff) innerReview(card Card, rating Rating, RequestRetention float64) { + br.lock.Lock() + defer br.lock.Unlock() + now := time.Now() + + history := NewBaseHistory(card) + reviewlog := NewReviewLog(history, rating) + _, err := br.db.Insert(history) + if err != nil { + logging.LogErrorf("error insert history %s \n", err) + } + _, err = br.db.Insert(reviewlog) + if err != nil { + logging.LogErrorf("error insert reviewLog %s \n", err) + } + + switch card.GetAlgo() { + case AlgoFSRS: + fsrsCard := (card.Impl()).(fsrs.Card) + params := br.ParamsMap[AlgoFSRS].(fsrs.Parameters) + params.RequestRetention = RequestRetention + schedulingInfo := params.Repeat(fsrsCard, now)[rating.ToFsrs()] + newCard := schedulingInfo.Card + card.SetDue(newCard.Due) + card.SetImpl(newCard) + } + + RatingMap := map[Rating]State{} + preState := card.GetState() + switch preState { + case New: + RatingMap[Again] = Learning + RatingMap[Hard] = Learning + RatingMap[Good] = Learning + RatingMap[Easy] = Review + + case Learning, Relearning: + RatingMap[Again] = preState + RatingMap[Hard] = preState + RatingMap[Good] = Review + RatingMap[Easy] = Review + case Review: + RatingMap[Again] = Relearning + RatingMap[Hard] = Review + RatingMap[Good] = Review + RatingMap[Easy] = Review + } + currentState := RatingMap[rating] + card.SetState(currentState) + card.SetReps(card.GetReps() + 1) + if currentState == Relearning { + card.SetLapses(card.GetLapses() + 1) + } + + br.SetCard(card) +} + +func (br *BaseRiff) Review(cardID string, rating Rating) { + br.lock.Lock() + card, err := br.GetCard(cardID) + br.lock.Unlock() + if err != nil { + return + } + + RequestRetention := br.getRequestRetention((card)) + br.innerReview(card, rating, RequestRetention) + +} +func (br *BaseRiff) getRequestRetention(card Card) float64 { + priority := card.GetPriority() + requestRetention := br.GlobalRequestRetention + switch { + case priority >= 0 && priority <= 0.5: + requestRetention = (br.GlobalRequestRetention - br.MinRequestRetention) / (0.5 - 0) * (priority - 0) + case priority > 0.5 && priority <= 1: + requestRetention = (br.MaxRequestRetention - br.GlobalRequestRetention) / (1 - 0.5) * (priority - 0.5) + } + return requestRetention +} + +func (br *BaseRiff) CountCards() int { + // 空实现 + return 0 +} + +func (br *BaseRiff) GetBlockIDs() (ret []string) { + // 空实现 + return nil +} + +func (br *BaseRiff) SetDeck(deck Deck) (err error) { + br.deckMap[deck.GetDID()] = deck + _, err = br.db.Where("d_i_d = ?", deck.GetDID()).Update(deck) + return +} + +func (br *BaseRiff) GetDeck(did string) (deck Deck, err error) { + deck, exist := br.deckMap[did] + if !exist { + err = fmt.Errorf("deck %s is not exist", did) + } + return +} + +func (br *BaseRiff) SetCardSource(cs CardSource) (err error) { + br.cardSourceMap[cs.GetCSID()] = cs + _, err = br.db.Where("c_s_i_d = ?", cs.GetCSID()).Update(cs) + return +} + +func (br *BaseRiff) GetCardSource(csid string) (cs CardSource, err error) { + cs, exist := br.cardSourceMap[csid] + if !exist { + err = fmt.Errorf("CardSource %s is not exist", csid) + } + return +} + +func (br *BaseRiff) SetCard(card Card) (err error) { + br.cardMap[card.ID()] = card + card.MarshalImpl() + _, err = br.db.Where("c_i_d = ?", card.ID()).Update(card) + if err != nil { + fmt.Printf("update card err:%s\n", err) + } + + return +} + +func (br *BaseRiff) GetCard(cardID string) (card Card, err error) { + card, exist := br.cardMap[cardID] + if !exist { + err = fmt.Errorf("card %s is not exist", cardID) + } + return +} + +// QueryReviewInfo 是对 ReviewInfo 搜索查询操作的抽象和封装 +type QueryReviewInfo interface { + ByBlockIDs(blockIDs []string) (ret QueryReviewInfo, err error) + ByDue(dueTime time.Time) (ret QueryReviewInfo, err error) + Query() (ret []ReviewInfo, err error) + Conut() (ret int64, err error) +} +type baseQueryReviewInfo struct { + br *BaseRiff + db *xorm.Engine + sission *xorm.Session + lock *sync.Mutex +} + +func (br *BaseRiff) newReviewInfoQuery() (ret QueryReviewInfo) { + br.lock.Lock() + defer br.lock.Unlock() + session := br.db.Table("base_card"). + Join("inner", "base_card_source", "base_card.c_s_i_d = base_card_source.c_s_i_d") + ret = &baseQueryReviewInfo{ + br: br, + db: br.db, + sission: session, + lock: br.lock, + } + return +} + +func (qr *baseQueryReviewInfo) ByBlockIDs(blockIDs []string) (ret QueryReviewInfo, err error) { + qr.lock.Lock() + defer qr.lock.Unlock() + + ret = qr + + existSet := map[string]bool{} + csidMap := map[string]bool{} + csidList := make([]string, 0) + + for _, blockID := range blockIDs { + existSet[blockID] = true + } + + for _, cs := range qr.br.cardSourceMap { + csBlockIDs := cs.GetBlockIDs() + csid := cs.GetCSID() + for _, csBlockID := range csBlockIDs { + if existSet[csBlockID] { + csidMap[csid] = true + } + } + } + + for csid := range csidMap { + csidList = append(csidList, csid) + } + + queryCsidLen := len(csidList) + if queryCsidLen > MAX_QUERY_PARAMS { + queryCsidLen = MAX_QUERY_PARAMS + } + queryCsidList := csidList[0:queryCsidLen] + + qr.sission.In("base_card_source.c_s_i_d", queryCsidList) + return +} + +func (qr *baseQueryReviewInfo) ByDue(dueTime time.Time) (ret QueryReviewInfo, err error) { + qr.lock.Lock() + defer qr.lock.Unlock() + + ret = qr + + qr.sission.Where("base_card.due < ?", dueTime) + return +} + +func (qr *baseQueryReviewInfo) Query() (ret []ReviewInfo, err error) { + qr.lock.Lock() + defer qr.lock.Unlock() + ret = make([]ReviewInfo, 0) + reviewInfoIDList, err := qr.sission. + Cols("base_card.c_i_d", "base_card_source.c_s_i_d"). + QueryString() + if err != nil { + return + } + for _, item := range reviewInfoIDList { + card, err := qr.br.GetCard(item["c_i_d"]) + if err != nil { + continue + } + cs, err := qr.br.GetCardSource(item["c_s_i_d"]) + if err != nil { + continue + } + ret = append(ret, ReviewInfo{ + BaseCard: *(card.(*BaseCard)), + BaseCardSource: *(cs.(*BaseCardSource)), + }) + } + + return +} + +func (qr *baseQueryReviewInfo) Conut() (ret int64, err error) { + qr.lock.Lock() + defer qr.lock.Unlock() + ri := new(ReviewInfo) + ret, err = qr.sission.Count(ri) + return +} + +// Rating 描述了闪卡复习的评分。 +type Rating int8 + +const ( + Again Rating = iota + 1 // 完全不会,必须再复习一遍 + Hard // 有点难 + Good // 一般 + Easy // 很容易 +) + +var RatingToFsrs = map[Rating]fsrs.Rating{ + Again: fsrs.Again, + Hard: fsrs.Hard, + Good: fsrs.Good, + Easy: fsrs.Easy, +} + +func (rate Rating) ToFsrs() fsrs.Rating { + return RatingToFsrs[rate] +} + +// Algo 描述了闪卡复习算法的名称。 +type Algo string + +const ( + AlgoFSRS Algo = "fsrs" + AlgoSM2 Algo = "sm2" +) + +type SaveExt string + +const ( + CardExt = ".cards" + CardSourceExt = ".cardSources" + DeckExt = ".decks" + HistoryExt = ".history" + reviewLogExt = ".revlog" +) + +// State 描述了闪卡的状态。 +type State int8 + +const ( + New State = iota + Learning + Review + Relearning +) + +const builtInDeck = "20240718214745-q7ocvvi" + +const MAX_QUERY_PARAMS = 3_0000 + +func newID() string { + now := time.Now() + return now.Format("20060102150405") + "-" + randStr(7) +} + +func randStr(length int) string { + letter := []rune("abcdefghijklmnopqrstuvwxyz0123456789") + b := make([]rune, length) + for i := range b { + b[i] = letter[rand.Intn(len(letter))] + } + return string(b) +} diff --git a/riff_test.go b/riff_test.go new file mode 100644 index 0000000..a1c7372 --- /dev/null +++ b/riff_test.go @@ -0,0 +1,433 @@ +// Riff - Spaced repetition. +// Copyright (c) 2022-present, b3log.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 . + +package riff + +import ( + "fmt" + "math/rand" + "os" + "reflect" + "testing" + "time" + + "github.com/open-spaced-repetition/go-fsrs" +) + +type timeTicker struct { + totalTicker map[string]time.Time +} + +func newTimeTicker() *timeTicker { + return &timeTicker{ + totalTicker: map[string]time.Time{}, + } +} + +func (tt *timeTicker) start(task string) { + tt.totalTicker[task] = time.Now() +} +func (tt *timeTicker) log(task string) { + since := time.Since(tt.totalTicker[task]) + fmt.Printf("task %s use time :%s\n", task, since) +} + +func checkIDList(IDList []string, data interface{}, dataGetter func(item interface{}) string) (err error) { + queryIDs := map[string]bool{} + v := reflect.ValueOf(data) + + for i := 0; i < v.Len(); i++ { + id := dataGetter(v.Index(i).Interface()) + queryIDs[id] = true + } + for _, ID := range IDList { + if !queryIDs[ID] { + err = fmt.Errorf("dont have id of %s", ID) + return + } + } + return +} + +func randomSubset[T any](slice []T, size int) ([]T, error) { + if size > len(slice) { + return nil, fmt.Errorf("requested subset size is larger than the slice size") + } + + // Create a copy of the original slice to avoid modifying it + + subset := make([]T, size) + // copy(subset, slice) + sourceLen := len(slice) + + for i := range subset { + subset[i] = slice[rand.Intn(sourceLen)] + } + + // Return the first `size` elements + return subset[:size], nil +} + +// 检查功能是否正确实现 +func TestFunction(t *testing.T) { + const saveDir = "testdata" + const RequestRetention = 0.95 + const cardSourceNum = 200 + const blocksNum = 40 + const preSourceCardNum = 3 + const totalCardNum = cardSourceNum * preSourceCardNum + + os.MkdirAll(saveDir, 0755) + defer os.RemoveAll(saveDir) + + blocksIDs := []string{} + blockIDsCIDMap := make(map[string][]string, 0) + + for i := 0; i < blocksNum; i++ { + blocksIDs = append(blocksIDs, newID()) + } + + riff := NewBaseRiff() + riff.SetParams(AlgoFSRS, fsrs.DefaultParam()) + deck := DefaultBaseDeck() + csList := []CardSource{} + cardList := []Card{} + csIDList := []string{} + cardIDList := []string{} + + for i := 0; i < cardSourceNum; i++ { + cs := NewBaseCardSource(deck.DID) + + // 放入blockIDs + subBlockIDs, _ := randomSubset(blocksIDs, preSourceCardNum) + cs.BlockIDs = subBlockIDs + // 准备一个临时数组来储存当前blocksIDs对应的cardSourceID + for _, blockID := range subBlockIDs { + blockIDsCIDMap[blockID] = append(blockIDsCIDMap[blockID], cs.CSID) + } + + csList = append(csList, cs) + csIDList = append(csIDList, cs.CSID) + for i := 0; i < preSourceCardNum; i++ { + card := NewBaseCard(cs) + card.UseAlgo(AlgoFSRS) + cardList = append(cardList, card) + cardIDList = append(cardIDList, card.CID) + } + } + + //检查待插入卡片数量 + if len(csIDList) != cardSourceNum { + t.Errorf("add card source error") + } + + if len(cardIDList) != totalCardNum { + t.Errorf("add card error") + } + + riff.AddDeck(deck) + riff.AddCardSource(csList) + riff.AddCard(cardList) + + queryCsList := []BaseCardSource{} + queryCardList := []BaseCard{} + + // 确保cardsource完全插入数据库 + riff.Find(&queryCsList) + if len(queryCsList) != cardSourceNum { + t.Errorf("add CardSource err num %d:%d ", len(queryCsList), cardSourceNum) + } + if err := checkIDList(csIDList, queryCsList, func(item interface{}) string { + return item.(BaseCardSource).CSID + }); err != nil { + t.Errorf("%s", err) + } + + // 确保card完全插入数据库 + riff.Find(&queryCardList) + if len(queryCardList) != totalCardNum { + t.Errorf("add Card err num %d:%d ", len(queryCardList), totalCardNum) + } + + if err := checkIDList(cardIDList, queryCardList, func(item interface{}) string { + return item.(BaseCard).CID + }); err != nil { + t.Errorf("%s", err) + } + + reviewInfoList := riff.Due() + for _, reviewInfo := range reviewInfoList { + riff.Review(reviewInfo.BaseCard.ID(), Again) + } + + reviewInfoList = riff.Due() + for _, reviewInfo := range reviewInfoList { + riff.Review(reviewInfo.BaseCard.ID(), Easy) + } + + newreviewCard := riff.Due() + + if len(newreviewCard) != 0 { + t.Errorf("review error with un review card num :%d\n", len(newreviewCard)) + } + + // 抽取一半的blockIDs检查 + testBlockIDs, _ := randomSubset(blocksIDs, blocksNum/2) + reviewInfoByblocks := riff.GetCardsByBlockIDs(testBlockIDs) + existMap := map[string]bool{} + for _, ri := range reviewInfoByblocks { + existMap[ri.BaseCardSource.CSID] = true + } + for _, blockID := range testBlockIDs { + mapCSIDs := blockIDsCIDMap[blockID] + for _, csid := range mapCSIDs { + if !existMap[csid] { + t.Errorf("blockID %s cardSource %s no call back", blockID, csid) + } + } + } + + riff.Save(saveDir) + + newRiff := NewBaseRiff() + newRiff.SetParams(AlgoFSRS, fsrs.DefaultParam()) + newRiff.Load(saveDir) + + // 检查重新加载后 数据是否恢复 + + queryCsList = []BaseCardSource{} + queryCardList = []BaseCard{} + + // 确保cardsource完全插入数据库 + newRiff.Find(&queryCsList) + if len(queryCsList) != cardSourceNum { + t.Errorf("add CardSource err num %d:%d ", len(queryCsList), cardSourceNum) + } + if err := checkIDList(csIDList, queryCsList, func(item interface{}) string { + return item.(BaseCardSource).CSID + }); err != nil { + t.Errorf("%s", err) + } + + // 确保card完全插入数据库 + newRiff.Find(&queryCardList) + if len(queryCardList) != totalCardNum { + t.Errorf("add Card err num %d:%d ", len(queryCardList), totalCardNum) + } + + if err := checkIDList(cardIDList, queryCardList, func(item interface{}) string { + return item.(BaseCard).CID + }); err != nil { + t.Errorf("%s", err) + } + +} + +// 检查性能参数 +// 10000 cardSOurce, +// 30 card +func TestPerformance(t *testing.T) { + ticker := newTimeTicker() + ticker.start("TestPerformance") + + const saveDir = "testdata" + const RequestRetention = 0.95 + const cardSourceNum = 20000 + const blocksNum = 20000 + const preSourceCardNum = 5 + const totalCardNum = cardSourceNum * preSourceCardNum + + os.MkdirAll(saveDir, 0755) + defer os.RemoveAll(saveDir) + + blocksIDs := []string{} + blockIDsCIDMap := make(map[string][]string, 0) + + for i := 0; i < blocksNum; i++ { + blocksIDs = append(blocksIDs, newID()) + } + + riff := NewBaseRiff() + riff.SetParams(AlgoFSRS, fsrs.DefaultParam()) + deck := DefaultBaseDeck() + + csList := []CardSource{} + cardList := []Card{} + csIDList := []string{} + cardIDList := []string{} + ticker.start("init card and cardsource") + for i := 0; i < cardSourceNum; i++ { + cs := NewBaseCardSource(deck.DID) + + // 放入blockIDs + subBlockIDs, _ := randomSubset(blocksIDs, preSourceCardNum) + cs.BlockIDs = subBlockIDs + // 准备一个临时数组来储存当前blocksIDs对应的cardSourceID + for _, blockID := range subBlockIDs { + blockIDsCIDMap[blockID] = append(blockIDsCIDMap[blockID], cs.CSID) + } + + csList = append(csList, cs) + csIDList = append(csIDList, cs.CSID) + + if i%10 == 0 { + time.Sleep(1 * time.Microsecond) + } + for i := 0; i < preSourceCardNum; i++ { + card := NewBaseCard(cs) + card.UseAlgo(AlgoFSRS) + cardList = append(cardList, card) + cardIDList = append(cardIDList, card.CID) + + } + } + ticker.log("init card and cardsource") + + ticker.start("adddeck") + riff.AddDeck(deck) + ticker.log("adddeck") + + ticker.start("add cardSource") + riff.AddCardSource(csList) + ticker.log("add cardSource") + + ticker.start("add card") + riff.AddCard(cardList) + ticker.log("add card") + + queryCsList := []BaseCardSource{} + queryCardList := []BaseCard{} + + // 确保cardsource完全插入数据库 + riff.Find(&queryCsList) + if len(queryCsList) != cardSourceNum { + t.Errorf("add CardSource err num %d:%d ", len(queryCsList), cardSourceNum) + } + if err := checkIDList(csIDList, queryCsList, func(item interface{}) string { + return item.(BaseCardSource).CSID + }); err != nil { + t.Errorf("%s", err) + } + + // 确保card完全插入数据库 + riff.Find(&queryCardList) + if len(queryCardList) != totalCardNum { + t.Errorf("add Card err num %d:%d ", len(queryCardList), totalCardNum) + } + + if err := checkIDList(cardIDList, queryCardList, func(item interface{}) string { + return item.(BaseCard).CID + }); err != nil { + t.Errorf("%s", err) + } + + ticker.start("query due") + reviewInfoList := riff.Due() + ticker.log("query due") + + fmt.Printf("due card len :%d \n", len(reviewInfoList)) + + ticker.start("review") + for _, reviewInfo := range reviewInfoList { + riff.Review(reviewInfo.BaseCard.ID(), Again) + } + ticker.log("review") + + ticker.start("query due again") + reviewInfoList = riff.Due() + ticker.log("query due again") + + ticker.start("review again") + for _, reviewInfo := range reviewInfoList { + riff.Review(reviewInfo.BaseCard.ID(), Easy) + } + ticker.log("review again") + + ticker.start("query due after review all") + newreviewCard := riff.Due() + ticker.log("query due after review all") + + if len(newreviewCard) != 0 { + t.Errorf("review error with un review card num :%d\n", len(newreviewCard)) + } + + // 抽取一半的blockIDs检查 + ticker.start("get card by blocks") + testBlockIDs, _ := randomSubset(blocksIDs, blocksNum) + reviewInfoByblocks := riff.GetCardsByBlockIDs(testBlockIDs) + fmt.Printf("reviewInfoByblocks len :%d \n", len(reviewInfoByblocks)) + existMap := map[string]bool{} + for _, ri := range reviewInfoByblocks { + existMap[ri.BaseCardSource.CSID] = true + } + for _, blockID := range testBlockIDs { + mapCSIDs := blockIDsCIDMap[blockID] + for _, csid := range mapCSIDs { + if !existMap[csid] { + t.Errorf("blockID %s cardSource %s no call back", blockID, csid) + } + } + } + ticker.log("get card by blocks") + + ticker.start("save") + riff.Save(saveDir) + ticker.log("save") + + ticker.start("load") + newRiff := NewBaseRiff() + newRiff.SetParams(AlgoFSRS, fsrs.DefaultParam()) + go newRiff.Load(saveDir) + + ticker.log("load") + + // 检查重新加载后 数据是否恢复 + + queryCsList = []BaseCardSource{} + queryCardList = []BaseCard{} + + // 确保cardsource完全插入数据库 + ticker.start("query total cardSource") + newRiff.WaitLoad() + newRiff.Find(&queryCsList) + ticker.log("query total cardSource") + + if len(queryCsList) != cardSourceNum { + t.Errorf("add CardSource err num %d:%d ", len(queryCsList), cardSourceNum) + } + if err := checkIDList(csIDList, queryCsList, func(item interface{}) string { + return item.(BaseCardSource).CSID + }); err != nil { + t.Errorf("%s", err) + } + + // 确保card完全插入数据库 + ticker.start("query total card") + newRiff.Find(&queryCardList) + ticker.log("query total card") + + if len(queryCardList) != totalCardNum { + t.Errorf("add Card err num %d:%d ", len(queryCardList), totalCardNum) + } + + if err := checkIDList(cardIDList, queryCardList, func(item interface{}) string { + return item.(BaseCard).CID + }); err != nil { + t.Errorf("%s", err) + } + ticker.log("TestPerformance") + time.Sleep(5 * time.Second) +} diff --git a/store.go b/store.go index 5d9d06d..e435910 100644 --- a/store.go +++ b/store.go @@ -17,10 +17,8 @@ package riff import ( - "math/rand" "path/filepath" "sync" - "time" ) // Store 描述了闪卡存储。 @@ -113,45 +111,3 @@ func (store *BaseStore) GetSaveDir() string { func (store *BaseStore) getMsgPackPath() string { return filepath.Join(store.saveDir, store.id+".cards") } - -// Rating 描述了闪卡复习的评分。 -type Rating int8 - -const ( - Again Rating = iota + 1 // 完全不会,必须再复习一遍 - Hard // 有点难 - Good // 一般 - Easy // 很容易 -) - -// Algo 描述了闪卡复习算法的名称。 -type Algo string - -const ( - AlgoFSRS Algo = "fsrs" - AlgoSM2 Algo = "sm2" -) - -// State 描述了闪卡的状态。 -type State int8 - -const ( - New State = iota - Learning - Review - Relearning -) - -func newID() string { - now := time.Now() - return now.Format("20060102150405") + "-" + randStr(7) -} - -func randStr(length int) string { - letter := []rune("abcdefghijklmnopqrstuvwxyz0123456789") - b := make([]rune, length) - for i := range b { - b[i] = letter[rand.Intn(len(letter))] - } - return string(b) -}