Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Value class deck #17718

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

Arthur-Milchior
Copy link
Member

I loved David's recent work in using value class. And thought that we could finally kill the overhead cost of DeepClone in JSONObject. So I transformed Deck into a value class, and added the needed properties so that the other part of the code can use Deck without considering it as a JSONObject.

@mikehardy mikehardy added the Needs Author Reply Waiting for a reply from the original author label Jan 6, 2025
@Arthur-Milchior Arthur-Milchior added Needs Review and removed Needs Author Reply Waiting for a reply from the original author Has Conflicts labels Jan 8, 2025
Copy link
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question on the 1 and 2 discrepancy

@david-allison david-allison added Needs Author Reply Waiting for a reply from the original author and removed Needs Review labels Jan 16, 2025
@Arthur-Milchior Arthur-Milchior removed Needs Author Reply Waiting for a reply from the original author Has Conflicts labels Jan 16, 2025
@Arthur-Milchior
Copy link
Member Author

Test added for each property

@Arthur-Milchior Arthur-Milchior force-pushed the value_class_deck branch 2 times, most recently from 292c595 to 4f95714 Compare January 16, 2025 23:52
Copy link
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The typings for a number of properties are incorrect, and I'm concerned.

previewGoodSecs is an Int. if you insert it into JSON as a String, it's quoted

}

"limit" -> {
ar.getJSONArray(0).put(1, value)
deck.firstFilter.limit = value as String
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a string?

Copy link
Member Author

@Arthur-Milchior Arthur-Milchior Feb 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the reason is that I misunderstood the way this part of the code worked. I thought all values were strings unless converted to other values. I'll update all of that

@Arthur-Milchior Arthur-Milchior force-pushed the value_class_deck branch 3 times, most recently from c9e9db4 to b7ba1ae Compare January 17, 2025 03:12
@Arthur-Milchior

This comment was marked as resolved.

@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Jan 17, 2025
@david-allison david-allison added Needs Review and removed Needs Author Reply Waiting for a reply from the original author labels Jan 18, 2025
@david-allison david-allison self-requested a review January 18, 2025 03:12
david-allison

This comment was marked as resolved.

@david-allison david-allison added the Needs Author Reply Waiting for a reply from the original author label Jan 18, 2025
Copy link
Contributor

github-actions bot commented Feb 9, 2025

Hello 👋, this PR has had no activity for more than 2 weeks and needs a reply from the author. If you think this is a mistake please comment and ping a maintainer to get this merged ASAP! Thanks for contributing! You have 7 days until this gets closed automatically

@github-actions github-actions bot added Stale and removed Stale labels Feb 9, 2025
@Arthur-Milchior Arthur-Milchior force-pushed the value_class_deck branch 4 times, most recently from d4d5bac to b397440 Compare February 11, 2025 17:54
@Arthur-Milchior Arthur-Milchior added Needs Review and removed Needs Author Reply Waiting for a reply from the original author Has Conflicts labels Feb 11, 2025
Copy link
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's been a while, but I don't feel my comment introducing a regression test for bugs that this PR previously had was merged:

#17718 (review)

Comment on lines 214 to 247
deck = if (extras?.containsKey("did") == true) {
col.decks.get(extras.getLong("did"))
} else {
null
} ?: col.decks.current()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

        deck = extras?.getNullableLong("did")?.let { col.decks.get(it) } ?: col.decks.current()

AnkiDroid/src/main/java/com/ichi2/libanki/NormalDeck.kt Outdated Show resolved Hide resolved
@Arthur-Milchior
Copy link
Member Author

Sorry for missing your test. It's done

The first interest of this change is that we are not deep cloning all
decks when creating a Deck object which should save a little bit of
time, especially loading time on collection with a lot of decks.

Instead, we have a better typing at zero runtime cost.

The second interest is that we are certain that the fact that the Deck
is implemented as a JSONObject is hidden from the developer using this
class. Instead, they can only use the provided extension, or add the
one they needs.

While most changes are relatively trivial, moving the implementation
from everywhere in the code to Deck.kt, there are some extra changes
that were needed.

I moved `getLongOrNull` to PythonExtension given that it implements
the behavior of a Python function we want to reuse, this seems more
consistent.

As value class can't be lateinit, I hardcoded
AppCompatPreferenceActivity as being a getter for the backing field
`_deck: Deck?`.

I added in testutils/JsonUtils helper method that accepts a json
holder, and a string representing the expected json, to simplify the
use of those methods.

The code used sometime `optLong("id")` and sometime
`getLong("id")`. There seems to be no reason for the difference
between both use case, and just be random choice from the
developer. Anyway, if a deck lacks an id, any place using
`getLong("id")` would have caused the code to crash, so replacing the
`optLong` by `getLong` would, in the worst case, move the time at
which the user is asked to clean their database.

There seems to be a single key we sometime set to JSONObject.NULL, it
was `delays`. For the sake of typing, the property `delays` in `deck`
is thus of type `JSONArray?`. When the value `null` is assigned to
this property, `NULL` is saved in the object, and reciprocally.

There is no need to check whether `deck` has "empty" key before
removing it. It's not even more efficient.
Deck is now declared as an interface so that it can be
inherited. `Deck` contains a facotry to decide whether to return a
normal deck or a filtered one.
@Arthur-Milchior Arthur-Milchior force-pushed the value_class_deck branch 2 times, most recently from 00541f5 to 6e2969e Compare February 14, 2025 03:13
@Arthur-Milchior
Copy link
Member Author

I removed your checkResult on startRegularActivity. It caused an unrelated lint failure. I don't understand enough and don't want to fix it in this PR

Initial implementation by David

This test ensures that every changes possibly done in the deck options
are correctly applied

Co-authored-by: David Allison <[email protected]>
Copy link
Member

@david-allison david-allison left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only a few string fixes, doesn't block reviewer 2

} catch (e: RuntimeException) {
Timber.e(e, "RuntimeException on saving deck")
Timber.e(e, "RuntimeException on saving filteredDeck")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert

commit()
}
}
}
}

