├── .github └── workflows │ └── static.yml ├── .gitignore ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.png ├── google3aaf24fad574bf82.html ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── Game.js ├── components ├── DayButton.js ├── DayButtonGrid.js ├── ExplanationModal.js └── FooterBar.js ├── functions ├── useDidUpdate.js └── useHighScoreState.js ├── index.js ├── serviceWorker.js └── services └── storage.js /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | name: Deploy static content to Pages 2 | 3 | on: 4 | # Runs on pushes to the gh-pages branch 5 | push: 6 | branches: 7 | - gh-pages 8 | # Allows you to run this workflow manually from the Actions tab 9 | workflow_dispatch: 10 | 11 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 12 | permissions: 13 | contents: read 14 | pages: write 15 | id-token: write 16 | 17 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 18 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: false 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v4 33 | - name: Setup Pages 34 | uses: actions/configure-pages@v4 35 | - name: Upload artifact 36 | uses: actions/upload-pages-artifact@v3 37 | with: 38 | # Upload entire repository 39 | path: '.' 40 | - name: Deploy to GitHub Pages 41 | id: deployment 42 | uses: actions/deploy-pages@v4 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | /.idea 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ##### Learn how to find the day of the week of any date just using your brain. Web app works offline. 2 | 3 | ##### Learn more about the [doomsday algorithm](https://en.wikipedia.org/wiki/Doomsday_rule) 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "math-games", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "grommet": "^2.8.1", 7 | "grommet-icons": "^4.4.0", 8 | "moment": "^2.24.0", 9 | "moment-random": "^1.0.5", 10 | "react": "^16.12.0", 11 | "react-dom": "^16.12.0", 12 | "react-github-btn": "^1.2.1", 13 | "react-scripts": "^5.0.1", 14 | "styled-components": "^5.1.0" 15 | }, 16 | "scripts": { 17 | "start": "react-scripts start", 18 | "build": "react-scripts build", 19 | "test": "react-scripts test", 20 | "eject": "react-scripts eject", 21 | "predeploy": "npm run build", 22 | "deploy": "gh-pages -d build" 23 | }, 24 | "eslintConfig": { 25 | "extends": "react-app" 26 | }, 27 | "browserslist": { 28 | "production": [ 29 | ">0.2%", 30 | "not dead", 31 | "not op_mini all" 32 | ], 33 | "development": [ 34 | "last 1 chrome version", 35 | "last 1 firefox version", 36 | "last 1 safari version" 37 | ] 38 | }, 39 | "homepage": "https://grantas33.github.io/Doomsday-algorithm-practice", 40 | "devDependencies": { 41 | "gh-pages": "^2.1.1" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grantas33/Doomsday-algorithm-practice/c042ffa0805dfba93d0b06a724b37e4cf18ce467/public/favicon.png -------------------------------------------------------------------------------- /public/google3aaf24fad574bf82.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google3aaf24fad574bf82.html -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 18 | 19 | 28 | Learn the doomsday algorithm 29 | 34 | 35 | 36 | 37 |
38 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grantas33/Doomsday-algorithm-practice/c042ffa0805dfba93d0b06a724b37e4cf18ce467/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grantas33/Doomsday-algorithm-practice/c042ffa0805dfba93d0b06a724b37e4cf18ce467/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Learn the doomsday algorithm", 3 | "name": "Learn and practice the doomsday algorithm, which is used to find the day of the week of any date", 4 | "icons": [ 5 | { 6 | "src": "logo192.png", 7 | "type": "image/png", 8 | "sizes": "192x192" 9 | }, 10 | { 11 | "src": "logo512.png", 12 | "type": "image/png", 13 | "sizes": "512x512" 14 | } 15 | ], 16 | "start_url": ".", 17 | "display": "standalone", 18 | "theme_color": "#000000", 19 | "background_color": "#ffffff", 20 | "orientation": "portrait" 21 | } 22 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .century { color: red } 2 | .centuryIndex { color: dodgerblue } 3 | .year { color: #00ff82 } 4 | .yearIndex { color: hotpink } 5 | .nearestMultiple { color: greenyellow} 6 | .month { color: green } 7 | .day { color: darkorange } 8 | .doomsdayWeekDay { color: #999900 } 9 | .doomsday { color: blueviolet } 10 | 11 | @media (min-width: 768px) { 12 | .modal { 13 | width: fit-content; 14 | margin: auto; /* This centers the modal */ 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {Grommet} from 'grommet'; 3 | import Game from "./Game"; 4 | import './App.css'; 5 | const theme = { 6 | global: { 7 | font: { 8 | family: 'Solway' 9 | } 10 | }, 11 | }; 12 | 13 | function App() { 14 | 15 | return ( 16 | 17 | 18 | 19 | ); 20 | } 21 | 22 | export default App; 23 | -------------------------------------------------------------------------------- /src/Game.js: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useRef, useState} from 'react'; 2 | import {Box, Heading, Text} from 'grommet'; 3 | import FooterBar from "./components/FooterBar"; 4 | import DayButtonGrid from "./components/DayButtonGrid"; 5 | import {useHighScoreState} from "./functions/useHighScoreState"; 6 | import GitHubButton from 'react-github-btn' 7 | const moment = require('moment'); 8 | const momentRandom = require('moment-random'); 9 | 10 | function Game(props) { 11 | 12 | const generateRandomDay = () => momentRandom( 13 | moment(props.endDate), 14 | moment(props.startDate) 15 | ); 16 | 17 | const parseDateToWeekDayNumber = (date) => Number(date.format('d')); 18 | 19 | let initialDay = generateRandomDay(); 20 | const [currentDay, setCurrentDay] = useState(initialDay.format("Y-MM-DD")); 21 | const [score, setScore] = useState(0); 22 | const [highScore, setHighScore] = useHighScoreState(); 23 | const [selectedDayOfWeek, setSelectedDayOfWeek] = useState(); 24 | const [expectedDayOfWeek, setExpectedDayOfWeek] = useState(parseDateToWeekDayNumber(initialDay)); 25 | const [timeLeft, setTimeLeft] = useState(10); 26 | let timer = useRef(); 27 | 28 | useEffect(() => { 29 | if (timeLeft > 0 && selectedDayOfWeek === undefined) { 30 | timer.current = setTimeout(() => { 31 | setTimeLeft(t => t - 1) 32 | }, 1000) 33 | } else { 34 | clearTimeout(timer.current) 35 | } 36 | }, [timeLeft, selectedDayOfWeek]); 37 | 38 | const startNewRound = () => { 39 | let nextDay = generateRandomDay(); 40 | setCurrentDay(nextDay.format("Y-MM-DD")); 41 | if (selectedDayOfWeek === expectedDayOfWeek && timeLeft > 0) { 42 | setScore(score => score + 1); 43 | if (score + 1 > highScore) setHighScore(score + 1); 44 | } else { 45 | setScore(0) 46 | } 47 | setSelectedDayOfWeek(undefined); 48 | setExpectedDayOfWeek(parseDateToWeekDayNumber(nextDay)); 49 | setTimeLeft(10) 50 | }; 51 | 52 | return ( 53 | 54 | 55 | 60 | Highscore: {highScore} 61 | Score: {score} 62 | 63 | 70 | 0) ? {type: "pulse", size: "large", duration: 500} : {}} flex={{grow: 1}}> 71 | 72 | {timeLeft} 73 | 74 | 75 | 76 | 77 | {currentDay} 78 | 79 | 80 | 81 | 82 | 83 | 88 | Star 89 | 90 | 91 | 92 | ); 93 | } 94 | 95 | export default Game; 96 | -------------------------------------------------------------------------------- /src/components/DayButton.js: -------------------------------------------------------------------------------- 1 | import {Box, Text} from "grommet"; 2 | import React from "react"; 3 | import moment from "moment"; 4 | 5 | export default function DayButton(props) { 6 | 7 | const anySelected = props.selectedDayOfWeek !== undefined; 8 | 9 | const isSelectedButFalse = anySelected && 10 | props.selectedDayOfWeek !== props.expectedDayOfWeek && 11 | props.number === props.selectedDayOfWeek; 12 | 13 | const isCorrect = anySelected && 14 | props.number === props.expectedDayOfWeek; 15 | 16 | return props.number !== undefined ? 17 | { 26 | if (!anySelected) props.setSelectedDayOfWeek(props.number) 27 | }} 28 | > 29 | {moment.weekdays(props.number)} 30 | : 31 | ; 35 | } 36 | -------------------------------------------------------------------------------- /src/components/DayButtonGrid.js: -------------------------------------------------------------------------------- 1 | import {Box} from "grommet"; 2 | import DayButton from "./DayButton"; 3 | import React from "react"; 4 | 5 | export default function DayButtonGrid(props) { 6 | 7 | let selected = props.selectedDayOfWeek; 8 | let setSelected = props.setSelectedDayOfWeek; 9 | let expected = props.expectedDayOfWeek; 10 | 11 | return 18 | 24 | 25 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | 38 | 44 | 45 | 46 | 47 | 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/components/ExplanationModal.js: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Box, Heading, Text, Tip } from "grommet"; 3 | import * as moment from "moment"; 4 | 5 | const centuryIndexMap = { 18: 5, 19: 3, 20: 2, 21: 0 }; 6 | const doomsdaysForMonth = { 1: "03", 2: "14", 3: "07", 4: "04", 5: "09", 6: "06", 7: "11", 8: "08", 9: "05", 10: "10", 11: "07", 12: "12" }; 7 | const doomsdaysForMonthInLeapYear = { ...doomsdaysForMonth, 1: "04", 2: "15" }; 8 | 9 | export default function ExplanationModal(props) { 10 | 11 | let day = moment(props.currentDay); 12 | 13 | let centuryIndex = centuryIndexMap[day.format("Y").substring(0, 2)]; 14 | let year = Number(day.format("YY")); 15 | let yearAfter1Step = year % 2 === 1 ? (year + 11) : year; 16 | let yearAfter2Step = yearAfter1Step / 2; 17 | let yearAfter3Step = yearAfter2Step % 2 === 1 ? yearAfter2Step + 11 : yearAfter2Step; 18 | let nearestMultiple = yearAfter3Step % 7 === 0 ? yearAfter3Step : yearAfter3Step + 7 - (yearAfter3Step % 7); 19 | let weekDay = (nearestMultiple - yearAfter3Step + centuryIndex) % 7; 20 | let doomsday = day.isLeapYear() ? 21 | `${day.format("MM-")}${doomsdaysForMonthInLeapYear[day.format("M")]}` : 22 | `${day.format("MM-")}${doomsdaysForMonth[day.format("M")]}`; 23 | 24 | let dayNumber = Number(day.format("D")); 25 | let doomsdayNumber = Number(doomsday.substring(3)); 26 | let doomsdayToDayChain = []; 27 | 28 | while (Math.abs(dayNumber - doomsdayNumber) > 6) { 29 | doomsdayToDayChain.push(`${day.format("MM-")}${doomsdayNumber < 10 ? `0${doomsdayNumber}` : doomsdayNumber}`); 30 | if (doomsdayNumber < dayNumber) doomsdayNumber += 7; 31 | else doomsdayNumber -= 7; 32 | } 33 | if (doomsdayToDayChain.length > 0) { 34 | doomsdayToDayChain.push(`${day.format("MM-")}${doomsdayNumber < 10 ? `0${doomsdayNumber}` : doomsdayNumber}`); 35 | } 36 | 37 | return 38 | 39 | {day.format("Y").substring(0, 2)} 40 | {day.format("YY")}- 41 | {day.format("MM")}- 42 | {day.format("DD")} 43 | 44 | 45 | 48 |

