mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Feat/Easy Days (#3442)
* Feat/Easy Days * format * add easy_days_percentages to deck_config * configure Easy Days via table * remove unused code * add translatable strings & add default of easy days * don't check easy_days_percentages when deserialize * pass test::all_reserved_fields_are_removed * consider next_day_at when interval_to_weekday * remove y-axis-title created in last simulation * EstimatedTotalKnowledge should be integer * Reorder deck option sections (dae) - Move FSRS to bottom left, to move it closer to the top, and so the left and right columns appear roughly balanced when FSRS is enabled. - Move Easy Days above Advanced * Don't crash if wrong number of days (dae) * Use lower field number (dae) Repeated fields are more compactly stored in the first 15 fields.
This commit is contained in:
parent
79a6a4dc53
commit
a982720a42
10 changed files with 210 additions and 8 deletions
|
@ -277,6 +277,19 @@ deck-config-new-interval-tooltip = The multiplier applied to a review interval w
|
|||
deck-config-minimum-interval-tooltip = The minimum interval given to a review card after answering `Again`.
|
||||
deck-config-custom-scheduling = Custom scheduling
|
||||
deck-config-custom-scheduling-tooltip = Affects the entire collection. Use at your own risk!
|
||||
# Easy Days section
|
||||
|
||||
deck-config-easy-days-title = Easy Days
|
||||
deck-config-easy-days-monday = Monday
|
||||
deck-config-easy-days-tuesday = Tuesday
|
||||
deck-config-easy-days-wednesday = Wednesday
|
||||
deck-config-easy-days-thursday = Thursday
|
||||
deck-config-easy-days-friday = Friday
|
||||
deck-config-easy-days-saturday = Saturday
|
||||
deck-config-easy-days-sunday = Sunday
|
||||
deck-config-easy-days-normal = Normal
|
||||
deck-config-easy-days-reduced = Reduced
|
||||
deck-config-easy-days-minimum = Minimum
|
||||
|
||||
## Adding/renaming
|
||||
|
||||
|
|
|
@ -109,7 +109,7 @@ message DeckConfig {
|
|||
|
||||
repeated float fsrs_weights = 3;
|
||||
|
||||
reserved 4 to 8;
|
||||
reserved 5 to 8;
|
||||
|
||||
uint32 new_per_day = 9;
|
||||
uint32 reviews_per_day = 10;
|
||||
|
@ -159,6 +159,7 @@ message DeckConfig {
|
|||
// for fsrs
|
||||
float desired_retention = 37;
|
||||
string ignore_revlogs_before_date = 46;
|
||||
repeated float easy_days_percentages = 4;
|
||||
// used for fsrs_reschedule in the past
|
||||
reserved 39;
|
||||
float historical_retention = 40;
|
||||
|
|
|
@ -80,6 +80,7 @@ const DEFAULT_DECK_CONFIG_INNER: DeckConfigInner = DeckConfigInner {
|
|||
historical_retention: 0.9,
|
||||
weight_search: String::new(),
|
||||
ignore_revlogs_before_date: String::new(),
|
||||
easy_days_percentages: Vec::new(),
|
||||
};
|
||||
|
||||
impl Default for DeckConfig {
|
||||
|
@ -92,6 +93,7 @@ impl Default for DeckConfig {
|
|||
inner: DeckConfigInner {
|
||||
learn_steps: vec![1.0, 10.0],
|
||||
relearn_steps: vec![10.0],
|
||||
easy_days_percentages: vec![1.0; 7],
|
||||
..DEFAULT_DECK_CONFIG_INNER
|
||||
},
|
||||
}
|
||||
|
|
|
@ -76,6 +76,8 @@ pub struct DeckConfSchema11 {
|
|||
#[serde(default)]
|
||||
ignore_revlogs_before_date: String,
|
||||
#[serde(default)]
|
||||
easy_days_percentages: Vec<f32>,
|
||||
#[serde(default)]
|
||||
stop_timer_on_answer: bool,
|
||||
#[serde(default)]
|
||||
seconds_to_show_question: f32,
|
||||
|
@ -309,6 +311,7 @@ impl Default for DeckConfSchema11 {
|
|||
sm2_retention: 0.9,
|
||||
weight_search: "".to_string(),
|
||||
ignore_revlogs_before_date: "".to_string(),
|
||||
easy_days_percentages: vec![1.0; 7],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -385,6 +388,7 @@ impl From<DeckConfSchema11> for DeckConfig {
|
|||
bury_interday_learning: c.bury_interday_learning,
|
||||
fsrs_weights: c.fsrs_weights,
|
||||
ignore_revlogs_before_date: c.ignore_revlogs_before_date,
|
||||
easy_days_percentages: c.easy_days_percentages,
|
||||
desired_retention: c.desired_retention,
|
||||
historical_retention: c.sm2_retention,
|
||||
weight_search: c.weight_search,
|
||||
|
@ -499,6 +503,7 @@ impl From<DeckConfig> for DeckConfSchema11 {
|
|||
sm2_retention: i.historical_retention,
|
||||
weight_search: i.weight_search,
|
||||
ignore_revlogs_before_date: i.ignore_revlogs_before_date,
|
||||
easy_days_percentages: i.easy_days_percentages,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -531,6 +536,7 @@ static RESERVED_DECKCONF_KEYS: Set<&'static str> = phf_set! {
|
|||
"sm2Retention",
|
||||
"weightSearch",
|
||||
"ignoreRevlogsBeforeDate",
|
||||
"easyDaysPercentages",
|
||||
};
|
||||
|
||||
static RESERVED_DECKCONF_NEW_KEYS: Set<&'static str> = phf_set! {
|
||||
|
|
|
@ -150,7 +150,12 @@ impl QueueBuilder {
|
|||
.values()
|
||||
.filter_map(|deck| Some((deck.id, deck.config_id()?)))
|
||||
.collect::<HashMap<_, _>>();
|
||||
let load_balancer = LoadBalancer::new(timing.days_elapsed, did_to_dcid, &col.storage)?;
|
||||
let load_balancer = LoadBalancer::new(
|
||||
timing.days_elapsed,
|
||||
did_to_dcid,
|
||||
col.timing_today()?.next_day_at,
|
||||
&col.storage,
|
||||
)?;
|
||||
|
||||
Ok(QueueBuilder {
|
||||
new: Vec::new(),
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use chrono::Datelike;
|
||||
use rand::distributions::Distribution;
|
||||
use rand::distributions::WeightedIndex;
|
||||
use rand::rngs::StdRng;
|
||||
|
@ -12,6 +13,7 @@ use rand::SeedableRng;
|
|||
use super::fuzz::constrained_fuzz_bounds;
|
||||
use crate::card::CardId;
|
||||
use crate::deckconfig::DeckConfigId;
|
||||
use crate::error::InvalidInputError;
|
||||
use crate::notes::NoteId;
|
||||
use crate::prelude::*;
|
||||
use crate::storage::SqliteStorage;
|
||||
|
@ -82,12 +84,15 @@ pub struct LoadBalancer {
|
|||
/// Load balancer operates at the preset level, it only counts
|
||||
/// cards in the same preset as the card being balanced.
|
||||
days_by_preset: HashMap<DeckConfigId, [LoadBalancerDay; LOAD_BALANCE_DAYS]>,
|
||||
easy_days_percentages_by_preset: HashMap<DeckConfigId, [f32; 7]>,
|
||||
next_day_at: TimestampSecs,
|
||||
}
|
||||
|
||||
impl LoadBalancer {
|
||||
pub fn new(
|
||||
today: u32,
|
||||
did_to_dcid: HashMap<DeckId, DeckConfigId>,
|
||||
next_day_at: TimestampSecs,
|
||||
storage: &SqliteStorage,
|
||||
) -> Result<LoadBalancer> {
|
||||
let cards_on_each_day =
|
||||
|
@ -126,8 +131,29 @@ impl LoadBalancer {
|
|||
deckconfig_group
|
||||
},
|
||||
);
|
||||
let configs = storage.get_deck_config_map()?;
|
||||
|
||||
Ok(LoadBalancer { days_by_preset })
|
||||
let mut easy_days_percentages_by_preset = HashMap::with_capacity(configs.len());
|
||||
for (dcid, conf) in configs {
|
||||
let easy_days_percentages = if conf.inner.easy_days_percentages.is_empty() {
|
||||
[1.0; 7]
|
||||
} else {
|
||||
conf.inner.easy_days_percentages.try_into().map_err(|_| {
|
||||
AnkiError::from(InvalidInputError {
|
||||
message: "expected 7 days".into(),
|
||||
source: None,
|
||||
backtrace: None,
|
||||
})
|
||||
})?
|
||||
};
|
||||
easy_days_percentages_by_preset.insert(dcid, easy_days_percentages);
|
||||
}
|
||||
|
||||
Ok(LoadBalancer {
|
||||
days_by_preset,
|
||||
easy_days_percentages_by_preset,
|
||||
next_day_at,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn review_context(
|
||||
|
@ -182,6 +208,24 @@ impl LoadBalancer {
|
|||
let days = self.days_by_preset.get(&deckconfig_id)?;
|
||||
let interval_days = &days[before_days as usize..=after_days as usize];
|
||||
|
||||
// calculate review counts and expected distribution
|
||||
let (review_counts, weekdays): (Vec<usize>, Vec<usize>) = interval_days
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, day)| {
|
||||
(
|
||||
day.cards.len(),
|
||||
interval_to_weekday(i as u32 + before_days, self.next_day_at),
|
||||
)
|
||||
})
|
||||
.unzip();
|
||||
let easy_days_percentages = self.easy_days_percentages_by_preset.get(&deckconfig_id)?;
|
||||
let percentages = weekdays
|
||||
.iter()
|
||||
.map(|&wd| easy_days_percentages[wd])
|
||||
.collect::<Vec<_>>();
|
||||
let expected_distribution = check_review_distribution(&review_counts, &percentages);
|
||||
|
||||
// calculate weights for each day
|
||||
let intervals_and_weights = interval_days
|
||||
.iter()
|
||||
|
@ -198,7 +242,7 @@ impl LoadBalancer {
|
|||
})
|
||||
.unwrap_or(1.0);
|
||||
|
||||
let weight = match interval_day.cards.len() {
|
||||
let weight = match review_counts[interval_index] {
|
||||
0 => 1.0, // if theres no cards due on this day, give it the full 1.0 weight
|
||||
card_count => {
|
||||
let card_count_weight = (1.0 / card_count as f32).powi(2);
|
||||
|
@ -208,7 +252,10 @@ impl LoadBalancer {
|
|||
}
|
||||
};
|
||||
|
||||
(target_interval, weight)
|
||||
(
|
||||
target_interval,
|
||||
weight * expected_distribution[interval_index],
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
|
@ -237,3 +284,27 @@ impl LoadBalancer {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn interval_to_weekday(interval: u32, next_day_at: TimestampSecs) -> usize {
|
||||
let target_datetime = next_day_at
|
||||
.adding_secs((interval - 1) as i64 * 86400)
|
||||
.local_datetime()
|
||||
.unwrap();
|
||||
target_datetime.weekday().num_days_from_monday() as usize
|
||||
}
|
||||
|
||||
fn check_review_distribution(actual_reviews: &[usize], percentages: &[f32]) -> Vec<f32> {
|
||||
if percentages.iter().sum::<f32>() == 0.0 {
|
||||
return vec![1.0; actual_reviews.len()];
|
||||
}
|
||||
let total_actual = actual_reviews.iter().sum::<usize>() as f32;
|
||||
let expected_distribution: Vec<f32> = percentages
|
||||
.iter()
|
||||
.map(|&p| p * (total_actual / percentages.iter().sum::<f32>()))
|
||||
.collect();
|
||||
expected_distribution
|
||||
.iter()
|
||||
.zip(actual_reviews.iter())
|
||||
.map(|(&e, &a)| (e - a as f32).max(0.0))
|
||||
.collect()
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import type { DeckOptionsState } from "./lib";
|
||||
import NewOptions from "./NewOptions.svelte";
|
||||
import TimerOptions from "./TimerOptions.svelte";
|
||||
import EasyDays from "./EasyDays.svelte";
|
||||
|
||||
export let state: DeckOptionsState;
|
||||
const addons = state.addonComponents;
|
||||
|
@ -57,6 +58,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
export const timerOptions = {};
|
||||
export const audioOptions = {};
|
||||
export const advancedOptions = {};
|
||||
export const easyDays = {};
|
||||
|
||||
let onPresetChange: () => void;
|
||||
</script>
|
||||
|
@ -88,11 +90,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</Row>
|
||||
|
||||
<Row class="row-columns">
|
||||
<BuryOptions {state} api={buryOptions} />
|
||||
<FsrsOptionsOuter {state} api={{}} />
|
||||
</Row>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Row class="row-columns">
|
||||
<BuryOptions {state} api={buryOptions} />
|
||||
</Row>
|
||||
|
||||
<Row class="row-columns">
|
||||
<AudioOptions {state} api={audioOptions} />
|
||||
</Row>
|
||||
|
@ -112,7 +118,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
{/if}
|
||||
|
||||
<Row class="row-columns">
|
||||
<FsrsOptionsOuter {state} api={{}} />
|
||||
<EasyDays {state} api={easyDays} />
|
||||
</Row>
|
||||
|
||||
<Row class="row-columns">
|
||||
|
|
97
ts/routes/deck-options/EasyDays.svelte
Normal file
97
ts/routes/deck-options/EasyDays.svelte
Normal file
|
@ -0,0 +1,97 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import * as tr from "@generated/ftl";
|
||||
import DynamicallySlottable from "$lib/components/DynamicallySlottable.svelte";
|
||||
import Item from "$lib/components/Item.svelte";
|
||||
import TitledContainer from "$lib/components/TitledContainer.svelte";
|
||||
import type { DeckOptionsState } from "./lib";
|
||||
|
||||
export let state: DeckOptionsState;
|
||||
export let api: Record<string, never>;
|
||||
|
||||
const config = state.currentConfig;
|
||||
const defaults = state.defaults;
|
||||
|
||||
if ($config.easyDaysPercentages.length !== 7) {
|
||||
$config.easyDaysPercentages = defaults.easyDaysPercentages;
|
||||
}
|
||||
|
||||
const easyDays = [
|
||||
tr.deckConfigEasyDaysMonday(),
|
||||
tr.deckConfigEasyDaysTuesday(),
|
||||
tr.deckConfigEasyDaysWednesday(),
|
||||
tr.deckConfigEasyDaysThursday(),
|
||||
tr.deckConfigEasyDaysFriday(),
|
||||
tr.deckConfigEasyDaysSaturday(),
|
||||
tr.deckConfigEasyDaysSunday(),
|
||||
];
|
||||
</script>
|
||||
|
||||
<TitledContainer title={tr.deckConfigEasyDaysTitle()}>
|
||||
<DynamicallySlottable slotHost={Item} {api}>
|
||||
<Item>
|
||||
<div class="easy-days-settings">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{tr.deckConfigEasyDaysNormal()}</th>
|
||||
<th>{tr.deckConfigEasyDaysReduced()}</th>
|
||||
<th>{tr.deckConfigEasyDaysMinimum()}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each easyDays as day, index}
|
||||
<tr>
|
||||
<td>{day}</td>
|
||||
<td>
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={$config.easyDaysPercentages[index]}
|
||||
value={1.0}
|
||||
checked={$config.easyDaysPercentages[index] ===
|
||||
1.0}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={$config.easyDaysPercentages[index]}
|
||||
value={0.5}
|
||||
checked={$config.easyDaysPercentages[index] ===
|
||||
0.5}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
type="radio"
|
||||
bind:group={$config.easyDaysPercentages[index]}
|
||||
value={0.0}
|
||||
checked={$config.easyDaysPercentages[index] ===
|
||||
0.0}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</TitledContainer>
|
||||
|
||||
<style>
|
||||
.easy-days-settings table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.easy-days-settings th,
|
||||
.easy-days-settings td {
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
</style>
|
|
@ -111,7 +111,7 @@ export function prepareData(
|
|||
},
|
||||
{
|
||||
label: tr.statisticsEstimatedTotalKnowledge(),
|
||||
value: tr.statisticsCards({ cards: data.sum }),
|
||||
value: tr.statisticsCards({ cards: +data.sum.toFixed(0) }),
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -81,6 +81,7 @@ export function renderSimulationChart(
|
|||
)
|
||||
.attr("direction", "ltr");
|
||||
|
||||
svg.select(".y-ticks .y-axis-title").remove();
|
||||
svg.select(".y-ticks")
|
||||
.append("text")
|
||||
.attr("class", "y-axis-title")
|
||||
|
|
Loading…
Reference in a new issue