From 84fe309583dbe09643d368cef3036b1a33821f50 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 6 Apr 2021 16:38:42 +1000 Subject: [PATCH] update scheduling ops - migrate to CollectionOp() - return actual change count when suspending/burying - add helper to convert vec to vec of newtype --- pylib/anki/scheduler/base.py | 25 +++-- pylib/tests/test_schedv1.py | 2 +- qt/aqt/browser.py | 14 +-- qt/aqt/filtered_deck.py | 4 +- qt/aqt/operations/scheduling.py | 143 ++++++++++++------------ qt/aqt/overview.py | 10 +- qt/aqt/reviewer.py | 31 +++-- rslib/backend.proto | 5 +- rslib/src/backend/notes.rs | 2 +- rslib/src/backend/scheduler/mod.rs | 18 ++- rslib/src/prelude.rs | 1 + rslib/src/scheduler/answering/undo.rs | 2 +- rslib/src/scheduler/bury_and_suspend.rs | 12 +- rslib/src/storage/card/mod.rs | 15 ++- rslib/src/types.rs | 15 +++ 15 files changed, 172 insertions(+), 127 deletions(-) diff --git a/pylib/anki/scheduler/base.py b/pylib/anki/scheduler/base.py index 280d9a7ec..268211e06 100644 --- a/pylib/anki/scheduler/base.py +++ b/pylib/anki/scheduler/base.py @@ -16,7 +16,7 @@ from typing import List, Optional, Sequence from anki.cards import CardId from anki.consts import CARD_TYPE_NEW, NEW_CARDS_RANDOM, QUEUE_TYPE_NEW, QUEUE_TYPE_REV from anki.decks import DeckConfigDict, DeckId, DeckTreeNode -from anki.notes import Note +from anki.notes import NoteId from anki.utils import ids2str, intTime CongratsInfo = _pb.CongratsInfoOut @@ -123,20 +123,31 @@ select id from cards where did in %s and queue = {QUEUE_TYPE_REV} and due <= ? l ) -> None: self.col._backend.unbury_cards_in_current_deck(mode) - def suspend_cards(self, ids: Sequence[CardId]) -> OpChanges: + def suspend_cards(self, ids: Sequence[CardId]) -> OpChangesWithCount: return self.col._backend.bury_or_suspend_cards( - card_ids=ids, mode=BuryOrSuspend.SUSPEND + card_ids=ids, note_ids=[], mode=BuryOrSuspend.SUSPEND ) - def bury_cards(self, ids: Sequence[CardId], manual: bool = True) -> OpChanges: + def suspend_notes(self, ids: Sequence[NoteId]) -> OpChangesWithCount: + return self.col._backend.bury_or_suspend_cards( + card_ids=[], note_ids=ids, mode=BuryOrSuspend.SUSPEND + ) + + def bury_cards( + self, ids: Sequence[CardId], manual: bool = True + ) -> OpChangesWithCount: if manual: mode = BuryOrSuspend.BURY_USER else: mode = BuryOrSuspend.BURY_SCHED - return self.col._backend.bury_or_suspend_cards(card_ids=ids, mode=mode) + return self.col._backend.bury_or_suspend_cards( + card_ids=ids, note_ids=[], mode=mode + ) - def bury_note(self, note: Note) -> None: - self.bury_cards(note.card_ids()) + def bury_notes(self, note_ids: Sequence[NoteId]) -> OpChangesWithCount: + return self.col._backend.bury_or_suspend_cards( + card_ids=[], note_ids=note_ids, mode=BuryOrSuspend.BURY_USER + ) # Resetting/rescheduling ########################################################################## diff --git a/pylib/tests/test_schedv1.py b/pylib/tests/test_schedv1.py index ac21d7fa7..c1a4d3c89 100644 --- a/pylib/tests/test_schedv1.py +++ b/pylib/tests/test_schedv1.py @@ -501,7 +501,7 @@ def test_misc(): col.addNote(note) c = note.cards()[0] # burying - col.sched.bury_note(note) + col.sched.bury_notes([note.id]) col.reset() assert not col.sched.getCard() col.sched.unbury_cards_in_current_deck() diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 0458fe294..88c0a09a8 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -732,9 +732,9 @@ where id in %s""" def suspend_selected_cards(self, checked: bool) -> None: cids = self.selected_cards() if checked: - suspend_cards(mw=self.mw, card_ids=cids) + suspend_cards(parent=self, card_ids=cids).run_in_background() else: - unsuspend_cards(mw=self.mw, card_ids=cids) + unsuspend_cards(parent=self.mw, card_ids=cids).run_in_background() # Exporting ###################################################################### @@ -796,25 +796,23 @@ where id in %s""" return reposition_new_cards_dialog( - mw=self.mw, parent=self, card_ids=self.selected_cards() - ) + parent=self, card_ids=self.selected_cards() + ).run_in_background() @ensure_editor_saved_on_trigger def set_due_date(self) -> None: set_due_date_dialog( - mw=self.mw, parent=self, card_ids=self.selected_cards(), config_key=Config.String.SET_DUE_BROWSER, - ) + ).run_in_background() @ensure_editor_saved_on_trigger def forget_cards(self) -> None: forget_cards( - mw=self.mw, parent=self, card_ids=self.selected_cards(), - ) + ).run_in_background() # Edit: selection ###################################################################### diff --git a/qt/aqt/filtered_deck.py b/qt/aqt/filtered_deck.py index fcce0af09..25af48f80 100644 --- a/qt/aqt/filtered_deck.py +++ b/qt/aqt/filtered_deck.py @@ -310,7 +310,9 @@ class FilteredDeckConfigDialog(QDialog): gui_hooks.filtered_deck_dialog_will_add_or_update_deck(self, self.deck) - add_or_update_filtered_deck(mw=self.mw, deck=self.deck, success=success) + add_or_update_filtered_deck(parent=self, deck=self.deck).success( + success + ).run_in_background() # Step load/save ######################################################## diff --git a/qt/aqt/operations/scheduling.py b/qt/aqt/operations/scheduling.py index acb3dcdff..a9a71c948 100644 --- a/qt/aqt/operations/scheduling.py +++ b/qt/aqt/operations/scheduling.py @@ -7,28 +7,32 @@ from typing import Optional, Sequence import aqt from anki.cards import CardId -from anki.collection import CARD_TYPE_NEW, Config +from anki.collection import ( + CARD_TYPE_NEW, + Config, + OpChanges, + OpChangesWithCount, + OpChangesWithId, +) from anki.decks import DeckId from anki.notes import NoteId from anki.scheduler import FilteredDeckForUpdate -from aqt import AnkiQt -from aqt.main import PerformOpOptionalSuccessCallback +from aqt.operations import CollectionOp from aqt.qt import * from aqt.utils import disable_help_button, getText, tooltip, tr def set_due_date_dialog( *, - mw: aqt.AnkiQt, parent: QWidget, card_ids: Sequence[CardId], config_key: Optional[Config.String.Key.V], -) -> None: +) -> Optional[CollectionOp[OpChanges]]: if not card_ids: - return + return None default_text = ( - mw.col.get_config_string(config_key) if config_key is not None else "" + aqt.mw.col.get_config_string(config_key) if config_key is not None else "" ) prompt = "\n".join( [ @@ -43,34 +47,35 @@ def set_due_date_dialog( title=tr.actions_set_due_date(), ) if not success or not days.strip(): - return - - mw.perform_op( - lambda: mw.col.sched.set_due_date(card_ids, days, config_key), - success=lambda _: tooltip( - tr.scheduling_set_due_date_done(cards=len(card_ids)), - parent=parent, - ), - ) + return None + else: + return CollectionOp( + parent, lambda col: col.sched.set_due_date(card_ids, days, config_key) + ).success( + lambda _: tooltip( + tr.scheduling_set_due_date_done(cards=len(card_ids)), + parent=parent, + ) + ) def forget_cards( - *, mw: aqt.AnkiQt, parent: QWidget, card_ids: Sequence[CardId] -) -> None: - if not card_ids: - return - - mw.perform_op( - lambda: mw.col.sched.schedule_cards_as_new(card_ids), - success=lambda _: tooltip( + *, parent: QWidget, card_ids: Sequence[CardId] +) -> CollectionOp[OpChanges]: + return CollectionOp( + parent, lambda col: col.sched.schedule_cards_as_new(card_ids) + ).success( + lambda _: tooltip( tr.scheduling_forgot_cards(cards=len(card_ids)), parent=parent - ), + ) ) def reposition_new_cards_dialog( - *, mw: AnkiQt, parent: QWidget, card_ids: Sequence[CardId] -) -> None: + *, parent: QWidget, card_ids: Sequence[CardId] +) -> Optional[CollectionOp[OpChangesWithCount]]: + from aqt import mw + assert mw.col.db row = mw.col.db.first( f"select min(due), max(due) from cards where type={CARD_TYPE_NEW} and odid=0" @@ -92,15 +97,14 @@ def reposition_new_cards_dialog( frm.start.selectAll() if not d.exec_(): - return + return None start = frm.start.value() step = frm.step.value() randomize = frm.randomize.isChecked() shift = frm.shift.isChecked() - reposition_new_cards( - mw=mw, + return reposition_new_cards( parent=parent, card_ids=card_ids, starting_from=start, @@ -112,89 +116,80 @@ def reposition_new_cards_dialog( def reposition_new_cards( *, - mw: AnkiQt, parent: QWidget, card_ids: Sequence[CardId], starting_from: int, step_size: int, randomize: bool, shift_existing: bool, -) -> None: - mw.perform_op( - lambda: mw.col.sched.reposition_new_cards( +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp( + parent, + lambda col: col.sched.reposition_new_cards( card_ids=card_ids, starting_from=starting_from, step_size=step_size, randomize=randomize, shift_existing=shift_existing, ), - success=lambda out: tooltip( + ).success( + lambda out: tooltip( tr.browsing_changed_new_position(count=out.count), parent=parent - ), + ) ) def suspend_cards( *, - mw: AnkiQt, + parent: QWidget, card_ids: Sequence[CardId], - success: PerformOpOptionalSuccessCallback = None, -) -> None: - mw.perform_op(lambda: mw.col.sched.suspend_cards(card_ids), success=success) +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp(parent, lambda col: col.sched.suspend_cards(card_ids)) def suspend_note( *, - mw: AnkiQt, - note_id: NoteId, - success: PerformOpOptionalSuccessCallback = None, -) -> None: - mw.taskman.run_in_background( - lambda: mw.col.card_ids_of_note(note_id), - lambda future: suspend_cards(mw=mw, card_ids=future.result(), success=success), - ) + parent: QWidget, + note_ids: Sequence[NoteId], +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp(parent, lambda col: col.sched.suspend_notes(note_ids)) -def unsuspend_cards(*, mw: AnkiQt, card_ids: Sequence[CardId]) -> None: - mw.perform_op(lambda: mw.col.sched.unsuspend_cards(card_ids)) +def unsuspend_cards( + *, parent: QWidget, card_ids: Sequence[CardId] +) -> CollectionOp[OpChanges]: + return CollectionOp(parent, lambda col: col.sched.unsuspend_cards(card_ids)) def bury_cards( *, - mw: AnkiQt, + parent: QWidget, card_ids: Sequence[CardId], - success: PerformOpOptionalSuccessCallback = None, -) -> None: - mw.perform_op(lambda: mw.col.sched.bury_cards(card_ids), success=success) +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp(parent, lambda col: col.sched.bury_cards(card_ids)) -def bury_note( +def bury_notes( *, - mw: AnkiQt, - note_id: NoteId, - success: PerformOpOptionalSuccessCallback = None, -) -> None: - mw.taskman.run_in_background( - lambda: mw.col.card_ids_of_note(note_id), - lambda future: bury_cards(mw=mw, card_ids=future.result(), success=success), - ) + parent: QWidget, + note_ids: Sequence[NoteId], +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp(parent, lambda col: col.sched.bury_notes(note_ids)) -def rebuild_filtered_deck(*, mw: AnkiQt, deck_id: DeckId) -> None: - mw.perform_op(lambda: mw.col.sched.rebuild_filtered_deck(deck_id)) +def rebuild_filtered_deck( + *, parent: QWidget, deck_id: DeckId +) -> CollectionOp[OpChangesWithCount]: + return CollectionOp(parent, lambda col: col.sched.rebuild_filtered_deck(deck_id)) -def empty_filtered_deck(*, mw: AnkiQt, deck_id: DeckId) -> None: - mw.perform_op(lambda: mw.col.sched.empty_filtered_deck(deck_id)) +def empty_filtered_deck(*, parent: QWidget, deck_id: DeckId) -> CollectionOp[OpChanges]: + return CollectionOp(parent, lambda col: col.sched.empty_filtered_deck(deck_id)) def add_or_update_filtered_deck( *, - mw: AnkiQt, + parent: QWidget, deck: FilteredDeckForUpdate, - success: PerformOpOptionalSuccessCallback, -) -> None: - mw.perform_op( - lambda: mw.col.sched.add_or_update_filtered_deck(deck), - success=success, - ) +) -> CollectionOp[OpChangesWithId]: + return CollectionOp(parent, lambda col: col.sched.add_or_update_filtered_deck(deck)) diff --git a/qt/aqt/overview.py b/qt/aqt/overview.py index 2e7e01ea7..bd98aac3e 100644 --- a/qt/aqt/overview.py +++ b/qt/aqt/overview.py @@ -119,12 +119,14 @@ class Overview: return self.mw.col.decks.current()["dyn"] def rebuild_current_filtered_deck(self) -> None: - if self._current_deck_is_filtered(): - rebuild_filtered_deck(mw=self.mw, deck_id=self.mw.col.decks.selected()) + rebuild_filtered_deck( + parent=self.mw, deck_id=self.mw.col.decks.selected() + ).run_in_background() def empty_current_filtered_deck(self) -> None: - if self._current_deck_is_filtered(): - empty_filtered_deck(mw=self.mw, deck_id=self.mw.col.decks.selected()) + empty_filtered_deck( + parent=self.mw, deck_id=self.mw.col.decks.selected() + ).run_in_background() def onCustomStudyKey(self) -> None: if not self._current_deck_is_filtered(): diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 553063d80..0394bc8fd 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -22,7 +22,7 @@ from aqt.operations.card import set_card_flag from aqt.operations.note import remove_notes from aqt.operations.scheduling import ( bury_cards, - bury_note, + bury_notes, set_due_date_dialog, suspend_cards, suspend_note, @@ -861,39 +861,34 @@ time = %(time)d; return set_due_date_dialog( - mw=self.mw, parent=self.mw, card_ids=[self.card.id], config_key=Config.String.SET_DUE_REVIEWER, - ) + ).run_in_background() def suspend_current_note(self) -> None: suspend_note( - mw=self.mw, - note_id=self.card.nid, - success=lambda _: tooltip(tr.studying_note_suspended()), - ) + parent=self.mw, + note_ids=[self.card.nid], + ).success(lambda _: tooltip(tr.studying_note_suspended())).run_in_background() def suspend_current_card(self) -> None: suspend_cards( - mw=self.mw, + parent=self.mw, card_ids=[self.card.id], - success=lambda _: tooltip(tr.studying_card_suspended()), - ) + ).success(lambda _: tooltip(tr.studying_card_suspended())).run_in_background() def bury_current_note(self) -> None: - bury_note( - mw=self.mw, - note_id=self.card.nid, - success=lambda _: tooltip(tr.studying_note_buried()), - ) + bury_notes( + parent=self.mw, + note_ids=[self.card.nid], + ).success(lambda _: tooltip(tr.studying_note_buried())).run_in_background() def bury_current_card(self) -> None: bury_cards( - mw=self.mw, + parent=self.mw, card_ids=[self.card.id], - success=lambda _: tooltip(tr.studying_card_buried()), - ) + ).success(lambda _: tooltip(tr.studying_card_buried())).run_in_background() def delete_current_note(self) -> None: # need to check state because the shortcut is global to the main diff --git a/rslib/backend.proto b/rslib/backend.proto index 6bcfd36c3..7a0b92de5 100644 --- a/rslib/backend.proto +++ b/rslib/backend.proto @@ -120,7 +120,7 @@ service SchedulingService { rpc CongratsInfo(Empty) returns (CongratsInfoOut); rpc RestoreBuriedAndSuspendedCards(CardIds) returns (OpChanges); rpc UnburyCardsInCurrentDeck(UnburyCardsInCurrentDeckIn) returns (Empty); - rpc BuryOrSuspendCards(BuryOrSuspendCardsIn) returns (OpChanges); + rpc BuryOrSuspendCards(BuryOrSuspendCardsIn) returns (OpChangesWithCount); rpc EmptyFilteredDeck(DeckId) returns (OpChanges); rpc RebuildFilteredDeck(DeckId) returns (OpChangesWithCount); rpc ScheduleCardsAsNew(ScheduleCardsAsNewIn) returns (OpChanges); @@ -1314,7 +1314,8 @@ message BuryOrSuspendCardsIn { BURY_USER = 2; } repeated int64 card_ids = 1; - Mode mode = 2; + repeated int64 note_ids = 2; + Mode mode = 3; } message ScheduleCardsAsNewIn { diff --git a/rslib/src/backend/notes.rs b/rslib/src/backend/notes.rs index ceeb53976..864c0e5e0 100644 --- a/rslib/src/backend/notes.rs +++ b/rslib/src/backend/notes.rs @@ -131,7 +131,7 @@ impl NotesService for Backend { fn cards_of_note(&self, input: pb::NoteId) -> Result { self.with_col(|col| { col.storage - .all_card_ids_of_note(NoteId(input.nid)) + .all_card_ids_of_note_in_order(NoteId(input.nid)) .map(|v| pb::CardIds { cids: v.into_iter().map(Into::into).collect(), }) diff --git a/rslib/src/backend/scheduler/mod.rs b/rslib/src/backend/scheduler/mod.rs index 3f612bc91..49084ad93 100644 --- a/rslib/src/backend/scheduler/mod.rs +++ b/rslib/src/backend/scheduler/mod.rs @@ -87,10 +87,18 @@ impl SchedulingService for Backend { }) } - fn bury_or_suspend_cards(&self, input: pb::BuryOrSuspendCardsIn) -> Result { + fn bury_or_suspend_cards( + &self, + input: pb::BuryOrSuspendCardsIn, + ) -> Result { self.with_col(|col| { let mode = input.mode(); - let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect(); + let cids = if input.card_ids.is_empty() { + col.storage + .card_ids_of_notes(&input.note_ids.into_newtype(NoteId))? + } else { + input.card_ids.into_newtype(CardId) + }; col.bury_or_suspend_cards(&cids, mode).map(Into::into) }) } @@ -105,7 +113,7 @@ impl SchedulingService for Backend { fn schedule_cards_as_new(&self, input: pb::ScheduleCardsAsNewIn) -> Result { self.with_col(|col| { - let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect(); + let cids = input.card_ids.into_newtype(CardId); let log = input.log; col.reschedule_cards_as_new(&cids, log).map(Into::into) }) @@ -114,12 +122,12 @@ impl SchedulingService for Backend { fn set_due_date(&self, input: pb::SetDueDateIn) -> Result { let config = input.config_key.map(Into::into); let days = input.days; - let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect(); + let cids = input.card_ids.into_newtype(CardId); self.with_col(|col| col.set_due_date(&cids, &days, config).map(Into::into)) } fn sort_cards(&self, input: pb::SortCardsIn) -> Result { - let cids: Vec<_> = input.card_ids.into_iter().map(CardId).collect(); + let cids = input.card_ids.into_newtype(CardId); let (start, step, random, shift) = ( input.starting_from, input.step_size, diff --git a/rslib/src/prelude.rs b/rslib/src/prelude.rs index 680066210..544c5dac3 100644 --- a/rslib/src/prelude.rs +++ b/rslib/src/prelude.rs @@ -1,6 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +pub(crate) use crate::types::IntoNewtypeVec; pub use crate::{ card::{Card, CardId}, collection::Collection, diff --git a/rslib/src/scheduler/answering/undo.rs b/rslib/src/scheduler/answering/undo.rs index 52f1885db..8d86d9bc2 100644 --- a/rslib/src/scheduler/answering/undo.rs +++ b/rslib/src/scheduler/answering/undo.rs @@ -33,7 +33,7 @@ mod test { let queued = col.next_card()?.unwrap(); let nid = note.id; let cid = queued.card.id; - let sibling_cid = col.storage.all_card_ids_of_note(nid)?[1]; + let sibling_cid = col.storage.all_card_ids_of_note_in_order(nid)?[1]; let assert_initial_state = |col: &mut Collection| -> Result<()> { let first = col.storage.get_card(cid)?.unwrap(); diff --git a/rslib/src/scheduler/bury_and_suspend.rs b/rslib/src/scheduler/bury_and_suspend.rs index 640e98c42..31a3771da 100644 --- a/rslib/src/scheduler/bury_and_suspend.rs +++ b/rslib/src/scheduler/bury_and_suspend.rs @@ -89,7 +89,8 @@ impl Collection { /// Bury/suspend cards in search table, and clear it. /// Marks the cards as modified. - fn bury_or_suspend_searched_cards(&mut self, mode: BuryOrSuspendMode) -> Result<()> { + fn bury_or_suspend_searched_cards(&mut self, mode: BuryOrSuspendMode) -> Result { + let mut count = 0; let usn = self.usn()?; let sched = self.scheduler_version(); @@ -113,18 +114,21 @@ impl Collection { card.remove_from_learning(); } card.queue = desired_queue; + count += 1; self.update_card_inner(&mut card, original, usn)?; } } - self.storage.clear_searched_cards_table() + self.storage.clear_searched_cards_table()?; + + Ok(count) } pub fn bury_or_suspend_cards( &mut self, cids: &[CardId], mode: BuryOrSuspendMode, - ) -> Result> { + ) -> Result> { let op = match mode { BuryOrSuspendMode::Suspend => Op::Suspend, BuryOrSuspendMode::BurySched | BuryOrSuspendMode::BuryUser => Op::Bury, @@ -141,7 +145,7 @@ impl Collection { nid: NoteId, include_new: bool, include_reviews: bool, - ) -> Result<()> { + ) -> Result { self.storage .search_siblings_for_bury(cid, nid, include_new, include_reviews)?; self.bury_or_suspend_searched_cards(BuryOrSuspendMode::BurySched) diff --git a/rslib/src/storage/card/mod.rs b/rslib/src/storage/card/mod.rs index 7bd54221f..a700f7c5b 100644 --- a/rslib/src/storage/card/mod.rs +++ b/rslib/src/storage/card/mod.rs @@ -308,13 +308,26 @@ impl super::SqliteStorage { .collect() } - pub(crate) fn all_card_ids_of_note(&self, nid: NoteId) -> Result> { + pub(crate) fn all_card_ids_of_note_in_order(&self, nid: NoteId) -> Result> { self.db .prepare_cached("select id from cards where nid = ? order by ord")? .query_and_then(&[nid], |r| Ok(CardId(r.get(0)?)))? .collect() } + pub(crate) fn card_ids_of_notes(&self, nids: &[NoteId]) -> Result> { + let mut stmt = self + .db + .prepare_cached("select id from cards where nid = ?")?; + let mut cids = vec![]; + for nid in nids { + for cid in stmt.query_map(&[nid], |row| row.get(0))? { + cids.push(cid?); + } + } + Ok(cids) + } + /// Place matching card ids into the search table. pub(crate) fn search_siblings_for_bury( &self, diff --git a/rslib/src/types.rs b/rslib/src/types.rs index 1a14b226f..5f1a00619 100644 --- a/rslib/src/types.rs +++ b/rslib/src/types.rs @@ -68,3 +68,18 @@ macro_rules! define_newtype { } define_newtype!(Usn, i32); + +pub(crate) trait IntoNewtypeVec { + fn into_newtype(self, func: F) -> Vec + where + F: FnMut(i64) -> T; +} + +impl IntoNewtypeVec for Vec { + fn into_newtype(self, func: F) -> Vec + where + F: FnMut(i64) -> T, + { + self.into_iter().map(func).collect() + } +}