angular-material-jalali-datepicker-adapter
Version:
Jalali date adapter for Angular Material
610 lines (601 loc) • 20.7 kB
JavaScript
import * as i0 from '@angular/core';
import { Injectable, inject } from '@angular/core';
import { DateAdapter, MAT_DATE_LOCALE, MAT_DATE_FORMATS } from '@angular/material/core';
class JalaliDateService {
_locale = 'fa-IR';
_calendar = 'persian';
_monthNames = [
'فروردین',
'اردیبهشت',
'خرداد',
'تیر',
'مرداد',
'شهریور',
'مهر',
'آبان',
'آذر',
'دی',
'بهمن',
'اسفند',
];
_dayNames = {
long: [
'شنبه',
'یکشنبه',
'دوشنبه',
'سهشنبه',
'چهارشنبه',
'پنجشنبه',
'جمعه',
],
short: ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج'],
narrow: ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج'],
};
breaks = [
-61, 9, 38, 199, 426, 686, 756, 818, 1111, 1181, 1210, 1635, 2060, 2097,
2192, 2262, 2324, 2394, 2456, 3178,
];
// Cache formatter to avoid recreating
_formatter;
get monthNames() {
return [...this._monthNames];
}
get dayNames() {
return this._dayNames;
}
getFormatter() {
return (this._formatter ||= new Intl.DateTimeFormat(this._locale, {
calendar: this._calendar,
numberingSystem: 'latn',
}));
}
getYear(date) {
return parseInt(this.getFormatter()
.formatToParts(date)
.find((part) => part.type === 'year')?.value || '0');
}
getMonth(date) {
return (parseInt(this.getFormatter()
.formatToParts(date)
.find((part) => part.type === 'month')?.value || '1') - 1);
}
getDate(date) {
return parseInt(this.getFormatter()
.formatToParts(date)
.find((part) => part.type === 'day')?.value || '1');
}
addDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
addMonths(date, months) {
const jalali = this.toJalali(date);
let newMonth = jalali.month + months;
let newYear = jalali.year;
while (newMonth > 12) {
newMonth -= 12;
newYear++;
}
while (newMonth < 1) {
newMonth += 12;
newYear--;
}
// Handle day overflow for different month lengths
const maxDaysInMonth = this.getDaysInMonth(newYear, newMonth);
const newDay = Math.min(jalali.day, maxDaysInMonth);
const gregorian = this.toGregorian(newYear, newMonth, newDay);
return new Date(gregorian.year, gregorian.month - 1, gregorian.date);
}
addYears(date, years) {
return this.addMonths(date, years * 12);
}
parse(value, parseFormat = 'yyyy/MM/dd') {
if (!value || value === '')
return null;
if (value instanceof Date) {
return this.isValid(value) ? value : null;
}
if (typeof value === 'string') {
return this.parseString(value.trim(), parseFormat);
}
if (typeof value === 'number') {
const date = new Date(value);
return this.isValid(date) ? date : null;
}
return null;
}
parseString(dateStr, format) {
// Convert Persian digits
const normalizedStr = dateStr.replace(/[۰-۹]/g, (d) => '۰۱۲۳۴۵۶۷۸۹'.indexOf(d).toString());
// Simple yyyy/MM/dd parsing (most common case)
if (format === 'yyyy/MM/dd') {
const match = normalizedStr.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})$/);
if (match) {
const [, year, month, day] = match.map(Number);
return this.createJalaliDate(year, month, day);
}
}
return null;
}
createJalaliDate(year, month, day) {
try {
const gregorian = this.toGregorian(year, month, day);
const date = new Date(gregorian.year, gregorian.month - 1, gregorian.date);
return this.isValid(date) ? date : null;
}
catch {
return null;
}
}
format(date, displayFormat) {
if (!this.isValid(date))
return '';
const year = this.getYear(date);
const month = this.getMonth(date) + 1;
const day = this.getDate(date);
const monthLong = this.monthNames[month - 1];
const monthShort = monthLong.slice(0, 3);
// Handle common format patterns
return displayFormat
.replace(/MMMM/g, monthLong)
.replace(/MMM/g, monthShort)
.replace(/yyyy/g, year.toString())
.replace(/MM/g, month.toString().padStart(2, '0'))
.replace(/dd/g, day.toString().padStart(2, '0'))
.replace(/M/g, month.toString())
.replace(/d/g, day.toString());
}
isValid(date) {
return date !== null && date instanceof Date && !isNaN(date.getTime());
}
getDaysInMonth(year, month) {
if (month < 6)
return 31;
if (month < 11)
return 30;
return this.isLeapYear(year) ? 30 : 29;
}
// Convert Gregorian to Jalali
toJalali(date) {
const year = this.getYear(date);
const month = this.getMonth(date) + 1; // Convert to 1-based
const day = this.getDate(date);
return { year, month, day };
}
toGregorian(year, month, date) {
const julian = this.jalaliToJulian(year, month, date);
return this.julianToGregorian(julian);
}
isLeapYear(year) {
try {
const leap = this.calculateLeap(year);
return leap === 0;
}
catch {
return false;
}
}
// Core conversion methods (simplified)
jalaliToJulian(year, month, date) {
const r = this.calculateJalali(year, false);
return (this.gregorianToJulian(r.gregorianYear, 3, r.march) +
(month - 1) * 31 -
this.div(month, 7) * (month - 7) +
date -
1);
}
julianToGregorian(julian) {
let j = 4 * julian + 139361631;
j =
j + this.div(this.div(4 * julian + 183187720, 146097) * 3, 4) * 4 - 3908;
const i = this.div(this.mod(j, 1461), 4) * 5 + 308;
const date = this.div(this.mod(i, 153), 5) + 1;
const month = this.mod(this.div(i, 153), 12) + 1;
const year = this.div(j, 1461) - 100100 + this.div(8 - month, 6);
return { year, month, date };
}
gregorianToJulian(year, month, date) {
const julian = this.div((year + this.div(month - 8, 6) + 100100) * 1461, 4) +
this.div(153 * this.mod(month + 9, 12) + 2, 5) +
date -
34840408;
return (julian -
this.div(this.div(year + 100100 + this.div(month - 8, 6), 100) * 3, 4) +
752);
}
calculateJalali(year, calculateLeap = true) {
const bl = this.breaks.length;
const gregorianYear = year + 621;
let leapJ = -14;
let jp = this.breaks[0];
if (year < jp || year >= this.breaks[bl - 1]) {
throw new Error(`Invalid Jalali year ${year}`);
}
let jump = 0;
for (let i = 1; i < bl; i++) {
const jm = this.breaks[i];
jump = jm - jp;
if (year < jm)
break;
leapJ = leapJ + this.div(jump, 33) * 8 + this.div(this.mod(jump, 33), 4);
jp = jm;
}
let n = year - jp;
leapJ = leapJ + this.div(n, 33) * 8 + this.div(this.mod(n, 33) + 3, 4);
if (this.mod(jump, 33) === 4 && jump - n === 4) {
leapJ += 1;
}
const leapG = this.div(gregorianYear, 4) -
this.div((this.div(gregorianYear, 100) + 1) * 3, 4) -
150;
const march = 20 + leapJ - leapG;
return {
gregorianYear,
march,
leap: calculateLeap ? this.calculateLeap(year, { jp, jump }) : -1,
};
}
calculateLeap(year, calculated) {
const bl = this.breaks.length;
let jp = calculated ? calculated.jp : this.breaks[0];
let jump = calculated ? calculated.jump : 0;
if (!calculated) {
if (year < jp || year >= this.breaks[bl - 1]) {
throw new Error(`Invalid Jalali year ${year}`);
}
for (let i = 1; i < bl; i++) {
const jm = this.breaks[i];
jump = jm - jp;
if (year < jm)
break;
jp = jm;
}
}
let n = year - jp;
if (jump - n < 6) {
n = n - jump + this.div(jump + 4, 33) * 33;
}
let leap = this.mod(this.mod(n + 1, 33) - 1, 4);
if (leap === -1) {
leap = 4;
}
return leap;
}
div(a, b) {
return ~~(a / b);
}
mod(a, b) {
return a - ~~(a / b) * b;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: JalaliDateService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: JalaliDateService, providedIn: 'root' });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.10", ngImport: i0, type: JalaliDateService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root',
}]
}] });
const JALALI_DATE_FORMATS = {
parse: {
dateInput: 'yyyy/MM/dd',
},
display: {
dateInput: 'yyyy/MM/dd',
monthYearLabel: 'yyyy MMMM',
dateA11yLabel: 'yyyy/MM/dd',
monthYearA11yLabel: 'yyyy MMMM',
},
};
class MaterialJalaliDateAdapter extends DateAdapter {
dateService = inject(JalaliDateService);
getYear(date) {
return this.dateService.getYear(date);
}
getMonth(date) {
return this.dateService.getMonth(date);
}
getDate(date) {
return this.dateService.getDate(date);
}
getDayOfWeek(date) {
// Convert JS day (0=Sun, 6=Sat) to Persian (0=Sat, 6=Fri)
return (date.getDay() + 1) % 7;
}
getMonthNames(_style) {
return this.dateService.monthNames;
}
getDateNames() {
// Cache the array since it's static
return this._dateNames ||= Array.from({ length: 31 }, (_, i) => String(i + 1));
}
_dateNames;
getDayOfWeekNames(style) {
return [...this.dateService.dayNames[style]];
}
getYearName(date) {
return this.getYear(date).toString();
}
getFirstDayOfWeek() {
return 0; // Saturday is first day in Persian calendar
}
getNumDaysInMonth(date) {
const year = this.getYear(date);
const month = this.getMonth(date); // Convert to 1-based
return this.dateService.getDaysInMonth(year, month);
}
clone(date) {
return new Date(date.getTime());
}
createDate(year, month, date) {
// Validate input parameters
if (!this.isValidJalaliInput(year, month + 1, date)) {
return this.invalid();
}
try {
const gregorian = this.dateService.toGregorian(year, month + 1, date);
const result = new Date(gregorian.year, gregorian.month - 1, gregorian.date);
return this.isValid(result) ? result : this.invalid();
}
catch {
return this.invalid();
}
}
today() {
return new Date();
}
parse(value, parseFormat) {
return this.dateService.parse(value, parseFormat);
}
format(date, displayFormat) {
return this.dateService.format(date, displayFormat);
}
addCalendarYears(date, years) {
if (!this.isValid(date))
return this.invalid();
try {
return this.dateService.addYears(date, years);
}
catch {
return this.invalid();
}
}
addCalendarMonths(date, months) {
if (!this.isValid(date))
return this.invalid();
try {
return this.dateService.addMonths(date, months);
}
catch {
return this.invalid();
}
}
addCalendarDays(date, days) {
if (!this.isValid(date))
return this.invalid();
try {
return this.dateService.addDays(date, days);
}
catch {
const result = this.clone(date);
result.setDate(result.getDate() + days);
return result;
}
}
toIso8601(date) {
return date.toISOString();
}
isDateInstance(obj) {
return obj instanceof Date;
}
isValid(date) {
return this.dateService.isValid(date);
}
invalid() {
return new Date(NaN);
}
deserialize(value) {
if (value == null || value === '') {
return null;
}
if (typeof value === 'string') {
const trimmed = value.trim();
return trimmed ? this.parse(trimmed, JALALI_DATE_FORMATS.parse.dateInput) : null;
}
if (typeof value === 'number' && !isNaN(value)) {
const date = new Date(value);
return this.isValid(date) ? date : null;
}
if (this.isDateInstance(value)) {
return this.isValid(value) ? this.clone(value) : null;
}
return null;
}
// Helper method for input validation
isValidJalaliInput(year, month, day) {
return (Number.isInteger(year) && year > 0 && year <= 3178 &&
Number.isInteger(month) && month >= 1 && month <= 12 &&
Number.isInteger(day) && day >= 1 && day <= 31);
}
}
class MaterialJalaliStringDateAdapter extends DateAdapter {
invalid() {
return 'INVALID_DATE';
}
dateService = inject(JalaliDateService);
getDateParts(date) {
if (!date || typeof date !== 'string') {
throw new Error('Invalid date string');
}
const parts = date.split('/');
if (parts.length !== 3) {
throw new Error('Invalid date format');
}
const year = parseInt(parts[0], 10);
const month = parseInt(parts[1], 10) - 1; // Convert to 0-based
const day = parseInt(parts[2], 10);
if (isNaN(year) || isNaN(month) || isNaN(day)) {
throw new Error('Invalid date components');
}
return { year, month, day };
}
getYear(date) {
return this.getDateParts(date).year;
}
getMonth(date) {
return this.getDateParts(date).month;
}
getDate(date) {
return this.getDateParts(date).day;
}
getDayOfWeek(date) {
const dateObj = this.dateService.parse(date, JALALI_DATE_FORMATS.parse.dateInput);
if (!dateObj)
return 0;
// Convert JS day (0=Sun, 6=Sat) to Persian (0=Sat, 6=Fri)
return (dateObj.getDay() + 1) % 7;
}
getMonthNames(_style) {
return this.dateService.monthNames;
}
getDateNames() {
// Cache the array since it's static
return this._dateNames ||= Array.from({ length: 31 }, (_, i) => String(i + 1));
}
_dateNames;
getDayOfWeekNames(style) {
return [...this.dateService.dayNames[style]];
}
getYearName(date) {
return this.getYear(date).toString();
}
getFirstDayOfWeek() {
return 0; // Saturday is first day
}
getNumDaysInMonth(date) {
const year = this.getYear(date);
const month = this.getMonth(date) + 1; // Convert to 1-based
return this.dateService.getDaysInMonth(year, month);
}
clone(date) {
return date;
}
createDate(year, month, date) {
// Ensure valid ranges
const validYear = Math.max(1, year);
const validMonth = Math.max(0, Math.min(11, month));
const maxDays = this.dateService.getDaysInMonth(validYear, validMonth);
const validDate = Math.max(1, Math.min(maxDays, date));
return `${validYear}/${(validMonth + 1).toString().padStart(2, '0')}/${validDate.toString().padStart(2, '0')}`;
}
today() {
const today = new Date();
return this.dateService.format(today, JALALI_DATE_FORMATS.parse.dateInput);
}
parse(value, parseFormat) {
if (!value)
return null;
if (typeof value === 'string') {
// If it's already in the correct format, validate and return as-is
if (/^\d{4}\/\d{1,2}\/\d{1,2}$/.test(value)) {
const dateObj = this.dateService.parse(value, parseFormat);
return dateObj ? value : null;
}
}
if (typeof value === 'number') {
const dateObj = new Date(value);
if (this.dateService.isValid(dateObj)) {
return this.dateService.format(dateObj, JALALI_DATE_FORMATS.parse.dateInput);
}
}
if (value instanceof Date && this.dateService.isValid(value)) {
return this.dateService.format(value, JALALI_DATE_FORMATS.parse.dateInput);
}
return null;
}
format(date, displayFormat) {
const dateObj = this.dateService.parse(date, JALALI_DATE_FORMATS.parse.dateInput);
if (!dateObj)
return '';
return this.dateService.format(dateObj, displayFormat);
}
addCalendarYears(date, years) {
const pYear = this.getYear(date);
const pMonth = this.getMonth(date);
const pDay = this.getDate(date);
return this.createDate(pYear + years, pMonth, pDay);
}
addCalendarMonths(date, months) {
const pYear = this.getYear(date);
const pMonth = this.getMonth(date);
const pDay = this.getDate(date);
const totalMonths = pMonth + months;
const newYear = pYear + Math.floor(totalMonths / 12);
let newMonth = totalMonths % 12;
// Handle negative months
if (newMonth < 0) {
newMonth = 12 + newMonth;
}
// Handle month overflow by adjusting day if needed
const maxDay = this.dateService.getDaysInMonth(newYear, newMonth);
const adjustedDay = Math.min(pDay, maxDay);
return this.createDate(newYear, newMonth, adjustedDay);
}
addCalendarDays(date, days) {
const dateObject = this.dateService.parse(date, JALALI_DATE_FORMATS.parse.dateInput);
if (!dateObject)
return date;
dateObject.setDate(dateObject.getDate() + days);
return this.dateService.format(dateObject, JALALI_DATE_FORMATS.parse.dateInput);
}
toIso8601(date) {
const dateObject = this.dateService.parse(date, JALALI_DATE_FORMATS.parse.dateInput);
if (!dateObject)
return '';
return dateObject.toISOString();
}
isDateInstance(obj) {
return typeof obj === 'string' && /^\d{4}\/\d{1,2}\/\d{1,2}$/.test(obj);
}
isValid(date) {
if (!date)
return false;
const dateObj = this.dateService.parse(date, JALALI_DATE_FORMATS.parse.dateInput);
return dateObj !== null && this.dateService.isValid(dateObj);
}
deserialize(value) {
if (!value)
return null;
if (typeof value === 'string') {
return this.parse(value, JALALI_DATE_FORMATS.parse.dateInput);
}
if (typeof value === 'number') {
return this.dateService.format(new Date(value), JALALI_DATE_FORMATS.parse.dateInput);
}
if (value instanceof Date) {
return this.dateService.format(value, JALALI_DATE_FORMATS.parse.dateInput);
}
return null;
}
}
function provideJalaiDateAdapter(type = 'date') {
return [
{
provide: DateAdapter,
useClass: type === 'date'
? MaterialJalaliDateAdapter
: MaterialJalaliStringDateAdapter,
},
{ provide: MAT_DATE_LOCALE, useValue: 'fa-IR' },
{ provide: MAT_DATE_FORMATS, useValue: JALALI_DATE_FORMATS },
];
}
/*
* Public API Surface of angular-material-jalali-datepicker-adapter
*/
/**
* Generated bundle index. Do not edit.
*/
export { JALALI_DATE_FORMATS, JalaliDateService, MaterialJalaliDateAdapter, MaterialJalaliStringDateAdapter, provideJalaiDateAdapter };
//# sourceMappingURL=angular-material-jalali-datepicker-adapter.mjs.map