DEV Community

loading...
Cover image for Shuffle Cards and Display Selected Subjects

Shuffle Cards and Display Selected Subjects

jacobwicks
Legal Aid Lawyer turned fullstack developer. Passionate about improving the user experience by developing efficient and beautiful apps.
・6 min read

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;
    };
};

GetNext Pass

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:

All Pass 2

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.

Discussion (0)