// save deck
// save filteredDeck
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

revert

registerExternalStorageListener()
if (deck.isNormal) {
Timber.w("Deck is not a dyn deck")
Timber.w("Deck is not a dyn filteredDeck")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Timber.w("Deck is not a dyn filteredDeck")
Timber.w("Deck is not filtered")

}
}
addPreferences(col)
buildLists()
updateSummaries()
}

// Set the activity title to include the name of the deck
// Set the activity title to include the name of the filteredDeck
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Set the activity title to include the name of the filteredDeck
// Set the activity title to include the name of the deck

@@ -255,13 +286,13 @@ class FilteredDeckOptions :
private fun addPreferences(col: Collection) {
addPreferencesFromResource(R.xml.cram_deck_options)
if (col.schedVer() != 1) {
Timber.d("sched v2: removing filtered deck custom study steps")
Timber.d("sched v2: removing filtered filteredDeck custom study steps")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Timber.d("sched v2: removing filtered filteredDeck custom study steps")
Timber.d("sched v2: removing filtered deck custom study steps")

@@ -280,9 +311,9 @@ class FilteredDeckOptions :

override fun closeWithResult() {
if (prefChanged) {
// Rebuild the filtered deck if a setting has changed
// Rebuild the filtered filteredDeck if a setting has changed
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Rebuild the filtered filteredDeck if a setting has changed
// Rebuild the filtered deck if a setting has changed


fun isFiltered(jsonObject: JSONObject) = jsonObject.getInt(DYN) != 0

fun factory(jsonObject: JSONObject): Deck = if (isFiltered(jsonObject)) FilteredDeck(jsonObject) else RegularDeck(jsonObject)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doccomment on these factories

if (filtered) {
private fun newDeckLegacy(filtered: Boolean) =
Deck.factory(BackendUtils.fromJsonBytes(col.backend.newDeckLegacy(filtered))).apply {
if (this is FilteredDeck) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm... I'd use filtered as the boolean, then require(this is Filtered) { "error" }

@@ -378,7 +373,13 @@ class Decks(
/** Falls back on default config if deck or config missing */
@LibAnkiAlias("config_dict_for_deck_id")
fun configDictForDeckId(did: DeckId): DeckConfig {
val conf = get(did)?.conf ?: 1
val deck = get(did)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this work:

        val conf = (get(did) as? RegularDeck?)?.conf ?: 1

*
* This better approximates `JSON.get` in the Python
*/
fun JSONObject.getLongOrNull(key: String): Long? {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very similar to a method in java/com/ichi2/anki/utils/ext/JSONObject.kt

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants