// Version: Feb 2020
    //
    // The following TypeScript program illustrates how easy it is to apply NexCalendar:
    //
    // Since software stores dates by its ordinal days from a special date,
    // it is easy to convert any Gregorian date to NexCalendar date.	
    //
    interface MonthDay {
        month:   number;
        day:     number;
    }
    interface WeekDate {
        week:    number;
        weekday: number;
    }
    interface WeekdayName {
        longName:  string;
        shortName: string;
    }
    class NexCalendar {
        // Weekday = Day of the Week (1 = Monday ... 7 = Sunday, 8 = Sunday)
        // Yearday = Ordinal Day of the Year (1 ... 365, 366 = Leap Sunday)
        static get maxDaysInYear() { return 366 } // 366 is equal to the Leap Sunday
        static get daysByWeek35() { return (35 * 7 + 1) }  // Week 35 is the annual long week with 8 days
        static get daysInMonths() { return [0, 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] }
        static get daysByMonths() { return [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366] }
        static invalidInt(x: number, max: number) : boolean {
            if (Number.isInteger(x) && Number.isInteger(max)) return (x < 1 || x > max);
            else return true;
        }
        static getYeardayFromMonthDay(month: number, day: number) : number  // month = 1 to 12, day = 1 to 31
        {
            if (this.invalidInt(month, 12) || this.invalidInt(day, this.daysInMonths[month])) return 0;
            return (this.daysByMonths[month - 1] + day);
        }
        static getMonthDay(yearday: number) : MonthDay 
        {
            if (this.invalidInt(yearday, this.maxDaysInYear)) return {month: 0, day: 0};
            var month = 2;
            if (yearday > this.daysByMonths[4]) month = 6;
            if (yearday > this.daysByMonths[8]) month = 10;
            if (yearday > this.daysByMonths[month]) month++; else month--;
            if (yearday > this.daysByMonths[month]) month++;
            var day = yearday - this.daysByMonths[month - 1];
            return { month, day };
        }
		// week = 1 to 52, weekday = 1 to 7 or 8 for week 35 or 52.
        static getYeardayFromWeekDate(week: number, weekday: number) : number
        {
            if (week === 35 && weekday === 8) return this.daysByWeek35;
            if (week === 52 && weekday === 8) return this.maxDaysInYear;
            if (this.invalidInt(week, 52) || this.invalidInt(weekday, 7)) return 0;
            var yearday = (week - 1) * 7 + weekday;
            if (week > 35) yearday++;
            return yearday;
        }
        static getWeekDate(yearday: number) : WeekDate
        {
            if (this.invalidInt(yearday, this.maxDaysInYear)) return {week: 0, weekday: 0}; // Unknown
            if (yearday === this.maxDaysInYear) return {week: 52, weekday: 8}; // The last week
            if (yearday === this.daysByWeek35) return {week: 35, weekday: 8}; // The last week
            if (yearday >= this.daysByWeek35) yearday--;
            var week =  (1 + Math.floor((yearday - 1) / 7));
            var weekday = yearday - ((week - 1) * 7);
            return { week, weekday };
        }
        static getWeekDateFromMonthDay(month: number, day: number) : WeekDate 
		{ return this.getWeekDate(this.getYeardayFromMonthDay(month, day));}
        static getMonthDayFromWeekDate(week: number, weekday: number) : MonthDay 
		{ return this.getMonthDay(this.getYeardayFromWeekDate(week, weekday)); }
        static getWeekdayName(week: number, weekday: number) : WeekdayName
        {   
            if (week === 35 && weekday === 7) weekday = 6; // Special Saturday
            if ((week === 35 || week === 52) && weekday === 8) weekday = 7; // Special Sunday
            if (this.invalidInt(week, 52) || this.invalidInt(weekday, 7)) return {longName:"",shortName:""};
            var date = new Date(2001,0,weekday) // 1 to 7 = Monday to Sunday 
            var longName = date.toLocaleString(undefined,{weekday:"long"});
            var shortName = date.toLocaleString(undefined,{weekday:"short"});
            return { longName, shortName };
        }
        static isLeapYear(year: number) : boolean
        {
            if( this.invalidInt(year,9999)) return false; 
            return ((year % 4 === 0) && (year % 100 !== 0) || (year % 400 === 0));
        }
		// year = 1 to 9999, month = 1 to 12, day = 1 to 31
        static newDate(year: number, month: number = 1, day: number = 1,
            hour: number = 0, minute: number = 0, second: number = 0, millisecond: number = 0)
            : Date | undefined
        {
            if (!this.isLeapYear(year))
            {
                if (this.invalidInt(month, 12)) return undefined;
                if (this.invalidInt(day, this.daysInMonths[month])) return undefined;
                if (month === 12 && day === 31) { }  // set the last day of the year
                else if (month > 1 && day === this.daysInMonths[month]) { month++; day = 1; }
                else if (month > 2) day++;
            }
            return new Date(year, month-1, day, hour, minute, second, millisecond);
        }
		// return the default Date String
        static toString(date ?: Date)  : string 
        {
            if (date instanceof Date && !isNaN(Number(date))) {}
            else return "";
            var diff = 0;
            var day = date.getDate();
            var month = date.getMonth() + 1;
            var year = date.getFullYear();
            var yearbits = Number(date) - Number(new Date(date.getFullYear(), 0, 0));
            var yearday = Math.floor(yearbits / (1000 * 60 * 60 * 24));
            var weekDate = this.getWeekDate(yearday);
            var wdayName = this.getWeekdayName(weekDate.week, weekDate.weekday);
            if (!this.isLeapYear(year) && month > 2)
            {    // the previous day
                diff = -1; day--;
                if (day === 0) day = this.daysInMonths[(month - 1)];
            }
			date.setDate(date.getDate() + diff);
            var parts = date.toString().split(" ");
            parts[0] = wdayName.shortName;
            parts[2] = (day < 10 ? "0"+day : ""+day);
            return parts.join(" ");
        }
    }
    // Three extended functions are provided as DateTime extensions. 
    interface Date {
        getNexDate(): MonthDay;
        getNexWeekDate(): WeekDate;
        getNexWeekdayName(): WeekdayName;
    }
    Date.prototype.getNexDate = function() 
    {
        var year = this.getFullYear(); var month = this.getMonth()+1; var day = this.getDate();
        if (NexCalendar.isLeapYear(year) || month < 3) return { year, month, day };
        if (day === 1) return {year, month: month - 1, day: NexCalendar.daysInMonths[month - 1]};
        else return {year: year, month: month, day: day - 1};  // month = 1 to 12
    }
    Date.prototype.getNexWeekDate = function()    
    {
        var diff = Number(this) - Number(new Date(this.getFullYear(), 0, 0));
        var yearday = Math.floor(diff / (1000 * 60 * 60 * 24));    
        var wkDate = NexCalendar.getWeekDate(yearday);
        return {year: this.getFullYear(), week: wkDate.week, weekday: wkDate.weekday};
    }
    Date.prototype.getNexWeekdayName = function()
    {
        var diff = Number(this) - Number(new Date(this.getFullYear(), 0, 0));
        var yearday = Math.floor(diff / (1000 * 60 * 60 * 24));    
        var wkDate = NexCalendar.getWeekDate(yearday);
        return NexCalendar.getWeekdayName(wkDate.week, wkDate.weekday);
    }
    // e.g.
    var date = NexCalendar.newDate(2021,2,29);
    console.log(NexCalendar.toString(date));
    var mthDay = (new Date()).getNexDate();
    console.log("Month/day: " + mthDay.month + "/" + mthDay.day); 
    var wkDate = (new Date()).getNexWeekDate();
    console.log("Week Date: W" + wkDate.week + "-" + wkDate.weekday); 
    var wkName = (new Date()).getNexWeekdayName();
    console.log("Today is " + wkName.longName + " (" + wkName.shortName + ")");
    console.log("Current Calendar: " + (new Date()).toString());
    console.log("NexCalendar: " + NexCalendar.toString(new Date()));
    // Copyright © 2020 Dr. Donny C.F. Lai & Justin J.S. Lai. All Rights Reserved.