// Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html mod answering; mod states; use anki_proto::cards; use anki_proto::generic; use anki_proto::scheduler; use anki_proto::scheduler::ComputeFsrsWeightsResponse; use anki_proto::scheduler::ComputeMemoryStateResponse; use anki_proto::scheduler::ComputeOptimalRetentionRequest; use anki_proto::scheduler::ComputeOptimalRetentionResponse; use anki_proto::scheduler::GetOptimalRetentionParametersResponse; use fsrs::FSRSItem; use fsrs::FSRSReview; use fsrs::FSRS; use crate::backend::Backend; use crate::prelude::*; use crate::scheduler::new::NewCardDueOrder; use crate::scheduler::states::CardState; use crate::scheduler::states::SchedulingStates; use crate::stats::studied_today; impl crate::services::SchedulerService for Collection { /// This behaves like _updateCutoff() in older code - it also unburies at /// the start of a new day. fn sched_timing_today(&mut self) -> Result { let timing = self.timing_today()?; self.unbury_if_day_rolled_over(timing)?; Ok(timing.into()) } /// Fetch data from DB and return rendered string. fn studied_today(&mut self) -> Result { self.studied_today().map(Into::into) } /// Message rendering only, for old graphs. fn studied_today_message( &mut self, input: scheduler::StudiedTodayMessageRequest, ) -> Result { Ok(studied_today(input.cards, input.seconds as f32, &self.tr).into()) } fn update_stats(&mut self, input: scheduler::UpdateStatsRequest) -> Result<()> { self.transact_no_undo(|col| { let today = col.current_due_day(0)?; let usn = col.usn()?; col.update_deck_stats(today, usn, input).map(Into::into) }) } fn extend_limits(&mut self, input: scheduler::ExtendLimitsRequest) -> Result<()> { self.transact_no_undo(|col| { let today = col.current_due_day(0)?; let usn = col.usn()?; col.extend_limits( today, usn, input.deck_id.into(), input.new_delta, input.review_delta, ) .map(Into::into) }) } fn counts_for_deck_today( &mut self, input: anki_proto::decks::DeckId, ) -> Result { self.counts_for_deck_today(input.did.into()) } fn congrats_info(&mut self) -> Result { self.congrats_info() } fn restore_buried_and_suspended_cards( &mut self, input: anki_proto::cards::CardIds, ) -> Result { let cids: Vec<_> = input.cids.into_iter().map(CardId).collect(); self.unbury_or_unsuspend_cards(&cids).map(Into::into) } fn unbury_deck( &mut self, input: scheduler::UnburyDeckRequest, ) -> Result { self.unbury_deck(input.deck_id.into(), input.mode()) .map(Into::into) } fn bury_or_suspend_cards( &mut self, input: scheduler::BuryOrSuspendCardsRequest, ) -> Result { let mode = input.mode(); let cids = if input.card_ids.is_empty() { self.storage .card_ids_of_notes(&input.note_ids.into_newtype(NoteId))? } else { input.card_ids.into_newtype(CardId) }; self.bury_or_suspend_cards(&cids, mode).map(Into::into) } fn empty_filtered_deck( &mut self, input: anki_proto::decks::DeckId, ) -> Result { self.empty_filtered_deck(input.did.into()).map(Into::into) } fn rebuild_filtered_deck( &mut self, input: anki_proto::decks::DeckId, ) -> Result { self.rebuild_filtered_deck(input.did.into()).map(Into::into) } fn schedule_cards_as_new( &mut self, input: scheduler::ScheduleCardsAsNewRequest, ) -> Result { let cids = input.card_ids.into_newtype(CardId); self.reschedule_cards_as_new( &cids, input.log, input.restore_position, input.reset_counts, input .context .and_then(scheduler::schedule_cards_as_new_request::Context::from_i32), ) .map(Into::into) } fn schedule_cards_as_new_defaults( &mut self, input: scheduler::ScheduleCardsAsNewDefaultsRequest, ) -> Result { Ok(Collection::reschedule_cards_as_new_defaults( self, input.context(), )) } fn set_due_date( &mut self, input: scheduler::SetDueDateRequest, ) -> Result { let config = input.config_key.map(|v| v.key().into()); let days = input.days; let cids = input.card_ids.into_newtype(CardId); self.set_due_date(&cids, &days, config).map(Into::into) } fn sort_cards( &mut self, input: scheduler::SortCardsRequest, ) -> Result { let cids = input.card_ids.into_newtype(CardId); let (start, step, random, shift) = ( input.starting_from, input.step_size, input.randomize, input.shift_existing, ); let order = if random { NewCardDueOrder::Random } else { NewCardDueOrder::Preserve }; self.sort_cards(&cids, start, step, order, shift) .map(Into::into) } fn reposition_defaults(&mut self) -> Result { Ok(Collection::reposition_defaults(self)) } fn sort_deck( &mut self, input: scheduler::SortDeckRequest, ) -> Result { self.sort_deck_legacy(input.deck_id.into(), input.randomize) .map(Into::into) } fn get_scheduling_states( &mut self, input: anki_proto::cards::CardId, ) -> Result { let cid: CardId = input.into(); self.get_scheduling_states(cid).map(Into::into) } fn describe_next_states( &mut self, input: scheduler::SchedulingStates, ) -> Result { let states: SchedulingStates = input.into(); self.describe_next_states(&states).map(Into::into) } fn state_is_leech(&mut self, input: scheduler::SchedulingState) -> Result { let state: CardState = input.into(); Ok(state.leeched().into()) } fn answer_card( &mut self, input: scheduler::CardAnswer, ) -> Result { self.answer_card(&mut input.into()).map(Into::into) } fn upgrade_scheduler(&mut self) -> Result<()> { self.transact_no_undo(|col| col.upgrade_to_v2_scheduler()) .map(Into::into) } fn get_queued_cards( &mut self, input: scheduler::GetQueuedCardsRequest, ) -> Result { self.get_queued_cards(input.fetch_limit as usize, input.intraday_learning_only) .map(Into::into) } fn custom_study( &mut self, input: scheduler::CustomStudyRequest, ) -> Result { self.custom_study(input).map(Into::into) } fn custom_study_defaults( &mut self, input: scheduler::CustomStudyDefaultsRequest, ) -> Result { self.custom_study_defaults(input.deck_id.into()) } fn compute_fsrs_weights( &mut self, input: scheduler::ComputeFsrsWeightsRequest, ) -> Result { self.compute_weights(&input.search) } fn compute_optimal_retention( &mut self, input: ComputeOptimalRetentionRequest, ) -> Result { Ok(ComputeOptimalRetentionResponse { optimal_retention: self.compute_optimal_retention(input)?, }) } fn evaluate_weights( &mut self, input: scheduler::EvaluateWeightsRequest, ) -> Result { let ret = self.evaluate_weights(&input.weights, &input.search)?; Ok(scheduler::EvaluateWeightsResponse { log_loss: ret.log_loss, rmse_bins: ret.rmse_bins, }) } fn get_optimal_retention_parameters( &mut self, input: scheduler::GetOptimalRetentionParametersRequest, ) -> Result { self.get_optimal_retention_parameters(&input.search) .map(|params| GetOptimalRetentionParametersResponse { params: Some(params), }) } fn compute_memory_state(&mut self, input: cards::CardId) -> Result { self.compute_memory_state(input.into()) } } impl crate::services::BackendSchedulerService for Backend { fn compute_fsrs_weights_from_items( &self, req: scheduler::ComputeFsrsWeightsFromItemsRequest, ) -> Result { let fsrs = FSRS::new(None)?; let fsrs_items = req.items.len() as u32; let weights = fsrs.compute_weights( req.items.into_iter().map(fsrs_item_proto_to_fsrs).collect(), None, )?; Ok(ComputeFsrsWeightsResponse { weights, fsrs_items, }) } } fn fsrs_item_proto_to_fsrs(item: anki_proto::scheduler::FsrsItem) -> FSRSItem { FSRSItem { reviews: item .reviews .into_iter() .map(fsrs_review_proto_to_fsrs) .collect(), } } fn fsrs_review_proto_to_fsrs(review: anki_proto::scheduler::FsrsReview) -> FSRSReview { FSRSReview { delta_t: review.delta_t, rating: review.rating, } }