1800s: 5

49 |

1900s: 3

50 |

2000s: 2

51 |

2100s: 0

52 | 53 | } 54 | dropProps={{ align: { top: 'bottom' } }} 55 | > 56 | Century index 57 |
for the {day.format("Y").substring(0, 2)}00s is {centuryIndex}. 58 |
59 | 60 | Calculating the year index for year {day.format("YY")} using 63 |

1. If odd, add 11

64 |

2. Divide by 2

65 |

3. If odd, add 11

66 | 67 | } 68 | dropProps={{ align: { top: 'bottom' } }} 69 | >"odd + 11" 70 |
method: 71 |
72 | 73 | {year !== yearAfter1Step && 74 | {year} is odd, adding 11: {year} + 11 = {yearAfter1Step}; 75 | } 76 | {yearAfter1Step} is even, dividing by 2: {yearAfter1Step} / 2 = {yearAfter2Step === yearAfter3Step ? <>{yearAfter2Step}. : `${yearAfter2Step};`} 77 | {yearAfter2Step !== yearAfter3Step && 78 | {yearAfter2Step} is odd, adding 11: {yearAfter2Step} + 11 = {yearAfter3Step}. 79 | } 80 | 81 | The nearest higher multiple of 7 to {yearAfter3Step} is {nearestMultiple}. 82 | Calculating the weekday of the doomsday: ({nearestMultiple} - {yearAfter3Step} + {centuryIndex}) mod 7 = {weekDay} ({moment.weekdays(weekDay)}). 83 | 84 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | {Object.entries(doomsdaysForMonth).map(([month, doomsday]) => ( 96 | 97 | 98 | 99 | 100 | ))} 101 | 102 |
MonthDoomsday
{month}{doomsday} {month === '1' && '(04 on leap years)'} {month === '2' && '(15 on leap years)'}
103 | 104 | } 105 | dropProps={{ align: { bottom: 'top' } }} 106 | > 107 | Doomsday 108 |
for {day.format("MMMM")} is {doomsday.substring(0, 3)}{doomsday.substring(3)}{day.isLeapYear() && (day.format("M") < 3) && (leap year)}. 109 |
110 | {doomsdayToDayChain.length > 0 && <> 111 | Selecting a doomsday closer to our date: 112 | 113 | 114 | {doomsdayToDayChain.map((date, index) => { 115 | if (index === doomsdayToDayChain.length - 1) return {date.substring(0, 3)}{date.substring(3)}.; 116 | return `${date} -> ` 117 | })} 118 | 119 | 120 | 121 | } 122 | {doomsdayNumber === dayNumber ? 123 | Our date matches the doomsday, and it is {day.format("dddd")}. : 124 | Calculating the day of the week: ({weekDay} {dayNumber > doomsdayNumber ? 125 | <> + ({dayNumber} - {doomsdayNumber}) : 126 | <> - ({doomsdayNumber} - {dayNumber}) 127 | }) mod 7 = {day.format("d")} ({day.format("dddd")}). 128 | } 129 |
130 | } -------------------------------------------------------------------------------- /src/components/FooterBar.js: -------------------------------------------------------------------------------- 1 | import React, {useState} from 'react'; 2 | import {Box, Button, Layer} from "grommet"; 3 | import useDidUpdate from "../functions/useDidUpdate"; 4 | import ExplanationModal from "./ExplanationModal"; 5 | 6 | export default function FooterBar(props) { 7 | 8 | const [fullyHidden, setFullyHidden] = useState(true); 9 | const [showExplanation, setShowExplanation] = React.useState(false); 10 | 11 | useDidUpdate(() => { 12 | setFullyHidden(false) 13 | }, [props.isVisible]); 14 | 15 | return 22 |