Myanmar Calendar Dart
A pure Dart package for Myanmar calendar calculation, conversion, formatting, holiday rules, and astrological information.
Results are checked day by day against the reference implementation,
ceMmDateTime.js: Myanmar dates, holidays
and astrology.
Requires Dart 3.8 or newer.
Migration
Upgrading from v2 or v1? See the migration guide: MIGRATION.md
Installation
dart pub add myanmar_calendar_dart
Quick Start
import 'package:myanmar_calendar_dart/myanmar_calendar_dart.dart';
void main() {
MyanmarCalendar.configure(
language: Language.english,
timezoneOffset: 6.5,
);
final today = MyanmarCalendar.today();
print(today.formatComplete(includeAstro: true, includeHolidays: true));
final converted = MyanmarCalendar.fromWestern(2024, 1, 1);
print(converted.formatMyanmar('&y &M &P &ff'));
}
Examples
A short tour (dates, a month grid, holidays, parsing):
dart run example/main.dart
The CLI demo app:
dart run example/lib/main.dart
With options:
dart run example/lib/main.dart \
--date=2024-01-04 \
--language=en \
--timezone=6.5 \
--cache=high \
--chronicle=true
Options:
--date=YYYY-MM-DDtarget Western date--language=en|my|zawgyi|mon|shan|karen--timezone=<offset>range-12..14--cache=default|high|memory|off--chronicle=true|false
What You Get
- Myanmar <-> Western date conversion
- Complete date object (Myanmar, Western, Shan, holidays, astro)
- Typed values (
MoonPhase,MyanmarYearType,SabbathType) and holiday IDs - Myanmar and Western date formatting with localization
- Built-in holidays with flexible disabling rules
- Custom holiday rules (typed matcher + date-rule helpers)
- Pluggable western holiday provider (Eid/Diwali/Chinese New Year)
- Chronicle and dynasty lookup APIs
- Cache controls for throughput and memory tuning
Core API
Main entry point
final date = MyanmarCalendar.today();
final fromWestern = MyanmarCalendar.fromWestern(2024, 4, 17);
final fromMyanmar = MyanmarCalendar.fromMyanmar(1385, 1, 1);
final complete = MyanmarCalendar.getCompleteDate(DateTime.now());
final astro = MyanmarCalendar.getAstroInfo(date.myanmarDate);
final holidays = MyanmarCalendar.getHolidayInfo(date.myanmarDate);
// A calendar day, with no time or time zone involved.
final day = MyanmarCalendar.getCompleteDateFor(2026, 10, 18);
// Another time zone or holiday set for one call, without touching the
// global configuration.
final utcDay = MyanmarCalendar.getCompleteDate(
DateTime.now().toUtc(),
config: const CalendarConfig(timezoneOffset: 0),
);
Dates, times and time zones
- A local
DateTimeis read by its fields, as wall-clock time in the configured time zone (Myanmar Time by default), whatever the device's zone. - A UTC
DateTime, an epoch value (fromMillisecondsSinceEpoch,fromTimestamp) andnow()are instants, shown in the configured zone. - The Myanmar date depends only on the calendar day, never on the time of
day. For calendar grids, prefer
getCompleteDateFor(year, month, day). MyanmarDateTime.toUtc()andmillisecondsSinceEpochgive the instant;toDateTime()gives the wall-clock fields.
Instance-first client (recommended for isolation)
MyanmarCalendarClient has the whole API and never reads global state;
MyanmarCalendar is a static convenience over a client for the global
configuration. Without a config, a client uses the package defaults.
final client = MyanmarCalendar.createClient(
config: const CalendarConfig(
defaultLanguage: 'en',
timezoneOffset: 6.5,
),
cacheConfig: const CacheConfig.memoryEfficient(),
);
final complete = client.getCompleteDate(DateTime(2024, 1, 4));
final yearInfo = client.getMyanmarYearInfo(1385); // MyanmarYearInfo (typed)
print(yearInfo.yearType);
Formatting
final myanmarText = date.formatMyanmar('&y &M &P &ff');
final westernText = date.formatWestern('%yyyy-%mm-%dd');
final localized = MyanmarCalendar.format(
date,
language: Language.myanmar,
);
Myanmar format tokens
Common tokens:
&yMyanmar year&MMyanmar month name&Pmoon phase&fffortnight day (zero padded)&Wweekday name&Nyear name cycle&Naylocalized day word (Nay)&Yatlocalized date-day word (Yat)
final withYat = date.formatMyanmar('&d &Yat', Language.myanmar);
final withNay = date.formatMyanmar('&d &Nay', Language.myanmar);
Validation and parsing
final valid = MyanmarCalendar.isValidMyanmar(1385, 10, 1);
final result = MyanmarCalendar.validateMyanmar(1385, 10, 1);
// Throws DateParseException if the text isn't a date that exists.
final parsedMyanmar = MyanmarCalendar.parseMyanmar('1385/10/1');
final parsedWestern = MyanmarCalendar.parseWestern('2024-01-01');
// Or get null instead.
final maybe = MyanmarCalendar.tryParseMyanmar(userInput);
Myanmar dates are written year first (1385/10/1, 1385-10-1) or day first
(1.10.1385); Myanmar digits work too.
Typed values
The int codes stay, with typed getters beside them:
final day = MyanmarCalendar.getCompleteDateFor(2026, 10, 26);
if (day.phase == MoonPhase.fullMoon) print('Full moon');
if (day.yearKind.isWatat) print('Watat year');
if (day.astro.sabbathType == SabbathType.sabbath) print('Sabbath');
print(day.myanmar.dartWeekday); // 1 = Monday, like DateTime.weekday
Holiday Customization
Add custom holidays
final myHoliday = CustomHoliday.westernDate(
id: 'team_day',
name: 'Team Day',
type: HolidayType.cultural,
month: 7,
day: 27,
cacheVersion: 1,
);
MyanmarCalendar.configure(customHolidayRules: [myHoliday]);
When you change predicate logic but keep the same holiday ID, increment
cacheVersion so cached holiday results are invalidated deterministically.
You can also write a typed matcher directly:
final fullMoonFestival = CustomHoliday(
id: 'full_moon_festival',
name: 'Full Moon Festival',
type: HolidayType.cultural,
cacheVersion: 1,
localizedNames: {
Language.myanmar: 'လပြည့်ပွဲ',
},
matcher: (context) {
return context.myanmarDate.phase == MoonPhase.fullMoon;
},
);
Legacy untyped holiday predicates are removed. Use matcher: (context) { ... }.
Removed legacy aliases:
CustomHoliday.legacy(...)MyanmarCalendar.addCustomHoliday(...)MyanmarCalendar.addCustomHolidays(...)MyanmarCalendar.removeCustomHoliday(...)MyanmarCalendar.clearCustomHolidays(...)
Check for a holiday by ID
Holiday names are translated, so compare IDs instead:
final info = MyanmarCalendar.getCompleteDateFor(2026, 4, 14).holidays;
if (info.has(HolidayId.thingyanAkya)) print('Thingyan Akya');
if (info.hasCustom('team_day')) print('Team day');
for (final holiday in info.entries) {
print('${holiday.id ?? holiday.customId}: ${holiday.name}');
}
Disable built-in holidays
MyanmarCalendar.configure(
disabledHolidays: [HolidayId.independenceDay],
disabledHolidaysByYear: {
2026: [HolidayId.newYearDay],
},
disabledHolidaysByDate: {
'2026-04-17': [HolidayId.myanmarNewYearDay],
},
);
Override western-calendar holiday lookup
Use this when you need custom or more accurate date tables for holidays like Eid, Diwali, or Chinese New Year.
const provider = TableWesternHolidayProvider(
singleDayRules: {
HolidayId.diwali: {
2045: WesternHolidayDate(month: 11, day: 2),
},
},
multiDayRules: {
HolidayId.eidAlFitr: {
2045: [
WesternHolidayDate(month: 1, day: 1),
WesternHolidayDate(month: 1, day: 2),
],
},
},
);
MyanmarCalendar.configure(westernHolidayProvider: provider);
Use an empty provider to disable built-in table-based western holiday matches:
MyanmarCalendar.configure(
westernHolidayProvider: const TableWesternHolidayProvider(),
);
For a custom provider class, implement matches and a cacheKey that
changes whenever its rules change (results are cached by it):
class MyHolidayProvider extends WesternHolidayProvider {
const MyHolidayProvider({required this.version});
final int version;
@override
String get cacheKey => 'my_holiday_provider:v$version';
@override
bool matches(HolidayId holidayId, int year, int month, int day) {
// Implement your lookup logic
return false;
}
}
Remote Configuration (Firebase or Backend)
You can deliver custom holiday rules dynamically from Firebase Remote Config (Flutter apps) or any backend API (pure Dart/server apps).
Suggested JSON payload:
{
"holidayRules": [
{
"id": "team_day",
"name": "Team Day",
"type": "cultural",
"kind": "western_fixed",
"month": 7,
"day": 27,
"cacheVersion": 1,
"localizedNames": {
"my": "အသင်းနေ့"
}
}
]
}
Map remote payload to CustomHoliday rules:
import 'dart:convert';
import 'package:myanmar_calendar_dart/myanmar_calendar_dart.dart';
HolidayType _holidayTypeFromString(String value) {
return HolidayType.values.firstWhere(
(type) => type.name == value,
orElse: () => HolidayType.other,
);
}
Map<Language, String> _localizedNames(dynamic raw) {
if (raw is! Map<String, dynamic>) return const {};
final result = <Language, String>{};
for (final entry in raw.entries) {
result[Language.fromCode(entry.key)] = entry.value as String;
}
return result;
}
List<CustomHoliday> parseHolidayRules(Map<String, dynamic> payload) {
final rules = payload['holidayRules'] as List<dynamic>? ?? const [];
return rules.map((raw) {
final item = raw as Map<String, dynamic>;
final id = item['id'] as String;
final name = item['name'] as String;
final type = _holidayTypeFromString(item['type'] as String? ?? 'other');
final cacheVersion = item['cacheVersion'] as int? ?? 1;
final localized = _localizedNames(item['localizedNames']);
final kind = item['kind'] as String? ?? 'western_fixed';
if (kind == 'myanmar_fixed') {
return CustomHoliday.myanmarDate(
id: id,
name: name,
type: type,
month: item['month'] as int,
day: item['day'] as int,
year: item['year'] as int?,
fromYear: item['fromYear'] as int?,
toYear: item['toYear'] as int?,
cacheVersion: cacheVersion,
localizedNames: localized,
);
}
return CustomHoliday.westernDate(
id: id,
name: name,
type: type,
month: item['month'] as int,
day: item['day'] as int,
year: item['year'] as int?,
fromYear: item['fromYear'] as int?,
toYear: item['toYear'] as int?,
cacheVersion: cacheVersion,
localizedNames: localized,
);
}).toList();
}
Apply rules from Firebase Remote Config (Flutter app):
final jsonString = remoteConfig.getString('myanmar_calendar_rules');
final payload = jsonDecode(jsonString) as Map<String, dynamic>;
final rules = parseHolidayRules(payload);
MyanmarCalendar.configure(customHolidayRules: rules);
Apply rules from a custom backend:
final response = await client.get(Uri.parse('https://api.example.com/calendar/rules'));
final payload = jsonDecode(response.body) as Map<String, dynamic>;
final rules = parseHolidayRules(payload);
MyanmarCalendar.configure(customHolidayRules: rules);
When remote rule logic changes, increment cacheVersion for each changed rule.
Caching
Caching is enabled by default to improve repeated conversion and lookup paths.
Configure cache profile
// Default profile
MyanmarCalendar.configureCache(const CacheConfig());
// Larger cache for heavy throughput
MyanmarCalendar.configureCache(const CacheConfig.highPerformance());
// Smaller cache with TTL
MyanmarCalendar.configureCache(const CacheConfig.memoryEfficient());
// Disable cache completely
MyanmarCalendar.configureCache(const CacheConfig.disabled());
Cache operations
MyanmarCalendar.clearCache();
MyanmarCalendar.resetCacheStatistics();
final stats = MyanmarCalendar.getCacheStatistics();
print(stats.hitRate);
print(stats.toMap());
Notes
- Cache entries are isolated by configuration fingerprint to avoid cross-config collisions.
- Holiday and complete-date cache entries are language-aware.
- Custom holiday cache keys are based on stable
cacheKey/cacheVersionvalues. - Western holiday provider cache keys use
westernHolidayProvider.cacheKey. - Complete dates are cached per calendar day, so repeated calls with
DateTime.now()reuse one entry. - If your workload is mostly one-off conversions, disabling cache is reasonable.
Localization
Supported languages:
Language.englishLanguage.myanmarLanguage.zawgyiLanguage.monLanguage.shanLanguage.karen
MyanmarCalendar.setLanguage(Language.myanmar);
final mm = MyanmarCalendar.getCompleteDate(
DateTime(2024, 1, 4),
language: Language.myanmar,
);
final en = MyanmarCalendar.getCompleteDate(
DateTime(2024, 1, 4),
language: Language.english,
);
setLanguage updates the default language. For per-request localization, pass
language: directly to APIs that support it.
TranslationService.addTranslation changes a word everywhere, including
results already cached; TranslationService.resetTranslations() restores the
built-in words.
AI Prompts
Prompts for AI assistants live in their own library:
import 'package:myanmar_calendar_dart/ai.dart';
final prompt = AIPromptService.generatePrompt(
MyanmarCalendar.getCompleteDateFor(2026, 10, 18),
language: Language.myanmar,
type: AIPromptType.horoscope,
);
Chronicle APIs
final entries = MyanmarCalendar.getChronicleFor(DateTime(1752, 9, 14));
final dynasties = MyanmarCalendar.listDynasties();
final dynasty = MyanmarCalendar.getDynastyById('konbaung');
Error Handling
Invalid input throws a MyanmarCalendarException:
InvalidMyanmarDateException: a Myanmar date that doesn't exist (such as day 30 of a 29-day month, or First Waso in a common year)InvalidWesternDateException: a Western date or time that doesn't existDateParseException: text that isn't a date (also aFormatException)InvalidConfigurationException: a config value out of range
try {
MyanmarCalendar.fromWestern(2024, 2, 30);
} on InvalidWesternDateException catch (e) {
print(e.message);
}
Mistakes in calling code, such as a reversed date range, are reported with
ArgumentError.
Development
dart pub get
dart format --output=none --set-exit-if-changed .
dart analyze
dart test
Reference parity fixtures
The package includes golden fixture tests that compare Dart outputs with
reference/ceMmDateTime.js.
Regenerate fixtures when algorithm behavior changes:
node tool/generate_reference_parity_fixtures.mjs
node tool/generate_calendar_sweep_fixtures.mjs
dart test test/parity_reference_test.dart test/calendar_sweep_parity_test.dart
The sweep checks every day of several ranges: Myanmar dates both ways, holidays for 2010-2035 and astrology for 2020-2030.
Benchmark
dart run benchmark/month_grid_benchmark.dart
Acknowledgements
The core calculation algorithms are based on the original work by Dr Yan Naing Aye.
License
MIT License. See LICENSE.
Libraries
- ai
- Prompts for AI assistants built from a date's Myanmar calendar and astrological details. Kept apart from the core calendar API.
- myanmar_calendar_dart
- Myanmar Calendar Package