angular / components

Component infrastructure and Material Design components for Angular
https://material.angular.io
MIT License
24.32k stars 6.73k forks source link

Support for Hijri or Jalali or other international calendar in datepicker #2519

Closed araselahi closed 3 years ago

araselahi commented 7 years ago

I think making dateUtil.js injectable, would provide calendar support functionality for Hijri or Jalali or other international calendars in datepicker. Adding fromGregorian and toGregorian date functions to $mdDateLocale service may provide more abstract solution. These functions could be consequently used in dateUtil.js to provide localized date calculation. Currently $mdDateLocale just provide messages, formatters, and parsers for date internationalization but not date conversion.

I volunteer to produce dateUtil or $mdDateLocale for Hijri or Jalali. Please help me how.

fxck commented 7 years ago

There's no datepicker in material2. $mdDateLocale sounds like material. You are probably in a wrong repo.

araselahi commented 7 years ago

I just saw datepicker on material2 feature status: https://github.com/angular/material2#feature-status thought that it would inherit the same functionality from material. Hope you take internationalization date conversion into account for new datepicker. We all love material(2) and your great project.

fxck commented 7 years ago

The problem with datepicker, as mentioned in its issue, is that there are no specs(for desktop version anyway) in material design for it.. so until google designers create those, there won't be no official date/timepicker implementation. Sadly.

But I'm pretty sure that once the spec is done, internationalization will be taken into account.

araselahi commented 7 years ago

Internationalization date conversion may not affect visual design of datepicker. It just refer to how cells arrange according to the native calendar rules, mainly the same functions as currently exist in dateUtil, but with native alternative principals. About the visual customization, task already has been done gracefully in $mdDateLocale.

jelbourn commented 7 years ago

We're designing the datepicker now. We likely will only be able to support the Gregorian calendar in the initial version, but it's worth thinking about in the design.

araselahi commented 7 years ago

At the moment it may not important to support any special native calendar. The important point about this feature is to make the logic of date conversion 'injectable'. Then every folk will know how to implement their native conversion logic providers. Although I had some suggestions above, but as a designer, you know better how to construct the structure. Thanks Jeremy, for taking it into account and for your grate project.

mmalerba commented 7 years ago

I'll have a design doc to share shortly, it would be great if you could take a look and let me know if you think the design is flexible enough to support this.

araselahi commented 7 years ago

of course, it would be my pleasure.

mmalerba commented 7 years ago

@araselahi design doc is now available: https://docs.google.com/document/d/1RwPFESCbSSLMSDqxWo7jg4UUpGJTrXX6p5wYlb9pmNc/preview

araselahi commented 7 years ago

I have just seen the notification in my email. I will get back to you soon.

araselahi commented 7 years ago

@mmalerba 'CalendarLocale injectable' and 'Internationalization' sections currently include the definition only about the word translation and visual parsing and formatting but not other nationality date conversion logic. In other calendars, days calculation completely different from Gregorian. So I guess we need all the logic in dateUtil.js such as getFirstDateOfMonth, getNumberOfDaysInMonth and so on could be replaceable according to the other international calendar to determine how the days to be rendered on date picker. I think with this suggested feature, it would be a magic date picker for other folks. I am not sure the new SimpleDate data type could support other international calendar date because it is still subset of Gregorian calendar date. Am I right?

mmalerba commented 7 years ago

@araselahi I'd like to find some way to allow using moment.js objects instead of SimpleDate when the library is loaded. moment.js already has plugins for working with other languages. I still need to find the best way to do this though

peymanebrahimi commented 7 years ago

As @araselahi mentioned, it is not a matter of translation from English to another language. In Persian calendar for example the whole logic of the date system is different. 2017-April-15 may be translated into 2017 آپریل 15 which has nothing to do with Persian calendar for our daily usage which is 1396-1-26 yyyy-MM-dd. It is 26th day of the first month of the year 1396, but in Gregorian calendar it is 15th of the forth month of the year 2017.

araselahi commented 7 years ago

@peymanebrahimi it completely depends on how to implement Moment.js. If the output of configured Moment.js object was rendered on the final date picker surface as @mmalerba designed for, that would be great. Please check it out: https://momentjs.com/docs/#/plugins/jalaali/

