Shuffle Cards and Display Selected Subjects
jacobwicks
Posted on January 17, 2020
As you save more cards you'll notice that the cards are presented in the same order every time. Let's fix that.
Write the Shuffle Code
File: src/services/CardContext/services/index.ts
Will Match: src/services/CardContext/services/complete/index-3.ts
A good algorithm to shuffle an array is Fisher-Yates. Here's a short article about Fisher-Yates: How to Correctly Shuffle an Array in Javascript.
Add the shuffle function:
//https://medium.com/@nitinpatel_20236/how-to-shuffle-correctly-shuffle-an-array-in-javascript-15ea3f84bfb
const shuffle = (array: any[]) => {
if (array.length > 0) {
for(let i: number = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * i)
const temp = array[i]
array[i] = array[j]
array[j] = temp
}
};
return array;
};
Call shuffle
when you generate the initialState
:
//a function that loads the cards from localStorage
//and returns a CardState object
export const getInitialState = () => ({
//the cards that are displayed to the user
//if loadedCards is undefined, use cards
cards: loadedCards ? shuffle(loadedCards) : cards,
//index of the currently displayed card
current: 0,
//placeholder for the dispatch function
dispatch: (action:CardAction) => undefined,
//the array of subjects to show the user
show: []
} as CardState);
Now the cards will be shuffled. You can refresh to shuffle the cards while using the app. This works because every time you refresh the app it loads the cards from localStorage.
Display Only Selected Subjects
Now that the App has the Selector
component, the user can choose subjects. We are going to use the show
array to only show the user cards from the subjects that the user has selected. We will do this by rewriting the code in the next
case in the CardContext
reducer
. We'll make a function that takes the current
index, the show
array, and the array of cards
, and returns the next index. But instead of returning the next card in the array of all cards, the function will limit its array to just cards with the selected subjects.
Test
File: src/services/CardContext/services/index.test.ts
Will Match: src/services/CardContext/services/complete/test-2.ts
I'm not going to do the complete back and forth Red/Green Pass/Fail for these tests. It's been a long tutorial. But try it yourself!
Import Card from types.
import { Card } from '../../../types';
Write the tests. We use describe blocks to keep variables/helper functions in scope.
describe('getNext', () => {
//the getNext function that we're testing
const { getNext } = require('./index');
//a helper function. Will generate a Card object from a seed
//if provided a subject, that will be the card subject
const getCard = (
seed: string | number,
subject?: string | number
) => ({
question: `${seed}?`,
answer: `${seed}!`,
subject: subject ? `${subject}` : `${seed}`
});
//an array from 0-4. We'll use it to generate some arrays for tests
const seeds = [0, 1, 2, 3, 4];
//test that getNext works when show is empty
describe('show is empty', () => {
//now we have an array of cards 0-4
const cards = seeds.map(seed => getCard(seed));
//show is an empty array of strings
const show: string[] = [];
//the result for incrementing the last index in an array is 0, not current + 1
//so that's a different test. We're only running 0, 1, 2, 3 here
test.each(seeds.slice(0, 3))('increments current from %d',
//name the arguments, same order as in the array we generated
//renaming 'seed' to 'current'
(current) => {
const next = getNext({
cards,
current,
show
});
//when current is < last index in current, next should be current + 1
expect(next).toBe(current + 1);
});
it('returns 0 when current is last index of cards', () => {
const next = getNext({
cards,
current: 4,
show
});
//the next index goes back to 0.
//If it returned current + 1, or 5, that would be an invalid index
expect(next).toBe(0);
});
});
describe('show single subject', () => {
const selectedSubject = 'selectedSubject';
//show is now an array with one string in it
const show: string[] = [selectedSubject];
it('shows only cards from the selected subject', () => {
//generate an array of cards
const cards = seeds.map(seed =>
//seed modulus 2 returns the remainder of dividing the seed number by 2
//when the remainder is not zero, we'll generate a card from the seed
//but the subject will just be the seed, not the selected subject
//when the remainder is 0, we'll get a card with the selected subject
seed % 2
? getCard(seed)
: getCard(seed, selectedSubject));
//the % 2 of 0, 2, and 4 are all 0
//so the cards generated from 0, 2, and 4 should have subject === selectedSubject
//so cards[0, 2, 4] should have the selected sujbject
//we expect filtering cards for cards with selectedSubject will have a length of 3
expect(cards.filter(card => card.subject === selectedSubject)).toHaveLength(3);
let current = 0;
//use a for loop to get next 5 times
//each time, we should get the index of a card with the selected subject
for(let i: number = 0; i < 5; i++) {
const next = getNext({ cards, current, show});
expect(cards[next].subject).toEqual(selectedSubject);
current = next;
}
});
});
describe('show multiple subjects', () => {
//now show is an array of 3 strings
const show: string[] = [
'firstSubject',
'secondSubject',
'thirdSubject'
];
//a function to return a randomly chosen subject from the show array
const randomSubject = () => show[Math.floor(Math.random() * Math.floor(3))];
//an empty array.
//we'll use a for loop to generate cards to fill it up
const manyCards: Card[] = [];
//We'll put 21 cards into manyCards
for(let seed = 0; seed < 21; seed++) {
//modulus 3 this time, just to switch things up
seed % 3
? manyCards.push(getCard(seed))
: manyCards.push(getCard(seed, randomSubject()))
}
it('shows only cards from the selected subject', () => {
//to get the number of times to run getNext, we'll cound how many cards in ManyCards
//have a subject from the show array
//it's going to be 7 (21/3)
//but if you were using more unknown numbers, you might want to find it out dynamically
const times = manyCards.filter(card => show.includes(card.subject)).length;
let current = 0;
//use a for loop to assert that you always see a card with the selected subject
//you can run through it as many times as you want
//you could do i < times * 2 to run through it twice
for(let i: number = 0; i < times; i++) {
const next = getNext({ cards: manyCards, current, show});
expect(show).toContain(manyCards[next].subject);
current = next;
};
});
})
Great. Now we are testing all the aspects of the getNext
function that we need to. Let's write it!
Write getNext
File: src/services/CardContext/services/index.tsx
Will Match: src/services/CardContext/services/complete/index-4.tsx
Write the getNext
function. getNext
will take the array of cards
, the current
index, and the array of subjects. It uses Array.filter
to create a new array of cards that belong to the selected subjects. Then it finds the current card in that array. Then it gets the question from the card one index higher than the current card. Then it finds the index of that next card in the array of all cards by looking for the question on the card. It returns the index of the next card in the array of all cards.
export const getNext = ({
cards,
current,
show
}:{
cards: Card[],
current: number,
show: string[]
}) => {
//show array is empty, so we are showing all card
if (show.length === 0) {
const total = cards.length -1;
//just add 1, if +1 is too big return 0
const next = current + 1 <= total
? current + 1
: 0;
return next;
} else {
//filter cards. Only keep cards with a subject that's in show
const showCards = cards
.filter(card => show.includes(card.subject));
//get the index of the current card in the showCards array
const showCurrent = showCards
.findIndex(card => card.question === cards[current].question)
const showTotal = showCards.length - 1;
//showNext gives us the next index in the showcards array
const showNext = showCurrent + 1 <= showTotal
? showCurrent + 1
: 0;
//translate the showNext index to the index of the same card in cards
const next = cards
.findIndex(card => card.question === showCards[showNext].question);
return next;
};
};
CardContext Reducer
File: src/services/CardContext/index.tsx
Will Match: src/services/CardContext/complete/index-14.tsx
Add an import for getNext
.
import { getInitialState, getNext } from './services/';
Change the next
case of the reducer
to call getNext
:
case 'next': {
const { cards, current, show } = state;
//call to the getNext function
const next = getNext({
cards,
current,
show,
});
return {
...state,
current: next
}
}
Now the app will only show cards from the subjects that the user chooses with the selector.
Run all the tests:
That's it!
In the next tutorial I plan to write, I will show you how to save and load the flashcards to JSON files.
Posted on January 17, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.