diff --git a/card.go b/card.go index d566fd9..e925a5f 100644 --- a/card.go +++ b/card.go @@ -23,8 +23,32 @@ type Card interface { // ID 返回闪卡 ID。 ID() string - // BlockID 返回闪卡关联的内容块 ID。 - BlockID() string + // CardSourceID 返回关联的cardsource ID + CardSourceID() string + + // GetGroup 返回当前卡片的Group + GetGroup() string + + // SetGroup 设置当前卡片的Group + SetGroup(newGroup string) + + // GetTag 返回当前卡片的Tag + GetTag() string + + // SetTag 设置当前卡片的Tag + SetTag(newTag string) + + // GetSuspend 返回当前卡片的暂停状态 + GetSuspend() bool + + // SwtichSuspend 切换当前卡片的暂停状态 + SwtichSuspend() + + // GetContext 返回当前卡片的Context + GetContext() map[string]string + + // SetContext 使用 value 设置当前卡片的 key + SetContext(key string, value string) // NextDues 返回每种评分对应的下次到期时间。 NextDues() map[Rating]time.Time @@ -59,9 +83,13 @@ type Card interface { // BaseCard 描述了基础的闪卡实现。 type BaseCard struct { - CID string - BID string - NDues map[Rating]time.Time + CID string + SID string + Group string + Tag string + Suspend bool + Context map[string]string + NDues map[Rating]time.Time } func (card *BaseCard) NextDues() map[Rating]time.Time { @@ -76,6 +104,38 @@ func (card *BaseCard) ID() string { return card.CID } -func (card *BaseCard) BlockID() string { - return card.BID +func (card *BaseCard) CardSourceID() string { + return card.SID +} + +func (card *BaseCard) GetGroup() string { + return card.Group +} + +func (card *BaseCard) SetGroup(newGroup string) { + card.Group = newGroup +} + +func (card *BaseCard) GetTag() string { + return card.Tag +} + +func (card *BaseCard) SetTag(newTag string) { + card.Tag = newTag +} + +func (card *BaseCard) GetSuspend() bool { + return card.Suspend +} + +func (card *BaseCard) SwtichSuspend() { + card.Suspend = !card.Suspend +} + +func (card *BaseCard) GetContext() map[string]string { + return card.Context +} + +func (card *BaseCard) SetContext(key string, value string) { + card.Context[key] = value } diff --git a/card_source.go b/card_source.go new file mode 100644 index 0000000..83ebdd3 --- /dev/null +++ b/card_source.go @@ -0,0 +1,109 @@ +package riff + +type CardSource interface { + + // CardSource的ID + SourceID() string + + // 返回类型 + CardType() CardType + + // 返回卡源 Data + GetSourceData() string + + // 更新卡源 Data + SetSourceData(NewData string) + + // 返回关联的CardID list + GetCardIDs() []string + + RemoveCardID(CardID CardID) + + GetCardIDMap() map[string]string + + // 更新关联的CardID list + SetCardIDMap(key CardKey, CardID CardID) + + // 返回卡源的 Context + GetContext() map[string]string + + // 设置卡源的 Context + SetContext(key string, value string) +} + +type CardType string + +type CardKey string + +type CardID string + +// BaseCardSource 描述了卡源的基础实现 +type BaseCardSource struct { + SID string + CType CardType + CIDMap map[CardID]CardKey + Data string + Context map[string]string +} + +const ( + builtInCardType CardType = "siyuan_busic_card" + builtInCardIDMapKey string = "basic_card" + builtInContext string = "blockIDs" +) + +func (source *BaseCardSource) SourceID() string { + return source.SID +} + +func (source *BaseCardSource) CardType() CardType { + return source.CType +} + +func (source *BaseCardSource) GetSourceData() string { + return source.Data +} + +func (source *BaseCardSource) SetSourceData(NewData string) { + source.Data = NewData +} + +func (source *BaseCardSource) GetCardIDs() []string { + var CIDs []string + for CID := range source.CIDMap { + CIDs = append(CIDs, string(CID)) + } + return CIDs +} + +func (source *BaseCardSource) RemoveCardID(cardID CardID) { + + delete(source.CIDMap, cardID) + +} + +func (source *BaseCardSource) GetCardIDMap() map[string]string { + back := make(map[string]string) + for cardID, cardKey := range source.CIDMap { + back[string(cardKey)] = string(cardID) + } + return back +} + +func (source *BaseCardSource) SetCardIDMap(key CardKey, cardID CardID) { + // source.CIDMap[key] = CardID + for CID, CKey := range source.CIDMap { + if CKey == key { + delete(source.CIDMap, CID) + } + } + source.CIDMap[cardID] = key +} + +func (source *BaseCardSource) GetContext() map[string]string { + return source.Context +} + +func (source *BaseCardSource) SetContext(key string, value string) { + source.Context[key] = value +} diff --git a/card_source_store.go b/card_source_store.go new file mode 100644 index 0000000..b4cd794 --- /dev/null +++ b/card_source_store.go @@ -0,0 +1,26 @@ +package riff + +type CardSourceStore interface { + // 添加 CardSource 并添加对应卡片 + AddCardSource(id string, CType CardType, cardIDMap map[string]string) CardSource + + // 更新CardSource CIDMap 对应卡片,使其与 cardIDMap 一致 + // 不存在则新建,已存在则不操作,在 cardIDMap 不存在则删除 + UpdateCardSource(id string, cardIDMap map[string]string) error + + // 通过 CardSourceID 获得 CardSource + GetCardSourceByID(id string) CardSource + + // 通过 Card 获取 cardSource + GetCardSourceByCard(card Card) CardSource + + // 设置 store 内相同 cardSourceID 的 cardsource 为传入的cardSource + SetCardSource(cardSource CardSource) (err error) + + // 通过 id 删除 cardSource + RemoveCardSource(id string) + + Load() + + Save() +} diff --git a/card_source_test.go b/card_source_test.go new file mode 100644 index 0000000..8fdff81 --- /dev/null +++ b/card_source_test.go @@ -0,0 +1,53 @@ +package riff + +import ( + "fmt" + "testing" +) + +func TestCardSource(t *testing.T) { + sid := newID() + cid := newID() + cid2 := newID() + data := "1111" + context := map[string]string{ + "aaa": "bbb", + } + basecardSource := &BaseCardSource{ + SID: sid, + CType: builtInCardType, + CIDMap: map[CardID]CardKey{}, + Data: data, + } + basecardSource.SetCardIDMap("card", CardID(cid)) + var cardSource CardSource = basecardSource + if cardSource.SourceID() != sid { + t.Fatalf("cardSource id [%s] != [%s]", cardSource.SourceID(), sid) + } + if cardSource.CardType() != builtInCardType { + t.Fatalf("cardSource cardType [%s] != [%s]", cardSource.CardType(), builtInCardType) + } + if cardSource.GetSourceData() != data { + t.Fatalf("cardSource SourceData [%s] != [%s]", cardSource.GetSourceData(), data) + } + for key, value := range cardSource.GetContext() { + if context[key] != value { + t.Fatalf("cardSource context key [%s] value [%s] != [%s]", key, value, context[key]) + } + } + if cardSource.GetCardIDMap()["card"] != cid { + t.Fatalf("cardSource CardIDMap key card [%s] != [%s]", cardSource.GetCardIDMap()["card"], cid) + } + cardSource.SetCardIDMap("card", CardID(cid2)) + if cardSource.GetCardIDMap()["card"] != cid2 { + t.Fatalf("cardSource SetCardIDMap key card [%s] != [%s]", cardSource.GetCardIDMap()["card"], cid) + } + if len(cardSource.GetCardIDs()) != 1 { + t.Logf("current cardIDs is [%s]", fmt.Sprint(cardSource.GetCardIDs())) + t.Fatalf("cardSource GetCardIDs cardIDs len [%s] != 1", fmt.Sprint(len(cardSource.GetCardIDs()))) + } + if cardSource.GetCardIDs()[0] != cid2 { + t.Fatalf("cardSource GetCardIDs cardIDs first [%s] != [%s] ", cardSource.GetCardIDs()[0], cid2) + } + +} diff --git a/fsrs_store.go b/fsrs_store.go index 9a588e4..a7f0e92 100644 --- a/fsrs_store.go +++ b/fsrs_store.go @@ -17,6 +17,7 @@ package riff import ( + "errors" "os" "path/filepath" "sort" @@ -34,8 +35,9 @@ import ( type FSRSStore struct { *BaseStore - cards map[string]*FSRSCard - params fsrs.Parameters + cardSources map[string]*BaseCardSource + cards map[string]*FSRSCard + params fsrs.Parameters } func NewFSRSStore(id, saveDir string, requestRetention float64, maximumInterval int, weights string) *FSRSStore { @@ -49,9 +51,10 @@ func NewFSRSStore(id, saveDir string, requestRetention float64, maximumInterval } return &FSRSStore{ - BaseStore: NewBaseStore(id, "fsrs", saveDir), - cards: map[string]*FSRSCard{}, - params: params, + BaseStore: NewBaseStore(id, "fsrs", saveDir), + cardSources: map[string]*BaseCardSource{}, + cards: map[string]*FSRSCard{}, + params: params, } } @@ -59,9 +62,17 @@ func (store *FSRSStore) AddCard(id, blockID string) Card { store.lock.Lock() defer store.lock.Unlock() + cardSourceID := newID() c := fsrs.NewCard() - card := &FSRSCard{BaseCard: &BaseCard{id, blockID, nil}, C: &c} + card := &FSRSCard{BaseCard: &BaseCard{CID: id, SID: cardSourceID}, C: &c} store.cards[id] = card + + cardSource := &BaseCardSource{ + SID: cardSourceID, + CType: builtInCardType, + CIDMap: map[CardID]CardKey{CardID(id): CardKey(builtInCardIDMapKey)}, + Context: map[string]string{builtInContext: blockID}} + store.cardSources[cardSourceID] = cardSource return card } @@ -91,18 +102,53 @@ func (store *FSRSStore) RemoveCard(id string) Card { if nil == card { return nil } + cardSourceID := card.CardSourceID() + cardSource := store.cardSources[cardSourceID] + cardSource.GetCardIDMap() + if nil != cardSource { + cardSource.RemoveCardID(CardID(id)) + } + cardMap := cardSource.GetCardIDMap() + if 0 == len(cardMap) { + delete(store.cardSources, cardSourceID) + } delete(store.cards, id) return card } +// 获取 cardsource 关联的 blockIDs +func getCardSourceRelatedBlockID(cardSource CardSource) (ret []string) { + blockIDsStr := cardSource.GetContext()[builtInContext] + blockIDsStr = strings.Replace(blockIDsStr, " ", "", -1) + ret = strings.Split(blockIDsStr, ",") + ret = gulu.Str.RemoveDuplicatedElem(ret) + return +} + +// 根据 blockID 从 cardSource Context["blockIDs"] 里查询,返回一个不重复的CardIDs +// 这个操作没有加锁,调用者必须自己加锁 +func (store *FSRSStore) getCardIDsByBlockID(blockID string) (ret []string) { + for _, cardSource := range store.cardSources { + cardSourceBlockIDs := getCardSourceRelatedBlockID(cardSource) + if gulu.Str.Contains(blockID, cardSourceBlockIDs) { + for _, cardID := range cardSource.GetCardIDs() { + if _, ok := store.cards[cardID]; ok { + ret = append(ret, cardID) + } + } + } + } + ret = gulu.Str.RemoveDuplicatedElem(ret) + + return +} 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) - } + cardIDs := store.getCardIDsByBlockID(blockID) + for _, cardID := range cardIDs { + ret = append(ret, store.cards[cardID]) } return } @@ -111,11 +157,17 @@ func (store *FSRSStore) GetCardsByBlockIDs(blockIDs []string) (ret []Card) { store.lock.Lock() defer store.lock.Unlock() + var cardIDs []string + blockIDs = gulu.Str.RemoveDuplicatedElem(blockIDs) - for _, card := range store.cards { - if gulu.Str.Contains(card.BlockID(), blockIDs) { - ret = append(ret, card) - } + + for _, blockID := range blockIDs { + cardIDs = append(cardIDs, store.getCardIDsByBlockID(blockID)...) + } + + cardIDs = gulu.Str.RemoveDuplicatedElem(cardIDs) + for _, cardID := range cardIDs { + ret = append(ret, store.cards[cardID]) } return } @@ -124,16 +176,13 @@ 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 { + cards := store.GetCardsByBlockIDs(blockIDs) + for _, card := range cards { c := card.Impl().(*fsrs.Card) if !c.LastReview.IsZero() { continue } - - if gulu.Str.Contains(card.BlockID(), blockIDs) { - ret = append(ret, card) - } + ret = append(ret, card) } return } @@ -142,17 +191,15 @@ 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 { + + cards := store.GetCardsByBlockIDs(blockIDs) + for _, card := range cards { c := card.Impl().(*fsrs.Card) if now.Before(c.Due) { continue } - - if gulu.Str.Contains(card.BlockID(), blockIDs) { - ret = append(ret, card) - } + ret = append(ret, card) } return } @@ -162,8 +209,9 @@ func (store *FSRSStore) GetBlockIDs() (ret []string) { defer store.lock.Unlock() ret = []string{} - for _, card := range store.cards { - ret = append(ret, card.BlockID()) + for _, cardSource := range store.cardSources { + blockIDs := getCardSourceRelatedBlockID(cardSource) + ret = append(ret, blockIDs...) } ret = gulu.Str.RemoveDuplicatedElem(ret) sort.Strings(ret) @@ -246,6 +294,19 @@ func (store *FSRSStore) Load() (err error) { logging.LogErrorf("load cards failed: %s", err) return } + + p = store.getCardSourcesMsgPackPath() + if !filelock.IsExist(p) { + return + } + data, err = filelock.ReadFile(p) + if nil != err { + logging.LogErrorf("load cardsources failed: %s", err) + } + if err = msgpack.Unmarshal(data, &store.cardSources); nil != err { + logging.LogErrorf("load cardsources failed: %s", err) + return + } return } @@ -270,6 +331,17 @@ func (store *FSRSStore) Save() (err error) { logging.LogErrorf("save cards failed: %s", err) return } + + p = store.getCardSourcesMsgPackPath() + data, err = msgpack.Marshal(store.cardSources) + 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 } @@ -314,6 +386,118 @@ func (store *FSRSStore) SaveLog(log *Log) (err error) { return } +func (store *FSRSStore) AddCardSource(id string, CType CardType, cardIDMap map[CardID]CardKey) CardSource { + store.lock.Lock() + defer store.lock.Unlock() + cardSource := &BaseCardSource{ + SID: id, + CType: CType, + CIDMap: cardIDMap} + for cardID := range cardIDMap { + c := fsrs.NewCard() + card := &FSRSCard{BaseCard: &BaseCard{CID: string(cardID)}, C: &c} + store.cards[id] = card + } + store.cardSources[id] = cardSource + return cardSource +} + +func (store *FSRSStore) setNewCardSOurceRelatedCard(cardID, cardSourceID, key string) (err error) { + cardSource, ok := store.cardSources[cardSourceID] + if !ok { + err = errors.New("cardsource not found") + return + } + + c := fsrs.NewCard() + card := &FSRSCard{BaseCard: &BaseCard{CID: cardID, SID: cardSourceID}, C: &c} + store.cards[cardID] = card + cardSource.SetCardIDMap(CardKey(key), CardID(cardID)) + return +} + +func (store *FSRSStore) UpdateCardSource(id string, cardIDMap map[string]string) (err error) { + store.lock.Lock() + defer store.lock.Unlock() + cardSource, ok := store.cardSources[id] + if !ok { + err = errors.New("cardsource not found") + return + } + originCardIDMap := cardSource.GetCardIDMap() + var deleteCardIDs []string + for key := range originCardIDMap { + if cardID, ok := cardIDMap[key]; !ok { + deleteCardIDs = append(deleteCardIDs, cardID) + } + } + for _, cardID := range deleteCardIDs { + store.RemoveCard(cardID) + } + for key, cardID := range cardIDMap { + if originCardID, ok := originCardIDMap[key]; !ok { + err = store.setNewCardSOurceRelatedCard(cardID, id, key) + if nil != err { + return + } + } else { + if originCardID == cardID { + continue + } + store.RemoveCard(originCardID) + err = store.setNewCardSOurceRelatedCard(cardID, id, key) + if nil != err { + return + } + } + } + return +} + +func (store *FSRSStore) GetCardSourceByID(id string) CardSource { + store.lock.Lock() + defer store.lock.Unlock() + ret := store.cardSources[id] + if nil == ret { + return nil + } + return ret +} + +func (store *FSRSStore) GetCardSourceByCard(card Card) CardSource { + store.lock.Lock() + defer store.lock.Unlock() + id := card.CardSourceID() + ret := store.cardSources[id] + if nil == ret { + return nil + } + return ret +} + +func (store *FSRSStore) RemoveCardSource(id string) { + cardSource := store.cardSources[id] + if nil == cardSource { + return + } + cardIDs := cardSource.GetCardIDs() + + for _, cardID := range cardIDs { + store.RemoveCard(cardID) + } +} + +func (store *FSRSStore) SetCardSource(cardSource CardSource) (err error) { + cardIDMap := cardSource.GetCardIDMap() + id := cardSource.SourceID() + err = store.UpdateCardSource(id, cardIDMap) + if nil != err { + return + } + store.cardSources[id] = cardSource.(*BaseCardSource) + return +} + type FSRSCard struct { *BaseCard C *fsrs.Card diff --git a/fsrs_store_test.go b/fsrs_store_test.go index 6edb3ae..2d19bab 100644 --- a/fsrs_store_test.go +++ b/fsrs_store_test.go @@ -17,12 +17,13 @@ package riff import ( - "github.com/88250/gulu" "os" "strings" "testing" "time" + "github.com/88250/gulu" + "github.com/open-spaced-repetition/go-fsrs" ) @@ -33,22 +34,28 @@ const ( ) func TestFSRSStore(t *testing.T) { + const storePath = "testdata" os.MkdirAll(storePath, 0755) defer os.RemoveAll(storePath) store := NewFSRSStore("test-store", storePath, requestRetention, maximumInterval, weights) + // 判断是否实现全部必须接口 + // var _ CardSourceStore = store + var _ Store = store p := fsrs.DefaultParam() start := time.Now() repeatTime := start ids := map[string]bool{} - var firstCardID, firstBlockID, lastCardID, lastBlockID string - max := 10000 + var firstCardID, secondCardID, secondCardSourceID, firstBlockID, lastCardID, lastBlockID string + max := 10 for i := 0; i < max; i++ { id, blockID := newID(), newID() if 0 == i { firstCardID = id firstBlockID = blockID + } else if 1 == i { + secondCardID = id } else if max-1 == i { lastCardID = id lastBlockID = blockID @@ -65,6 +72,19 @@ func TestFSRSStore(t *testing.T) { } repeatTime = start } + secondCard := store.cards[secondCardID] + secondCardSourceID = secondCard.CardSourceID() + secondCardSource, ok := store.cardSources[secondCardSourceID] + if !ok { + t.Fatal("CardSource no add successful") + } + + if secondCardSource.CIDMap == nil { + t.Fatal("CardSource CIDMap field init fail") + } + if !gulu.Str.Contains(secondCardID, secondCardSource.GetCardIDs()) { + t.Fatal("add card no successful add to cardsource") + } cardsLen := len(store.cards) t.Logf("cards len [%d]", cardsLen) if len(ids) != len(store.cards) { @@ -81,6 +101,7 @@ func TestFSRSStore(t *testing.T) { } t.Logf("saved cards [len=%d]", len(store.cards)) + store = NewFSRSStore("test-store", storePath, requestRetention, maximumInterval, weights) if err := store.Load(); nil != err { t.Fatal(err) } @@ -90,6 +111,18 @@ func TestFSRSStore(t *testing.T) { t.Fatal("cards len not equal") } + secondCardSourceID = store.cards[secondCardID].CardSourceID() + + store.RemoveCard(secondCardID) + if cardsLen-1 != len(store.cards) { + t.Fatalf("remove cards len [%d] != [%d]", len(store.cards), cardsLen-1) + } + if cardsLen-1 != len(store.cardSources) { + t.Fatalf("remove cardSources len [%d] != [%d]", len(store.cardSources), cardsLen-1) + } + if _, ok := store.cardSources[secondCardSourceID]; ok { + t.Fatal("remove card related cardSources fail") + } cards := store.GetCardsByBlockID(firstBlockID) if 1 != len(cards) { t.Fatalf("cards by block id [len=%d]", len(cards)) diff --git a/store.go b/store.go index 5d9d06d..fd31dbf 100644 --- a/store.go +++ b/store.go @@ -113,6 +113,9 @@ func (store *BaseStore) GetSaveDir() string { func (store *BaseStore) getMsgPackPath() string { return filepath.Join(store.saveDir, store.id+".cards") } +func (store *BaseStore) getCardSourcesMsgPackPath() string { + return filepath.Join(store.saveDir, store.id+".cardsources") +} // Rating 描述了闪卡复习的评分。 type Rating int8