jelbourn commented 7 years ago

@mmalerba does what we have now actually support different calendars, or are there assumptions about the Gregorian calendar baked in?

mmalerba commented 7 years ago

I think we still have some assumptions baked in, but it should be easy enough to change them to settings in the DateAdapter going to mark this one as good for community contribution. It would be nice to have someone who knows more about other calendars help with it.

araselahi commented 7 years ago

@mmalerba with DateAdapter implementation, currently you have done the critical task for calendar logic internationalization. thank you so much for your consideration. I am here to contribute for implementing JalaliDateAdapter. is there any sample like: How to implement a new DateAdapter ?

mmalerba commented 6 years ago

@peymanebrahimi has been working on a Jalali adapter https://github.com/angular/material2/issues/5972#issuecomment-329956087, you may want to consider collaborating with him.

Currently the best documentation is just the JSDoc on the DateAdapter https://github.com/angular/material2/blob/master/src/lib/core/datetime/date-adapter.ts

You can also use the NativeDateAdapter and MomentDateAdapter as example implementations: https://github.com/angular/material2/blob/master/src/lib/core/datetime/native-date-adapter.ts https://github.com/angular/material2/blob/master/src/material-moment-adapter/adapter/moment-date-adapter.ts

Sorry it doesn't have better documentation yet... I need to work on that. @araselahi

tobiasschweizer commented 6 years ago

