2021-01-24 14:07:58 +00:00
import Command from "../../core/command" ;
2021-03-30 10:25:07 +00:00
import { ask , askYesOrNo , askMultipleChoice , prompt , callMemberByUsername } from "../../core/libd" ;
2021-01-24 14:07:58 +00:00
import { Storage } from "../../core/structures" ;
import { User } from "discord.js" ;
import moment from "moment" ;
const DATE_FORMAT = "D MMMM YYYY" ;
2021-01-25 04:46:48 +00:00
const DOW_FORMAT = "dddd" ;
2021-01-24 14:07:58 +00:00
const TIME_FORMAT = "HH:mm:ss" ;
type DST = "na" | "eu" | "sh" ;
2021-01-25 04:46:48 +00:00
const TIME_EMBED_COLOR = 0x191970 ;
2021-01-24 14:07:58 +00:00
const DAYLIGHT_SAVINGS_REGIONS : { [ region in DST ] : string } = {
na : "North America" ,
eu : "Europe" ,
sh : "Southern Hemisphere"
} ;
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
2021-01-25 04:46:48 +00:00
- Starts : 2nd Sunday of March
- Ends : 1st Sunday of November
2021-01-24 14:07:58 +00:00
Europe
2021-01-25 04:46:48 +00:00
- Starts : Last Sunday of March
- Ends : Last Sunday of October
2021-01-24 14:07:58 +00:00
Southern Hemisphere
- Starts : 1st Sunday of October
- Ends : 1st Sunday of April ` ;
const DST_NOTE_SETUP = ` Which daylight savings region most closely matches your own?
North America ( 1 ️ ⃣ )
2021-01-25 04:46:48 +00:00
- Starts : 2nd Sunday of March
- Ends : 1st Sunday of November
2021-01-24 14:07:58 +00:00
Europe ( 2 ️ ⃣ )
2021-01-25 04:46:48 +00:00
- Starts : Last Sunday of March
- Ends : Last Sunday of October
2021-01-24 14:07:58 +00:00
Southern Hemisphere ( 3 ️ ⃣ )
- Starts : 1st Sunday of October
- Ends : 1st Sunday of April ` ;
const DAYS_OF_MONTH = [ 31 , 28 , 31 , 30 , 31 , 30 , 31 , 31 , 30 , 31 , 30 , 31 ] ;
// Returns an integer of the specific day the Sunday falls on, -1 if not found
// Also modifies the date object to the specified day as a side effect
function getSunday ( date : Date , order : number ) {
const daysInCurrentMonth = DAYS_OF_MONTH [ date . getUTCMonth ( ) ] ;
let occurrencesLeft = order - 1 ;
// Search for the last Sunday of the month
if ( order === 0 ) {
for ( let day = daysInCurrentMonth ; day >= 1 ; day -- ) {
date . setUTCDate ( day ) ;
if ( date . getUTCDay ( ) === 0 ) {
return day ;
}
}
} else if ( order > 0 ) {
for ( let day = 1 ; day <= daysInCurrentMonth ; day ++ ) {
date . setUTCDate ( day ) ;
if ( date . getUTCDay ( ) === 0 ) {
if ( occurrencesLeft > 0 ) {
occurrencesLeft -- ;
} else {
return day ;
}
}
}
}
return - 1 ;
}
// region: [firstMonth (0-11), firstOrder, secondMonth (0-11), secondOrder]
const DST_REGION_TABLE = {
na : [ 2 , 2 , 10 , 1 ] ,
eu : [ 2 , 0 , 9 , 0 ] ,
sh : [ 3 , 1 , 9 , 1 ] // this one is reversed for the sake of code simplicity
} ;
// capturing: northern hemisphere is concave, southern hemisphere is convex
function hasDaylightSavings ( region : DST ) {
const [ firstMonth , firstOrder , secondMonth , secondOrder ] = DST_REGION_TABLE [ region ] ;
const date = new Date ( ) ;
const now = date . getTime ( ) ;
const currentYear = date . getUTCFullYear ( ) ;
const firstDate = new Date ( Date . UTC ( currentYear , firstMonth ) ) ;
const secondDate = new Date ( Date . UTC ( currentYear , secondMonth ) ) ;
getSunday ( firstDate , firstOrder ) ;
getSunday ( secondDate , secondOrder ) ;
const insideBounds = now >= firstDate . getTime ( ) && now < secondDate . getTime ( ) ;
return region !== "sh" ? insideBounds : ! insideBounds ;
}
function getTimeEmbed ( user : User ) {
const { timezone , daylightSavingsRegion } = Storage . getUser ( user . id ) ;
let localDate = "N/A" ;
2021-01-25 04:46:48 +00:00
let dayOfWeek = "N/A" ;
2021-01-24 14:07:58 +00:00
let localTime = "N/A" ;
let timezoneOffset = "N/A" ;
if ( timezone !== null ) {
const daylightSavingsOffset = daylightSavingsRegion && hasDaylightSavings ( daylightSavingsRegion ) ? 1 : 0 ;
const daylightTimezone = timezone + daylightSavingsOffset ;
const now = moment ( ) . utcOffset ( daylightTimezone * 60 ) ;
localDate = now . format ( DATE_FORMAT ) ;
2021-01-25 04:46:48 +00:00
dayOfWeek = now . format ( DOW_FORMAT ) ;
2021-01-24 14:07:58 +00:00
localTime = now . format ( TIME_FORMAT ) ;
2021-01-25 04:46:48 +00:00
timezoneOffset = daylightTimezone >= 0 ? ` + ${ daylightTimezone } ` : daylightTimezone . toString ( ) ;
2021-01-24 14:07:58 +00:00
}
const embed = {
embed : {
2021-01-25 04:46:48 +00:00
color : TIME_EMBED_COLOR ,
2021-01-24 14:07:58 +00:00
author : {
name : user.username ,
icon_url : user.displayAvatarURL ( {
format : "png" ,
dynamic : true
} )
} ,
fields : [
{
name : "Local Date" ,
value : localDate
} ,
2021-01-25 04:46:48 +00:00
{
name : "Day of the Week" ,
value : dayOfWeek
} ,
2021-01-24 14:07:58 +00:00
{
name : "Local Time" ,
value : localTime
} ,
{
2021-01-25 04:46:48 +00:00
name : daylightSavingsRegion !== null ? "Current Timezone Offset" : "Timezone Offset" ,
2021-01-24 14:07:58 +00:00
value : timezoneOffset
} ,
{
name : "Observes Daylight Savings?" ,
value : daylightSavingsRegion ? "Yes" : "No"
}
]
}
} ;
if ( daylightSavingsRegion ) {
embed . embed . fields . push (
{
name : "Daylight Savings Active?" ,
value : hasDaylightSavings ( daylightSavingsRegion ) ? "Yes" : "No"
} ,
{
name : "Daylight Savings Region" ,
value : DAYLIGHT_SAVINGS_REGIONS [ daylightSavingsRegion ]
}
) ;
}
return embed ;
}
export default new Command ( {
description : "Show others what time it is for you." ,
2021-01-25 04:46:48 +00:00
aliases : [ "tz" ] ,
2021-01-24 14:07:58 +00:00
async run ( { channel , author } ) {
channel . send ( getTimeEmbed ( author ) ) ;
} ,
subcommands : {
2021-01-25 04:46:48 +00:00
// Welcome to callback hell. We hope you enjoy your stay here!
2021-01-24 14:07:58 +00:00
setup : new Command ( {
description : "Registers your timezone information for the bot." ,
2021-03-30 10:25:07 +00:00
async run ( { author , channel } ) {
2021-01-24 14:07:58 +00:00
const profile = Storage . getUser ( author . id ) ;
2021-01-25 04:46:48 +00:00
profile . timezone = null ;
profile . daylightSavingsRegion = null ;
let hour : number ;
2021-01-24 14:07:58 +00:00
ask (
await channel . send (
"What hour (0 to 23) is it for you right now?\n*(Note: Make sure to use Discord's inline reply feature or this won't work!)*"
) ,
author . id ,
( reply ) = > {
2021-01-25 04:46:48 +00:00
hour = parseInt ( reply ) ;
2021-01-24 14:07:58 +00:00
if ( isNaN ( hour ) ) {
return false ;
}
2021-01-25 04:46:48 +00:00
return hour >= 0 && hour <= 23 ;
2021-01-24 14:07:58 +00:00
} ,
async ( ) = > {
2021-01-25 04:46:48 +00:00
// 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 ;
2021-01-24 14:07:58 +00:00
}
}
2021-01-25 04:46:48 +00:00
}
// 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
2021-01-24 14:07:58 +00:00
) ;
2021-01-25 04:46:48 +00:00
const finalize = ( ) = > {
Storage . save ( ) ;
channel . send (
"You've finished setting up your timezone! Just check to see if this looks right, and if it doesn't, run this setup again." ,
getTimeEmbed ( author )
) ;
} ;
if ( hasDST ) {
const finalizeDST = ( region : DST ) = > {
profile . daylightSavingsRegion = region ;
// If daylight savings is active, subtract the timezone offset by one to store the standard time.
if ( hasDaylightSavings ( region ) ) {
profile . timezone ! -- ;
}
finalize ( ) ;
} ;
askMultipleChoice ( await channel . send ( DST_NOTE_SETUP ) , author . id , [
( ) = > finalizeDST ( "na" ) ,
( ) = > finalizeDST ( "eu" ) ,
( ) = > finalizeDST ( "sh" )
] ) ;
} else {
finalize ( ) ;
}
2021-01-24 14:07:58 +00:00
} ,
2021-01-25 04:46:48 +00:00
( ) = > "you need to enter in a valid integer between 0 to 23"
2021-01-24 14:07:58 +00:00
) ;
}
} ) ,
delete : new Command ( {
description : "Delete your timezone information." ,
2021-03-30 10:25:07 +00:00
async run ( { channel , author } ) {
2021-01-24 14:07:58 +00:00
prompt (
await channel . send (
"Are you sure you want to delete your timezone information?\n*(This message will automatically be deleted after 10 seconds.)*"
) ,
author . id ,
( ) = > {
const profile = Storage . getUser ( author . id ) ;
profile . timezone = null ;
profile . daylightSavingsRegion = null ;
Storage . save ( ) ;
}
) ;
}
} ) ,
utc : new Command ( {
description : "Displays UTC time." ,
async run ( { channel } ) {
const time = moment ( ) . utc ( ) ;
channel . send ( {
embed : {
2021-01-25 04:46:48 +00:00
color : TIME_EMBED_COLOR ,
2021-01-24 14:07:58 +00:00
fields : [
{
name : "Local Date" ,
value : time.format ( DATE_FORMAT )
} ,
2021-01-25 04:46:48 +00:00
{
name : "Day of the Week" ,
value : time.format ( DOW_FORMAT )
} ,
2021-01-24 14:07:58 +00:00
{
name : "Local Time" ,
value : time.format ( TIME_FORMAT )
}
]
}
} ) ;
}
} ) ,
daylight : new Command ( {
description : "Provides information on the daylight savings region" ,
run : DST_NOTE_INFO
} )
} ,
user : new Command ( {
description : "See what time it is for someone else." ,
async run ( { channel , args } ) {
channel . send ( getTimeEmbed ( args [ 0 ] ) ) ;
}
} ) ,
any : new Command ( {
description : "See what time it is for someone else (by their username)." ,
2021-03-30 10:25:07 +00:00
async run ( { channel , args , message } ) {
2021-01-24 14:07:58 +00:00
callMemberByUsername ( message , args . join ( " " ) , ( member ) = > {
channel . send ( getTimeEmbed ( member . user ) ) ;
} ) ;
}
} )
} ) ;