// 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::ComputeFsrsParamsResponse; use anki_proto::scheduler::ComputeMemoryStateResponse; use anki_proto::scheduler::ComputeOptimalRetentionRequest; use anki_proto::scheduler::ComputeOptimalRetentionResponse; use anki_proto::scheduler::FsrsBenchmarkResponse; use anki_proto::scheduler::FuzzDeltaRequest; use anki_proto::scheduler::FuzzDeltaResponse; use anki_proto::scheduler::GetOptimalRetentionParametersResponse; use anki_proto::scheduler::SimulateFsrsReviewRequest; use anki_proto::scheduler::SimulateFsrsReviewResponse; 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::search::SortMode; 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) }) } 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, ) }) } 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(|s| scheduler::schedule_cards_as_new_request::Context::try_from(s).ok()), ) .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()) } 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_params( &mut self, input: scheduler::ComputeFsrsParamsRequest, ) -> Result { self.compute_params( &input.search, input.ignore_revlogs_before_ms.into(), 1, 1, &input.current_params, input.num_of_relearning_steps as usize, ) } fn simulate_fsrs_review( &mut self, input: SimulateFsrsReviewRequest, ) -> Result { self.simulate_review(input) } fn compute_optimal_retention( &mut self, input: ComputeOptimalRetentionRequest, ) -> Result { Ok(ComputeOptimalRetentionResponse { optimal_retention: self.compute_optimal_retention(input)?, }) } fn evaluate_params( &mut self, input: scheduler::EvaluateParamsRequest, ) -> Result { let ret = self.evaluate_params( &input.params, &input.search, input.ignore_revlogs_before_ms.into(), )?; Ok(scheduler::EvaluateParamsResponse { log_loss: ret.log_loss, rmse_bins: ret.rmse_bins, }) } fn get_optimal_retention_parameters( &mut self, input: scheduler::GetOptimalRetentionParametersRequest, ) -> Result { let revlogs = self .search_cards_into_table(&input.search, SortMode::NoOrder)? .col .storage .get_revlog_entries_for_searched_cards_in_card_order()?; let simulator_config = self.get_optimal_retention_parameters(revlogs)?; Ok(GetOptimalRetentionParametersResponse { deck_size: simulator_config.deck_size as u32, learn_span: simulator_config.learn_span as u32, max_cost_perday: simulator_config.max_cost_perday, max_ivl: simulator_config.max_ivl, learn_costs: simulator_config.learn_costs.to_vec(), review_costs: simulator_config.review_costs.to_vec(), first_rating_prob: simulator_config.first_rating_prob.to_vec(), review_rating_prob: simulator_config.review_rating_prob.to_vec(), first_rating_offsets: simulator_config.first_rating_offsets.to_vec(), first_session_lens: simulator_config.first_session_lens.to_vec(), forget_rating_offset: simulator_config.forget_rating_offset, forget_session_len: simulator_config.forget_session_len, loss_aversion: simulator_config.loss_aversion, learn_limit: simulator_config.learn_limit as u32, review_limit: simulator_config.review_limit as u32, }) } fn compute_memory_state(&mut self, input: cards::CardId) -> Result { self.compute_memory_state(input.into()) } fn fuzz_delta(&mut self, input: FuzzDeltaRequest) -> Result { Ok(FuzzDeltaResponse { delta_days: self.get_fuzz_delta(input.card_id.into(), input.interval)?, }) } } impl crate::services::BackendSchedulerService for Backend { fn compute_fsrs_params_from_items( &self, req: scheduler::ComputeFsrsParamsFromItemsRequest, ) -> Result { let fsrs = FSRS::new(None)?; let fsrs_items = req.items.len() as u32; let params = fsrs.compute_parameters( req.items.into_iter().map(fsrs_item_proto_to_fsrs).collect(), None, true, )?; Ok(ComputeFsrsParamsResponse { params, fsrs_items }) } fn fsrs_benchmark( &self, req: scheduler::FsrsBenchmarkRequest, ) -> Result { let fsrs = FSRS::new(None)?; let train_set = req .train_set .into_iter() .map(fsrs_item_proto_to_fsrs) .collect(); let params = fsrs.benchmark(train_set, true); Ok(FsrsBenchmarkResponse { params }) } fn export_dataset(&self, req: scheduler::ExportDatasetRequest) -> Result<()> { self.with_col(|col| { col.export_dataset( req.min_entries.try_into().unwrap(), req.target_path.as_ref(), ) }) } } 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, } }