Have you thought of supporting Julian Day Numbers for internal conversions from one calendar format to another? There are already JS libraries around that could possibly be used for the conversions (e.g., http://npmjs.com/package/moonbeams).

I am interested in developing the following features:

mmalerba commented 6 years ago

@tobiasschweizer choosing a different precision is tracked by https://github.com/angular/material2/issues/4853, this is something we plan to add

BCE dates and a Julian calendar are not features we plan to add to Angular Material, but they are features that we want enable via a custom DateAdapter. If you're interested in working on these we would love for you to share it with the community. If you encounter any limitations of the DateAdapter that prevent you from accomplishing this, feel free to open issues and we will look into it.

tobiasschweizer commented 6 years ago

@mmalerba Thank you very much for the explanation!

Let me have a look at DateApapter first. I guess I find some relevant information here https://material.angular.io/components/datepicker/overview#choosing-a-date-implementation-and-date-format-settings

As far as I understood, DateAdpater is an interface/abstract class (https://github.com/angular/material2/blob/dfe8091d959f1bb54cc33a8d942cbe7ea4e3fe56/src/lib/core/datetime/date-adapter.ts#L21) making use of generics that currently has two implementations: Native Date Adapter (https://github.com/angular/material2/blob/ea54edb7ef1c5338120c3991acbe325ad7c27764/src/lib/core/datetime/native-date-adapter.ts#L60) and the Moment Date Adapter (https://github.com/angular/material2/blob/9fa075e001188f1a346dbc260606e871bc653e1a/src/material-moment-adapter/adapter/moment-date-adapter.ts#L34).

mmalerba commented 6 years ago

@tobiasschweizer Yep, that's correct. I've been meaning to write a guide on creating a custom DateAdapter but haven't gotten the time. Those two concrete classes should serve as good examples though.

tobiasschweizer commented 6 years ago

@mmalerba Thats' fine. Let me look at it in detail. I will come up with some suggestions of how to integrate the requested features, so we can discuss them.

tobiasschweizer commented 6 years ago

@mmalerba This is a primitive first sketch of how I think this could work (I do not address the BCE problem yet, divide et impera ...). I hope I got the basic idea of the design and this contribution is useful.

The DateAdapter would have two new abstract methods getSupportedCalendars(): string[] and convertCalendar(cal: D, toFormat: string): D. The DatePicker UI component could display a list of available calendars to the user. If the user chooses another calendar than the one currently given, a conversion would happen from the current calendar to the chosen one.

JDNCalendar is an abstract class that would have to be implemented/subclassed for all supported calendar types (here only Gregorian and Julian). The clue would be that every implementation or subclass of JDNCalendar can be created from a Julian Day Number and convert itself to a Julian Day Number. Like this, a Gregorian calendar date can be converted to a Julian calendar date etc., preserving the currently selected date.

We have implemented that earlier, but the code base is a mess: https://github.com/dhlab-basel/Knora/blob/develop/salsah/src/public/js/datehelpers.js, https://github.com/dhlab-basel/Knora/blob/develop/salsah/src/public/js/jquery.dateobj.js

However, the new abstract methods for DateAdapter do not make sense for the existing implementations since there is no conversion between calendars that could happen. In this case, the DatePicker should not show them (if getSupportedCalendars() returns an array of length 1).

const Gregorian = 'Gregorian';
const Julian = 'Julian';

/**
 * Represents a calendar conversion error.
 */
class CalendarConversionError extends Error {

    constructor(readonly message: string) {
        super(message)
    }

}

/**
 * Abstract class representing a calendar format
 * that can be converted from and to a Julian Day Number (JDN).
 */
abstract class JDNCalendar {

    // type of calendar
    calendarType: string;

    // supported calendars
    readonly supportedCalendars = [Gregorian, Julian];

    /**
     * Convert an instance of JDNCalendar to a JDN.
     *
     * @returns {number}
     */
    abstract toJDN(): number;

    /**
     * Converts from one calendar format into another.
     *
     * @param {"Gregorian" | "Julian"} toCalendarType
     * @returns {JDNCalendar}
     */
    convertCalendar(toCalendarType: string): JDNCalendar {

        if (this.supportedCalendars.indexOf(toCalendarType) == -1) throw new CalendarConversionError("Calendar not supported: " + toCalendarType);

        if (this.calendarType == toCalendarType) return this; // no conversion needed

        const jdn = this.toJDN();

        switch (toCalendarType) {
            case Gregorian:
                return new GregorianCalendar(jdn);

            case Julian:
                return new JulianCalendar(jdn);
        }

    }

}

/**
 * Represents a Gregorian calendar date.
 */
class GregorianCalendar extends JDNCalendar {

    /**
     * @param {number} jdn Julian Day Number
     */
    constructor(jdn: number) {
        super();

        // create a Gregorian calendar date from jdn

    }

    readonly calendarType = Gregorian;

    toJDN(): number {
        return 1; // for testing only
    }

}

/**
 * Represents a Julian calendar date.
 */
class JulianCalendar extends JDNCalendar {

    /**
     *
     * @param {number} jdn Julian Day Number
     */
    constructor(jdn: number) {
        super();

        // create a Julian calendar date from jdn

    }

    readonly calendarType = Julian;

    toJDN(): number {
        return 2; // for testing only
    }

}

/*
 new abstract methods for DateAdapter:
  - getSupportedCalendars(): string[]
  - convertCalendar(fromCal: D, toFormat: string)
*/

/**
 * DateAdapter for JDNCalendar
 */
class JDNCalendarAdapter extends DateAdapter<JDNCalendar> {

    // TODO: implement all abstract methods of DateAdapter and override non abstract methods of DateAdapter if necessary

    getSupportedCalendars(cal: JDNCalendar): string[] {
        return cal.supportedCalendars;
    }

    convertCalendar(cal: JDNCalendar, toFormat: string): JDNCalendar {

        return cal.convertCalendar(toFormat);

    }

}
mmalerba commented 6 years ago

I don't think we want to add calendar conversion APIs to the DateAdapter (since as you said, it doesn't make sense in a lot of cases). Rather I would make a JulianCalendarDateAdapter that handles things like parsing and formatting Julian dates, and getting the julian year, month, and day.

If you then wanted to be able to dynamically toggle between the two calendars you could make a wrapper DateAdapter that delegates to one of the two depending on the mode its in:

class CombinedAdapter extends DateAdapter {
  private gregorianAdapter = new NativeDateAdapter();
  private julianAdapter = new JulianCalendarDateAdapter();
  private currentAdapter = this._gregorianAdapter;

  setCalendar(cal: 'Gregorian' | 'Julian') {
    this.currentAdapter = cal === 'Gregorian' ? this.gregorianAdapter : this.julianAdapter;
  }

  // Implement all methods to delegate to currentAdapter:

  parse(value) {
    return this.currentAdapter.parse(value);
  }

  ...
}

The one thing we might need to change to make this work is to add some way to flag the datepicker that the DateAdapter has changed modes and that the datepicker should update its view.

tobiasschweizer commented 6 years ago

I understand that the calendar conversion method in DateAdapter does not make sense for implementations where only one calendar is available.

However, the JDNCalendar I proposed is not the same as the Julian calendar date.

A JDN (Julian Day Number) is an integer (no fractions unlike the Julian Day Count JDC) that represents the number of days since an astronomical event (JDN 0) of November 24, 4714 BC, in the Gregorian calendar or Monday, January 1, 4713 BC, Julian calendar (https://en.wikipedia.org/wiki/Julian_day).

Both Gregorian and Julian calendar dates (as well as others) can be created from a JDN and converted to a JDN. Using JDN, one can thus do calendar conversions (see https://www.fourmilab.ch/documents/calendar/).

So my idea is to create an implementation of DateAdapter for JDNCalendar (maybe I have to think of a better name for the latter) that could convert any calendar format to any other calendar format for which an implementation for JDNCalendar exists, preserving the currently selected date.

Subclasses or implementations of JDNCalendar can be constructed from a given JDN and converted to a JDN, from which others can be constructed again. So the JDN is like the common denominator for different calendar formats.

If I understood you correctly, you proposed a specific implementation of DateAdapter for Julian calendar dates. This would mean that we would have to make a specific implementation for each calendar we want to support. Instead, I propose to make use of one abstract class (JDNCalendar) for which only one implementation of DateAdapter has to exists and which can also be implemented for other than Gregorian and Julian calendar formats if they can be converted from and to JDN , e.g., Hebrew calendar.

mmalerba commented 6 years ago

Ah I see, I had not heard about Julian Day Number before. In that case I think your proposal sounds reasonable. Would the JDNCalendar then be responsible for reporting things like what the months are in its particular calendar system?

Another thing to consider is the parse and display formats. We allow the user to provide mappings for the formats needed by the datepicker via the MAT_DATE_FORMATS token. It sounds like for your idea you would need to come up with a way to represent the format independent of the calendar system. It's kind of the same problem I was trying to solve when I made these mappings in the first place, you could just map each key to a string of the same name and have each concrete JDNCalendar decide how to represent it itself. If we added more formats to the datepicker at some point you would need to just add those to your implementations as well.

tobiasschweizer commented 6 years ago

I really like your design that separates the UI-component (datepicker) from a concrete implementation of a calendar (currently JS Date object and MomentJS) using DateAdapter as the interface through which communication between the UI and the calendar is established.

This abstraction greatly enhances the re-usability of the UI component. However, I also see things that are actually calendar specific like the distinction between BCE and CE dates and only make sense for the Gregorian and the Julian calendar.

Would the JDNCalendar then be responsible for reporting things like what the months are in its particular calendar system?

Yes, JDNCalendar would have to have methods that tell the UI-component everything it needs to know to make the date selection possible for the given calendar (e.g., the Hebrew calendar's months are called differently than the months of the Gregorian and Julian calendar). But this would mean that DateApater would have to be extended for that purpose so that the UI component can ask the implementation of DateApter about it. But there are already methods for that in DateAdapter:

Although we aim to be as generic as possible, the UI component might be limited to calendars that work with a concept of years, months, and days.

Another thing to consider is the parse and display formats.

I think localization and format information has to be provided for each implementation of JDNCalendar.

How shall we proceed? I would like to work on JDNCalendar and support implementations for the Gregorian and Julian calendar for now. Of course, implementations for other calendars may be added too.

I think JDNCalendar can be developed outside Angular Material 2 repo, but we need to decide on the additional methods for DateAdapter, and that will affect existing implementations for Date object and Moment JS.

See my sketch: design_jdncal

Another thing to keep in mind is the precision. A precision can by expressed using two JDNs (see http://www.knora.org/documentation/manual/rst/knora-ontologies/knora-base.html#datevalue, https://github.com/dhlab-basel/Knora/blob/cc53ef8cd072b7c9c53c7e5b3ab3a92c497b3038/webapi/src/main/scala/org/knora/webapi/util/DateUtilV1.scala). If the two numbers are equal the precision is day. Month precison would be from the first day of the month to the last and the same logic applies to the year. Periods may be expressed by simply having two datepickers, i.e. two dates that might have different precisions.

tobiasschweizer commented 6 years ago

I am having a look at the abstract methods of DateAdapter and the ones that are overridden by its subclasses.

I am quite sure I forgot some of the methods or misunderstood some of them, please feel free to correct me ;-)

  1. With date: D as parameter (get information from or modify a date instance):
  1. Methods needed by the UI component for the given calendar format D (no instance of D required though):
  1. Methods to construct an instance of a date:
  1. Methods to serialize a date:
tobiasschweizer commented 6 years ago

I think each implementation or subclass of JDNCalendar has to provide an own implementation of the methods in group 2 since these are specific to the calendar format.

For createDate, parse and deserialize as well as today in group 3 an indication of the calendar format is needed. Gregorian could be made the default, so existing implementations wouldn't break.

When serializing the date, the calendar information has to go somewhere (group 4), it could be omitted for Gregorian.

For the year (group 1), BCE dates could be tricky.

mmalerba commented 6 years ago

Apologies for the slow response. I agree with what you've outlined above. I think this is a cool idea, it would be great for people who need a more robust solution for dealing with multiple calendars.

Here's some thoughts & rationale for some of the decisions:

But there are already methods for that in DateAdapter:

  • getMonthNames()
  • getDayOfWeekNames()

Although we aim to be as generic as possible, the UI component might be limited to calendars that work with a concept of years, months, and days.

Yes this does limit us to calendars that use years, months, and days. Though I think that should cover the vast majority of calendars people would want to use with the datepicker. Though one of the things I don't like about these various get*Name[s] methods is that they kind of mix date format stuff into the logic. Another idea would be to have method like getNumMonthsInYear(date: D) and getNumDaysInMonth(date: D) we could then get the names of the months by creating a date for the month and doing something like format(date, formats.LONG_MONTH_NAME). Changing the API to this would also allow us to support calendars that have a variable number of months depending on the year .

Wouldn't it be clearer to use the term "day"?

I used "date" to stay consistent with what javascript does and avoid confusion with days of the week. Though I agree its confusing in its own way. I would be ok replacing the "date" APIs with "dayOfMonth".

Isn't that somehow redundant? -> getYear(date: D): number

The idea was that the name variant could be used in situations where the string representation was more than just the number. For example in Japanese maybe someone would want to include the year kanji: "2017年". Though as I mentioned above I don't like how these "-name" APIs mix in formatting logic. It might be better to move to something like format(date, formats.YEAR_NAME).

Do you want to start a repo for it? If you run into flexibility issues with the DateAdapter API as you go let me know and I'll consider making changes. I'd also be happy to review code if you need an extra set of eyes on anything.

tobiasschweizer commented 6 years ago

@mmalerba No worries! Thank you very much for your detailed response.

I have started implementing JDNConvertibleCalendar here: https://github.com/dhlab-basel/JDNConvertibleCalendar. I still need to figure out how to structure it because I want it to become an npm module.

I also still need to figure out how date manipulations work (increasing and decreasing year, month, and day). I have already added some simple conversion tests. The current implementation will still change a lot, but I hope that there will be a proof of concept soon.

I did not bother with precision yet (every date has an exact start and end so far: day precision), also I did not care about string formats yet. I think a lot of this should be handled in the implementation of DateAdapter for JDNConvertibleCalendar. JDNConvertibleCalendar should only handle the conversion logic and the math used for manipulations (jump to the next month or year when increasing days etc.). Since I work at a university, I will get all the scientific support I need.

In which repo would the implementation of DateAdapter have to go for JDNConvertibleCalendar?

mmalerba commented 6 years ago

Hmm I think its up to you how you want to structure it. You could put it in the same repo but make a separate npm package from the core calendar logic if you want to make the calendar logic available to people who aren't using Angular. The angular specific stuff would probably consist of the date adapter, a set of formats, and a module or 2 that people can import to easily set up the providers.

This is similar to what we do for the MomentDateAdapter it lives in the same repo as the rest of material, but we publish it as its own npm package since it has additional dependencies.

tobiasschweizer commented 6 years ago

I think it would make sense to put the JDNConvertibleCalendarDateAdapter in the Angular Material 2 repo. https://github.com/dhlab-basel/JDNConvertibleCalendar could still be used without Angular Material 2. Also if DateAdapter is going to be changed in the future, this would make things easier to coordinate.

I forked the Angular Material 2 repo. Shall I implement JDNConvertibleCalendarDateAdapter there and create a PR? I might have some questions how to include it in app-module.ts in our Angular app https://github.com/dhlab-basel/Salsah.

How shall I deal with the DateAdapter interface? Shall I just add what I think is needed as proposed in https://github.com/angular/material2/issues/2519#issuecomment-349264844?

Maybe it would make things easier if we could talk briefly. However, I suppose your are located in the US and I am here in Europe (time diff.).

tobiasschweizer commented 6 years ago

@mmalerba I created a new branch on my forked repo from angular/material2: https://github.com/dhlab-basel/material2/tree/datepicker-datadapter-jdnconvcal

I am struggling with package.json and the basic structure (index.ts, tsconfig). I would be glad if you could give me a hand here. I guess once I have the basic setup similar to src/material-moment-adapter I should be fine and can implement it by myself.

mmalerba commented 6 years ago

@tobiasschweizer I can set up some time to do a Hangouts conversation if you like. Shoot me an email: mmalerba@google.com

tobiasschweizer commented 6 years ago

@mmalerba As soon as a build with our merged PR https://github.com/angular/material2/pull/9639 is available, I will make a proof of concept in my repo https://github.com/dhlab-basel/JDNConvertibleCalendarDateAdapter with a custom header so that the user can switch between Gregorian and Julian calendar formats. https://github.com/dhlab-basel/JDNConvertibleCalendar will have to be extended too so that more calendar formats are supported in the future.

tobiasschweizer commented 6 years ago

I think it is already there https://github.com/angular/material2-builds/commit/d6e71c9c9ee0d54683ee4aa338be634a319fa770

tobiasschweizer commented 5 years ago

@mmalerba @araselahi

We added support for the Islamic calendar.

See https://stackblitz.com/edit/angular-qapxhn

I set the locale to 'ar', 'en' is used as a fallback for the names of weekdays and months. I think there is still a RTL issue:

Islamic_Date

Also abbreviations would be nice for transliterations of the Arabic names, see https://github.com/dhlab-basel/JDNConvertibleCalendar/blob/master/src/names.json

Could you give me a hand here?

tobiasschweizer commented 5 years ago

btw: all the available calendars can also be used individually. In the date adapter, I still have to figure out a way to set the initial calendar correctly, see https://github.com/dhlab-basel/JDNConvertibleCalendarDateAdapter/pull/9

jelbourn commented 3 years ago

(revisiting old feature requests)

Closing this issue since I don't realistically ever see us prioritizing adding support for non-Gregorian calendars. With the current state of our backlog, it would be years before we would even consider this, and I don't foresee it ever getting near the top of the queue.

tobiasschweizer commented 3 years ago

(revisiting old feature requests)

Closing this issue since I don't realistically ever see us prioritizing adding support for non-Gregorian calendars. With the current state of our backlog, it would be years before we would even consider this, and I don't foresee it ever getting near the top of the queue.

I understand.

Could you tell me anything about the priority of the precision task (year or month selection)? At the moment, the date and range pickers only allow for day selection.

jelbourn commented 3 years ago

@tobiasschweizer it's something I'd like to explore eventually, but I don't see us dedicating any time to it in 2021

angular-automatic-lock-bot[bot] commented 3 years ago

This issue has been automatically locked due to inactivity. Please file a new issue if you are encountering a similar or related problem.

Read more about our automatic conversation locking policy.

This action has been performed automatically by a bot.