mirror of
				https://github.com/keanuplayz/TravBot-v3.git
				synced 2024-08-15 02:33:12 +00:00 
			
		
		
		
	Fixed time setup accounting for differences in day
This commit is contained in:
		
							parent
							
								
									8da5ad0ca6
								
							
						
					
					
						commit
						d7c18d1b06
					
				
					 2 changed files with 184 additions and 71 deletions
				
			
		|  | @ -4,8 +4,10 @@ import {User} from "discord.js"; | |||
| import moment from "moment"; | ||||
| 
 | ||||
| const DATE_FORMAT = "D MMMM YYYY"; | ||||
| const DOW_FORMAT = "dddd"; | ||||
| const TIME_FORMAT = "HH:mm:ss"; | ||||
| type DST = "na" | "eu" | "sh"; | ||||
| const TIME_EMBED_COLOR = 0x191970; | ||||
| 
 | ||||
| const DAYLIGHT_SAVINGS_REGIONS: {[region in DST]: string} = { | ||||
|     na: "North America", | ||||
|  | @ -16,12 +18,12 @@ const DAYLIGHT_SAVINGS_REGIONS: {[region in DST]: string} = { | |||
| const DST_NOTE_INFO = `*Note: To make things simple, the way the bot will handle specific points in time when switching Daylight Savings is just to switch at UTC 00:00, ignoring local timezones. After all, there's no need to get this down to the exact hour.*
 | ||||
| 
 | ||||
| North America | ||||
| - Starts: 2nd Sunday March | ||||
| - Ends: 1st Sunday November | ||||
| - Starts: 2nd Sunday of March | ||||
| - Ends: 1st Sunday of November | ||||
| 
 | ||||
| Europe | ||||
| - Starts: Last Sunday March | ||||
| - Ends: Last Sunday October | ||||
| - Starts: Last Sunday of March | ||||
| - Ends: Last Sunday of October | ||||
| 
 | ||||
| Southern Hemisphere | ||||
| - Starts: 1st Sunday of October | ||||
|  | @ -30,12 +32,12 @@ Southern Hemisphere | |||
| const DST_NOTE_SETUP = `Which daylight savings region most closely matches your own?
 | ||||
| 
 | ||||
| North America (1️⃣) | ||||
| - Starts: 2nd Sunday March | ||||
| - Ends: 1st Sunday November | ||||
| - Starts: 2nd Sunday of March | ||||
| - Ends: 1st Sunday of November | ||||
| 
 | ||||
| Europe (2️⃣) | ||||
| - Starts: Last Sunday March | ||||
| - Ends: Last Sunday October | ||||
| - Starts: Last Sunday of March | ||||
| - Ends: Last Sunday of October | ||||
| 
 | ||||
| Southern Hemisphere (3️⃣) | ||||
| - Starts: 1st Sunday of October | ||||
|  | @ -99,6 +101,7 @@ function hasDaylightSavings(region: DST) { | |||
| function getTimeEmbed(user: User) { | ||||
|     const {timezone, daylightSavingsRegion} = Storage.getUser(user.id); | ||||
|     let localDate = "N/A"; | ||||
|     let dayOfWeek = "N/A"; | ||||
|     let localTime = "N/A"; | ||||
|     let timezoneOffset = "N/A"; | ||||
| 
 | ||||
|  | @ -107,13 +110,14 @@ function getTimeEmbed(user: User) { | |||
|         const daylightTimezone = timezone + daylightSavingsOffset; | ||||
|         const now = moment().utcOffset(daylightTimezone * 60); | ||||
|         localDate = now.format(DATE_FORMAT); | ||||
|         dayOfWeek = now.format(DOW_FORMAT); | ||||
|         localTime = now.format(TIME_FORMAT); | ||||
|         timezoneOffset = daylightTimezone > 0 ? `+${daylightTimezone}` : daylightTimezone.toString(); | ||||
|         timezoneOffset = daylightTimezone >= 0 ? `+${daylightTimezone}` : daylightTimezone.toString(); | ||||
|     } | ||||
| 
 | ||||
|     const embed = { | ||||
|         embed: { | ||||
|             color: 0x000080, | ||||
|             color: TIME_EMBED_COLOR, | ||||
|             author: { | ||||
|                 name: user.username, | ||||
|                 icon_url: user.displayAvatarURL({ | ||||
|  | @ -126,12 +130,16 @@ function getTimeEmbed(user: User) { | |||
|                     name: "Local Date", | ||||
|                     value: localDate | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Day of the Week", | ||||
|                     value: dayOfWeek | ||||
|                 }, | ||||
|                 { | ||||
|                     name: "Local Time", | ||||
|                     value: localTime | ||||
|                 }, | ||||
|                 { | ||||
|                     name: timezone !== null ? "Current Timezone Offset" : "Timezone Offset", | ||||
|                     name: daylightSavingsRegion !== null ? "Current Timezone Offset" : "Timezone Offset", | ||||
|                     value: timezoneOffset | ||||
|                 }, | ||||
|                 { | ||||
|  | @ -160,14 +168,19 @@ function getTimeEmbed(user: User) { | |||
| 
 | ||||
| export default new Command({ | ||||
|     description: "Show others what time it is for you.", | ||||
|     aliases: ["tz"], | ||||
|     async run({channel, author}) { | ||||
|         channel.send(getTimeEmbed(author)); | ||||
|     }, | ||||
|     subcommands: { | ||||
|         // Welcome to callback hell. We hope you enjoy your stay here!
 | ||||
|         setup: new Command({ | ||||
|             description: "Registers your timezone information for the bot.", | ||||
|             async run({author, channel, ask, askYesOrNo, askMultipleChoice, prompt}) { | ||||
|             async run({author, channel, ask, askYesOrNo, askMultipleChoice}) { | ||||
|                 const profile = Storage.getUser(author.id); | ||||
|                 profile.timezone = null; | ||||
|                 profile.daylightSavingsRegion = null; | ||||
|                 let hour: number; | ||||
| 
 | ||||
|                 ask( | ||||
|                     await channel.send( | ||||
|  | @ -175,26 +188,111 @@ export default new Command({ | |||
|                     ), | ||||
|                     author.id, | ||||
|                     (reply) => { | ||||
|                         const hour = parseInt(reply); | ||||
|                         hour = parseInt(reply); | ||||
| 
 | ||||
|                         if (isNaN(hour)) { | ||||
|                             return false; | ||||
|                         } | ||||
| 
 | ||||
|                         const isValidHour = hour >= 0 && hour <= 23; | ||||
| 
 | ||||
|                         if (isValidHour) { | ||||
|                             const date = new Date(); | ||||
|                             profile.timezone = hour - date.getUTCHours(); | ||||
|                         } | ||||
| 
 | ||||
|                         return isValidHour; | ||||
|                         return hour >= 0 && hour <= 23; | ||||
|                     }, | ||||
|                     async () => { | ||||
|                         askYesOrNo( | ||||
|                         // You need to also take into account whether or not it's the same day in UTC or not.
 | ||||
|                         // The problem this setup avoids is messing up timezones by 24 hours.
 | ||||
|                         // For example, the old logic was just (hour - hourUTC). When I setup my timezone (UTC-6) at 18:00, it was UTC 00:00.
 | ||||
|                         // That means that that formula was doing (18 - 0) getting me UTC+18 instead of UTC-6 because that naive formula didn't take into account a difference in days.
 | ||||
| 
 | ||||
|                         // (day * 24 + hour) - (day * 24 + hour)
 | ||||
|                         // Since the timezones will be restricted to -12 to +14, you'll be given three options.
 | ||||
|                         // The end of the month should be calculated automatically, you should have enough information at that point.
 | ||||
| 
 | ||||
|                         // But after mapping out the edge cases, I figured out that you can safely gather accurate information just based on whether the UTC day matches the user's day.
 | ||||
|                         // 21:xx (-12, -d) -- 09:xx (+0, 0d) -- 23:xx (+14, 0d)
 | ||||
|                         // 22:xx (-12, -d) -- 10:xx (+0, 0d) -- 00:xx (+14, +d)
 | ||||
|                         // 23:xx (-12, -d) -- 11:xx (+0, 0d) -- 01:xx (+14, +d)
 | ||||
|                         // 00:xx (-12, 0d) -- 12:xx (+0, 0d) -- 02:xx (+14, +d)
 | ||||
| 
 | ||||
|                         // For example, 23:xx (-12) - 01:xx (+14) shows that even the edge cases of UTC-12 and UTC+14 cannot overlap, so the dataset can be reduced to a yes or no option.
 | ||||
|                         // - 23:xx same day = +0, 23:xx diff day = -1
 | ||||
|                         // - 00:xx same day = +0, 00:xx diff day = +1
 | ||||
|                         // - 01:xx same day = +0, 01:xx diff day = +1
 | ||||
| 
 | ||||
|                         // First, create a tuple list matching each possible hour-dayOffset-timezoneOffset combination. In the above example, it'd go something like this:
 | ||||
|                         // [[23, -1, -12], [0, 0, -11], ..., [23, 0, 12], [0, 1, 13], [1, 1, 14]]
 | ||||
|                         // Then just find the matching one by filtering through dayOffset (equals zero or not), then the hour from user input.
 | ||||
|                         // Remember that you don't know where the difference in day might be at this point, so you can't do (hour - hourUTC) safely.
 | ||||
|                         // In terms of the equation, you're missing a variable in (--> day <-- * 24 + hour) - (day * 24 + hour). That's what the loop is for.
 | ||||
| 
 | ||||
|                         // Now you might be seeing a problem with setting this up at the end/beginning of a month, but that actually isn't a problem.
 | ||||
|                         // Let's say that it's 00:xx of the first UTC day of a month. hourSumUTC = 24
 | ||||
|                         // UTC-12 --> hourSumLowerBound (hourSumUTC - 12) = 12
 | ||||
|                         // UTC+14 --> hourSumUpperBound (hourSumUTC + 14) = 38
 | ||||
|                         // Remember, the nice thing about making these bounds relative to the UTC hour sum is that there can't be any conflicts even at the edges of months.
 | ||||
|                         // And remember that we aren't using this question: (day * 24 + hour) - (day * 24 + hour). We're drawing from a list which does not store its data in absolute terms.
 | ||||
|                         // That means there's no 31st, 1st, 2nd, it's -1, 0, +1. I just need to make sure to calculate an offset to subtract from the hour sums.
 | ||||
| 
 | ||||
|                         const date = new Date(); // e.g. 2021-05-01 @ 05:00
 | ||||
|                         const day = date.getUTCDate(); // e.g. 1
 | ||||
|                         const hourSumUTC = day * 24 + date.getUTCHours(); // e.g. 29
 | ||||
|                         const timezoneTupleList: [number, number, number][] = []; | ||||
|                         const uniques: number[] = []; // only for temporary use
 | ||||
|                         const duplicates = []; | ||||
| 
 | ||||
|                         // Setup the tuple list in a separate block.
 | ||||
|                         for (let timezoneOffset = -12; timezoneOffset <= 14; timezoneOffset++) { | ||||
|                             const hourSum = hourSumUTC + timezoneOffset; // e.g. 23, UTC-6 (17 to 43)
 | ||||
|                             const hour = hourSum % 24; // e.g. 23
 | ||||
|                             // This works because you get the # of days w/o hours minus UTC days without hours.
 | ||||
|                             // Since it's all relative to UTC, it'll end up being -1, 0, or 1.
 | ||||
|                             const dayOffset = Math.floor(hourSum / 24) - day; // e.g. -1
 | ||||
|                             timezoneTupleList.push([hour, dayOffset, timezoneOffset]); | ||||
| 
 | ||||
|                             if (uniques.includes(hour)) { | ||||
|                                 duplicates.push(hour); | ||||
|                             } else { | ||||
|                                 uniques.push(hour); | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         // I calculate the list beforehand and check for duplicates to reduce unnecessary asking.
 | ||||
|                         if (duplicates.includes(hour)) { | ||||
|                             const isSameDay = await askYesOrNo( | ||||
|                                 await channel.send( | ||||
|                                     `Is the current day of the month the ${moment().utc().format("Do")} for you?` | ||||
|                                 ), | ||||
|                                 author.id | ||||
|                             ); | ||||
| 
 | ||||
|                             // Filter through isSameDay (aka !hasDayOffset) then hour from user-generated input.
 | ||||
|                             // isSameDay is checked first to reduce the amount of conditionals per loop.
 | ||||
|                             if (isSameDay) { | ||||
|                                 for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { | ||||
|                                     if (dayOffset === 0 && hour === hourPoint) { | ||||
|                                         profile.timezone = timezoneOffset; | ||||
|                                     } | ||||
|                                 } | ||||
|                             } else { | ||||
|                                 for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { | ||||
|                                     if (dayOffset !== 0 && hour === hourPoint) { | ||||
|                                         profile.timezone = timezoneOffset; | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } else { | ||||
|                             // If it's a unique hour, just search through the tuple list and find the matching entry.
 | ||||
|                             for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { | ||||
|                                 if (hour === hourPoint) { | ||||
|                                     profile.timezone = timezoneOffset; | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
| 
 | ||||
|                         // I should note that error handling should be added sometime because await throws an exception on Promise.reject.
 | ||||
|                         const hasDST = await askYesOrNo( | ||||
|                             await channel.send("Does your timezone change based on daylight savings?"), | ||||
|                             author.id, | ||||
|                             async (hasDST) => { | ||||
|                             author.id | ||||
|                         ); | ||||
| 
 | ||||
|                         const finalize = () => { | ||||
|                             Storage.save(); | ||||
|                             channel.send( | ||||
|  | @ -209,7 +307,7 @@ export default new Command({ | |||
| 
 | ||||
|                                 // If daylight savings is active, subtract the timezone offset by one to store the standard time.
 | ||||
|                                 if (hasDaylightSavings(region)) { | ||||
|                                             (profile.timezone as number)--; | ||||
|                                     profile.timezone!--; | ||||
|                                 } | ||||
| 
 | ||||
|                                 finalize(); | ||||
|  | @ -223,12 +321,8 @@ export default new Command({ | |||
|                         } else { | ||||
|                             finalize(); | ||||
|                         } | ||||
|                             } | ||||
|                         ); | ||||
|                     }, | ||||
|                     () => { | ||||
|                         return "you need to enter in a valid integer between 0 to 23"; | ||||
|                     } | ||||
|                     () => "you need to enter in a valid integer between 0 to 23" | ||||
|                 ); | ||||
|             } | ||||
|         }), | ||||
|  | @ -256,12 +350,16 @@ export default new Command({ | |||
| 
 | ||||
|                 channel.send({ | ||||
|                     embed: { | ||||
|                         color: 0x000080, | ||||
|                         color: TIME_EMBED_COLOR, | ||||
|                         fields: [ | ||||
|                             { | ||||
|                                 name: "Local Date", | ||||
|                                 value: time.format(DATE_FORMAT) | ||||
|                             }, | ||||
|                             { | ||||
|                                 name: "Day of the Week", | ||||
|                                 value: time.format(DOW_FORMAT) | ||||
|                             }, | ||||
|                             { | ||||
|                                 name: "Local Time", | ||||
|                                 value: time.format(TIME_FORMAT) | ||||
|  |  | |||
|  | @ -46,7 +46,7 @@ export interface CommonLibrary { | |||
|         onReject: () => string, | ||||
|         timeout?: number | ||||
|     ) => void; | ||||
|     askYesOrNo: (message: Message, senderID: string, onSuccess: (condition: boolean) => void, timeout?: number) => void; | ||||
|     askYesOrNo: (message: Message, senderID: string, timeout?: number) => Promise<boolean>; | ||||
|     askMultipleChoice: ( | ||||
|         message: Message, | ||||
|         senderID: string, | ||||
|  | @ -196,6 +196,8 @@ export function updateGlobalEmoteRegistry(): void { | |||
|     FileManager.write("emote-registry", data, true); | ||||
| } | ||||
| 
 | ||||
| // Maybe promisify this section to reduce the potential for creating callback hell? Especially if multiple questions in a row are being asked.
 | ||||
| 
 | ||||
| // Pagination function that allows for customization via a callback.
 | ||||
| // Define your own pages outside the function because this only manages the actual turning of pages.
 | ||||
| $.paginate = async ( | ||||
|  | @ -315,7 +317,8 @@ $.ask = async ( | |||
|     }, timeout); | ||||
| }; | ||||
| 
 | ||||
| $.askYesOrNo = async (message: Message, senderID: string, onSuccess: (condition: boolean) => void, timeout = 30000) => { | ||||
| $.askYesOrNo = (message: Message, senderID: string, timeout = 30000): Promise<boolean> => { | ||||
|     return new Promise(async (resolve, reject) => { | ||||
|         let isDeleted = false; | ||||
| 
 | ||||
|         await message.react("✅"); | ||||
|  | @ -326,7 +329,7 @@ $.askYesOrNo = async (message: Message, senderID: string, onSuccess: (condition: | |||
|                     const isCheckReacted = reaction.emoji.name === "✅"; | ||||
| 
 | ||||
|                     if (isCheckReacted || reaction.emoji.name === "❌") { | ||||
|                     onSuccess(isCheckReacted); | ||||
|                         resolve(isCheckReacted); | ||||
|                         isDeleted = true; | ||||
|                         message.delete(); | ||||
|                     } | ||||
|  | @ -337,14 +340,26 @@ $.askYesOrNo = async (message: Message, senderID: string, onSuccess: (condition: | |||
|             {time: timeout} | ||||
|         ); | ||||
| 
 | ||||
|     if (!isDeleted) message.delete(); | ||||
|         if (!isDeleted) { | ||||
|             message.delete(); | ||||
|             reject("Prompt timed out."); | ||||
|         } | ||||
|     }); | ||||
| }; | ||||
| 
 | ||||
| // This MUST be split into an array. These emojis are made up of several characters each, adding up to 29 in length.
 | ||||
| const multiNumbers = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"]; | ||||
| 
 | ||||
| // This will bring up an option to let the user choose between one option out of many.
 | ||||
| // This definitely needs a single callback alternative, because using the numerical version isn't actually that uncommon of a pattern.
 | ||||
| $.askMultipleChoice = async (message: Message, senderID: string, callbackStack: (() => void)[], timeout = 90000) => { | ||||
|     if (callbackStack.length > multiNumbers.length) { | ||||
|         message.channel.send( | ||||
|             `\`ERROR: The amount of callbacks in "askMultipleChoice" must not exceed the total amount of allowed options (${multiNumbers.length})!\`` | ||||
|         ); | ||||
|         return; | ||||
|     } | ||||
| 
 | ||||
|     let isDeleted = false; | ||||
| 
 | ||||
|     for (let i = 0; i < callbackStack.length; i++) { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue