├── .gitignore ├── example ├── toKhmerDate.js ├── today.js ├── newYearNext.js ├── enum-usage.js └── roundTrip.js ├── tsconfig.json ├── momentkh.browser.js ├── LICENSE ├── test ├── animal-emojis.test.js ├── abbreviations.test.js ├── generate-new-year.js ├── raw-formats.test.js ├── verify-conversion.test.js ├── verify-new-year.test.js ├── generate-conversions.js ├── verify-roundtrip.test.js ├── escape-format.test.js ├── validation.test.js ├── gregorian-to-khmer.test.js ├── new-year.test.js ├── khmer-to-gregorian.test.js └── new-year.json ├── package.json ├── dist ├── momentkh.d.ts.map ├── momentkh.d.ts └── momentkh.js ├── index.html └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .idea/* 3 | node_modules/ -------------------------------------------------------------------------------- /example/toKhmerDate.js: -------------------------------------------------------------------------------- 1 | const momentkh = require('@thyrith/momentkh'); 2 | 3 | const khmer = momentkh.fromGregorian(2026,5,2,0,0,0); 4 | console.log(momentkh.format(khmer)); -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2016", 4 | "module": "commonjs", 5 | "lib": ["ES2016", "DOM"], 6 | "declaration": true, 7 | "declarationMap": true, 8 | "outDir": "./dist", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true, 13 | "moduleResolution": "node" 14 | }, 15 | "include": ["momentkh.ts"], 16 | "exclude": ["node_modules", "dist", "test"] 17 | } 18 | -------------------------------------------------------------------------------- /momentkh.browser.js: -------------------------------------------------------------------------------- 1 | // Browser-compatible wrapper for MomentKH 2 | // This file wraps the CommonJS module for browser use 3 | 4 | (function(global) { 5 | 'use strict'; 6 | 7 | // Create a minimal module system for the browser 8 | var module = { exports: {} }; 9 | var exports = module.exports; 10 | 11 | // Include the compiled momentkh.js code 12 | // (The actual code will be injected here during build) 13 | 14 | // For now, we'll load it via a separate script tag and expose it 15 | // After the main momentkh.js loads, expose it globally 16 | if (typeof define === 'function' && define.amd) { 17 | // AMD 18 | define([], function() { return module.exports.momentkh || module.exports; }); 19 | } else if (typeof module !== 'undefined' && module.exports) { 20 | // CommonJS 21 | // Already set 22 | } else { 23 | // Browser global 24 | global.momentkh = module.exports.momentkh || module.exports; 25 | } 26 | 27 | })(typeof window !== 'undefined' ? window : this); 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 ThyrithSor 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/animal-emojis.test.js: -------------------------------------------------------------------------------- 1 | 2 | const { fromGregorian, format, constants } = require('../dist/momentkh'); 3 | const assert = require('assert'); 4 | 5 | console.log('Testing Animal Year Emojis...'); 6 | 7 | try { 8 | // Constants checks 9 | assert.strictEqual(constants.AnimalYearEmojis.length, 12, 'AnimalYearEmojis should have 12 items'); 10 | 11 | // Verify emoji mapping 12 | const expectedEmojis = ['🐀', '🐂', '🐅', '🐇', '🐉', '🐍', '🐎', '🐐', '🐒', '🐓', '🐕', '🐖']; 13 | assert.deepStrictEqual(constants.AnimalYearEmojis, expectedEmojis, 'Emoji list does not match expected list'); 14 | 15 | // Test Case 1: 2024 is Year of the Dragon (Rong) 16 | // New Year 2024 starts April 13. 17 | // Before April 13, it's Rabbit (Thoh) 🐇 18 | // After April 13 (specifically after the time), it's Dragon (Rong) 🐉 19 | 20 | const dateBeforeNY = fromGregorian(2024, 4, 1); 21 | const emojiBefore = format(dateBeforeNY, 'as'); 22 | console.log(`2024-04-01 (Before NY) -> as: ${emojiBefore}`); 23 | assert.strictEqual(emojiBefore, '🐇', 'Should be Rabbit before NY 2024'); 24 | 25 | const dateAfterNY = fromGregorian(2024, 5, 1); 26 | const emojiAfter = format(dateAfterNY, 'as'); 27 | console.log(`2024-05-01 (After NY) -> as: ${emojiAfter}`); 28 | assert.strictEqual(emojiAfter, '🐉', 'Should be Dragon after NY 2024'); 29 | 30 | console.log('SUCCESS: All emoji tests passed!'); 31 | 32 | } catch (e) { 33 | console.error('FAILURE:', e); 34 | process.exit(1); 35 | } 36 | -------------------------------------------------------------------------------- /example/today.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example 1: Get Khmer date of today 3 | * 4 | * This example demonstrates how to convert today's date 5 | * to the Khmer calendar format. 6 | */ 7 | 8 | const momentkh = require('@thyrith/momentkh'); 9 | 10 | // Get today's date 11 | const today = new Date(); 12 | const khmer = momentkh.fromDate(today); 13 | 14 | // Display results 15 | console.log('='.repeat(80)); 16 | console.log('Example 1: Get Khmer Date of Today'); 17 | console.log('='.repeat(80)); 18 | console.log(); 19 | 20 | console.log('Today (Gregorian):'); 21 | console.log(` Date: ${today.toDateString()}`); 22 | console.log(` Time: ${today.toTimeString()}`); 23 | console.log(); 24 | 25 | console.log('Today (Khmer Calendar):'); 26 | console.log(` Formatted: ${momentkh.format(khmer)}`); 27 | console.log(); 28 | 29 | console.log('Detailed Information:'); 30 | console.log(` Day: ${khmer.khmer.day}${khmer.khmer.moonPhase === 0 ? 'កើត' : 'រោច'}`); 31 | console.log(` Month: ${khmer.khmer.monthName}`); 32 | console.log(` BE Year: ${khmer.khmer.beYear}`); 33 | console.log(` Animal Year: ${khmer.khmer.animalYear}`); 34 | console.log(` Sak: ${khmer.khmer.sak}`); 35 | console.log(` Weekday: ${khmer.khmer.dayOfWeek}`); 36 | console.log(); 37 | 38 | console.log('Custom Formats:'); 39 | console.log(` Format 1: ${momentkh.format(khmer, 'W dN ខែm ព.ស.b')}`); 40 | console.log(` Format 2: ${momentkh.format(khmer, 'dN m ឆ្នាំa e')}`); 41 | console.log(` Format 3: ${momentkh.format(khmer, 'c/M/D - j/m/D')}`); 42 | console.log(); 43 | 44 | console.log('='.repeat(80)); 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@thyrith/momentkh", 3 | "version": "3.0.2", 4 | "description": "Working on khmer calendar by implementting moment js", 5 | "license": "MIT", 6 | "type": "component", 7 | "repository": "ThyrithSor/momentkh", 8 | "homepage": "https://github.com/ThyrithSor/momentkh", 9 | "main": "momentkh.js", 10 | "types": "dist/momentkh.d.ts", 11 | "files": [ 12 | "momentkh.js", 13 | "momentkh.ts", 14 | "dist/", 15 | "README.md", 16 | "LICENSE" 17 | ], 18 | "scripts": { 19 | "build": "tsc && node build-browser.js", 20 | "build:dist": "tsc", 21 | "test": "npm run test:validation && npm run test:gregorian && npm run test:khmer && npm run test:newyear && npm run test:verify-conversion && npm run test:verify-newyear && npm run test:verify-roundtrip", 22 | "test:validation": "node test/validation.test.js", 23 | "test:gregorian": "node test/gregorian-to-khmer.test.js", 24 | "test:khmer": "node test/khmer-to-gregorian.test.js", 25 | "test:newyear": "node test/new-year.test.js", 26 | "test:verify-conversion": "node test/verify-conversion.test.js", 27 | "test:verify-newyear": "node test/verify-new-year.test.js", 28 | "test:verify-roundtrip": "node test/verify-roundtrip.test.js", 29 | "prepublishOnly": "npm run build && npm test" 30 | }, 31 | "authors": [ 32 | { 33 | "name": "Thyrith Sor", 34 | "email": "sor.thyrith@gmail.com" 35 | } 36 | ], 37 | "keywords": [ 38 | "moment", 39 | "khmer", 40 | "calendar", 41 | "lunar", 42 | "date", 43 | "chankiti", 44 | "chhankitek", 45 | "ប្រតិទិនខ្មែរ", 46 | "ចន្ទគតិ" 47 | ], 48 | "extra": { 49 | "component": { 50 | "scripts": [ 51 | "momentkh.js" 52 | ] 53 | } 54 | }, 55 | "dependencies": { 56 | }, 57 | "spm": { 58 | "main": "momentkh.js", 59 | "output": [ 60 | "locale/*.js" 61 | ] 62 | }, 63 | "devDependencies": { 64 | "typescript": "^5.9.3" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /test/abbreviations.test.js: -------------------------------------------------------------------------------- 1 | 2 | const { fromGregorian, format, constants } = require('../dist/momentkh'); 3 | const assert = require('assert'); 4 | 5 | console.log('Testing Month Abbreviations...'); 6 | 7 | try { 8 | // Constants checks 9 | assert.strictEqual(constants.SolarMonthAbbreviationNames.length, 12, 'Solar month abbreviations should have 12 items'); 10 | assert.strictEqual(constants.LunarMonthAbbreviationNames.length, 14, 'Lunar month abbreviations should have 14 items'); 11 | 12 | // Test Case 1: Solar Month Abbreviations (Ms) 13 | 14 | // January (1) -> 'មក' 15 | const dateJan = fromGregorian(2023, 1, 1); 16 | const msJan = format(dateJan, 'Ms'); 17 | console.log(`Jan (1) -> Ms: ${msJan}`); 18 | assert.strictEqual(msJan, 'មក'); 19 | 20 | // December (12) -> 'ធន' 21 | const dateDec = fromGregorian(2023, 12, 1); 22 | const msDec = format(dateDec, 'Ms'); 23 | console.log(`Dec (12) -> Ms: ${msDec}`); 24 | assert.strictEqual(msDec, 'ធន'); 25 | 26 | // Test Case 2: Lunar Month Abbreviations (ms) 27 | 28 | // Migasir (0) -> 'មិ' 29 | // 2023-11-20 is likely in Kadeuk or Migasir? Let's construct specifically. 30 | // We can mock the object or search for a date. 31 | // 2023-12-15 is likely in Migasir. 32 | const dateMigasir = fromGregorian(2023, 12, 15); 33 | // Let's check what month it actually is 34 | console.log(`Actual month index: ${dateMigasir.khmer.monthIndex} (${dateMigasir.khmer.monthName})`); 35 | const msLunar = format(dateMigasir, 'ms'); 36 | console.log(`Abbr 'ms': ${msLunar}`); 37 | 38 | // Check against the constant array directly to be sure 39 | const expectedLunarAbbr = constants.LunarMonthAbbreviationNames[dateMigasir.khmer.monthIndex]; 40 | assert.strictEqual(msLunar, expectedLunarAbbr); 41 | 42 | console.log('SUCCESS: All abbreviation tests passed!'); 43 | 44 | } catch (e) { 45 | console.error('FAILURE:', e); 46 | process.exit(1); 47 | } 48 | -------------------------------------------------------------------------------- /example/newYearNext.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example 2: Get Khmer New Year datetime of next year 3 | * 4 | * This example demonstrates how to calculate the exact date and time 5 | * of Khmer New Year (Moha Songkran) for next year. 6 | */ 7 | 8 | const momentkh = require('@thyrith/momentkh'); 9 | 10 | // Get next year 11 | const currentYear = new Date().getFullYear(); 12 | const nextYear = currentYear + 1; 13 | 14 | // Get Khmer New Year information 15 | const newYear = momentkh.getNewYear(nextYear); 16 | 17 | // Convert to Khmer calendar for additional info 18 | const khmerNewYear = momentkh.fromGregorian( 19 | newYear.year, 20 | newYear.month, 21 | newYear.day, 22 | newYear.hour, 23 | newYear.minute 24 | ); 25 | 26 | // Display results 27 | console.log('='.repeat(80)); 28 | console.log('Example 2: Get Khmer New Year DateTime of Next Year'); 29 | console.log('='.repeat(80)); 30 | console.log(); 31 | 32 | console.log(`Khmer New Year ${nextYear}:`); 33 | console.log(` Gregorian Date: ${newYear.year}-${String(newYear.month).padStart(2, '0')}-${String(newYear.day).padStart(2, '0')}`); 34 | console.log(` Time: ${String(newYear.hour).padStart(2, '0')}:${String(newYear.minute).padStart(2, '0')}`); 35 | console.log(` Exact Moment: ${newYear.year}-${String(newYear.month).padStart(2, '0')}-${String(newYear.day).padStart(2, '0')} ${String(newYear.hour).padStart(2, '0')}:${String(newYear.minute).padStart(2, '0')}`); 36 | console.log(); 37 | 38 | console.log('Khmer Calendar Information:'); 39 | console.log(` Khmer Date: ${momentkh.format(khmerNewYear, 'dN ខែm')}`); 40 | console.log(` BE Year: ${khmerNewYear.khmer.beYear}`); 41 | console.log(` Animal Year: ${khmerNewYear.khmer.animalYear}`); 42 | console.log(` Sak: ${khmerNewYear.khmer.sak}`); 43 | console.log(` Weekday: ${khmerNewYear.khmer.dayOfWeek}`); 44 | console.log(); 45 | 46 | console.log('Full Formatted:'); 47 | console.log(` ${momentkh.format(khmerNewYear)}`); 48 | console.log(); 49 | 50 | // Compare with current year 51 | const currentNewYear = momentkh.getNewYear(currentYear); 52 | console.log(`Previous New Year (${currentYear}):`); 53 | console.log(` Date: ${currentNewYear.year}-${String(currentNewYear.month).padStart(2, '0')}-${String(currentNewYear.day).padStart(2, '0')}`); 54 | console.log(` Time: ${String(currentNewYear.hour).padStart(2, '0')}:${String(currentNewYear.minute).padStart(2, '0')}`); 55 | console.log(); 56 | 57 | // Calculate days until next New Year 58 | const today = new Date(); 59 | const newYearDate = new Date(newYear.year, newYear.month - 1, newYear.day, newYear.hour, newYear.minute); 60 | const daysUntil = Math.ceil((newYearDate - today) / (1000 * 60 * 60 * 24)); 61 | 62 | console.log(`Days until Khmer New Year ${nextYear}: ${daysUntil} days`); 63 | console.log(); 64 | 65 | console.log('='.repeat(80)); 66 | -------------------------------------------------------------------------------- /test/generate-new-year.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate Khmer New Year moments for range of years 3 | * Output: test/new-year.json 4 | */ 5 | 6 | const momentkh = require('../momentkh'); 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | 10 | console.log('Generating Khmer New Year data...\n'); 11 | 12 | // Generate New Year moments from 1800 to 2300 13 | const startYear = 1800; 14 | const endYear = 2300; 15 | const newYearData = {}; 16 | 17 | let count = 0; 18 | const total = endYear - startYear + 1; 19 | 20 | for (let year = startYear; year <= endYear; year++) { 21 | try { 22 | // Get New Year info 23 | const newYear = momentkh.getNewYear(year); 24 | 25 | // Create Date object from the New Year info 26 | const newYearDate = new Date( 27 | newYear.year, 28 | newYear.month - 1, // JavaScript months are 0-indexed 29 | newYear.day, 30 | newYear.hour, 31 | newYear.minute, 32 | 0, // seconds 33 | 0 // milliseconds 34 | ); 35 | 36 | // Format as ISO string with UTC+7:00 timezone 37 | // JavaScript Date treats local time, so we format it as local time with +07:00 offset 38 | const year4 = newYearDate.getFullYear(); 39 | const month2 = String(newYearDate.getMonth() + 1).padStart(2, '0'); 40 | const day2 = String(newYearDate.getDate()).padStart(2, '0'); 41 | const hour2 = String(newYearDate.getHours()).padStart(2, '0'); 42 | const minute2 = String(newYearDate.getMinutes()).padStart(2, '0'); 43 | const second2 = String(newYearDate.getSeconds()).padStart(2, '0'); 44 | 45 | const isoString = `${year4}-${month2}-${day2}T${hour2}:${minute2}:${second2}+07:00`; 46 | 47 | // Add to data object 48 | newYearData[year] = isoString; 49 | 50 | count++; 51 | 52 | // Progress indicator 53 | if (count % 100 === 0) { 54 | console.log(`Generated ${count}/${total} years...`); 55 | } 56 | } catch (error) { 57 | console.error(`Error generating New Year for ${year}: ${error.message}`); 58 | } 59 | } 60 | 61 | // Write to file 62 | const outputPath = path.join(__dirname, 'new-year.json'); 63 | fs.writeFileSync( 64 | outputPath, 65 | JSON.stringify(newYearData, null, 2), 66 | 'utf8' 67 | ); 68 | 69 | console.log(`\n✓ Successfully generated ${count} New Year moments`); 70 | console.log(`✓ Year range: ${startYear} - ${endYear}`); 71 | console.log(`✓ Saved to: ${outputPath}`); 72 | console.log(`\nSample entries:`); 73 | 74 | // Show some sample entries 75 | const sampleYears = [1900, 1950, 2000, 2024, 2050, 2100, 2200]; 76 | sampleYears.forEach(year => { 77 | if (newYearData[year]) { 78 | const date = new Date(newYearData[year]); 79 | console.log(` ${year}: ${date.toLocaleString('en-US', { 80 | year: 'numeric', 81 | month: 'short', 82 | day: '2-digit', 83 | hour: '2-digit', 84 | minute: '2-digit', 85 | hour12: false 86 | })}`); 87 | } 88 | }); 89 | -------------------------------------------------------------------------------- /test/raw-formats.test.js: -------------------------------------------------------------------------------- 1 | 2 | const { fromGregorian, format } = require('../dist/momentkh'); 3 | const assert = require('assert'); 4 | 5 | console.log('Testing Raw Format Codes...'); 6 | 7 | try { 8 | // Test Date: Use fromKhmer to guarantee specific Khmer values 9 | // Day 14, Waxing (0), Pisakh (5), 2568 -> May 2024 10 | const greg = require('../dist/momentkh').fromKhmer(14, 0, 5, 2568); 11 | const date = fromGregorian(greg.year, greg.month, greg.day); 12 | 13 | // Test Case 1: Day (d vs dr) 14 | const d = format(date, 'd'); 15 | const dr = format(date, 'dr'); 16 | console.log(`Day: d='${d}', dr='${dr}'`); 17 | assert.strictEqual(d, '១៤', 'd should be Khmer numeral 14'); 18 | assert.strictEqual(dr, '14', 'dr should be Latin numeral 14'); 19 | 20 | // Test Case 2: Padded Day (D vs Dr) 21 | // Use a single digit day (Day 5) 22 | const gregSingle = require('../dist/momentkh').fromKhmer(5, 0, 4, 2568); 23 | const dateSingle = fromGregorian(gregSingle.year, gregSingle.month, gregSingle.day); 24 | const D = format(dateSingle, 'D'); 25 | const Dr = format(dateSingle, 'Dr'); 26 | console.log(`Padded Day: D='${D}', Dr='${Dr}'`); 27 | assert.strictEqual(D, '០៥', 'D should be Khmer numeral padded'); 28 | assert.strictEqual(Dr, '05', 'Dr should be Latin numeral padded'); 29 | 30 | // Test Case 3: BE Year (b vs br) 31 | // BE for April 2024 is 2567 (before new year logic updates era? Wait, logic is complex.) 32 | // Let's check what it actually is. 33 | const b = format(date, 'b'); 34 | const br = format(date, 'br'); 35 | console.log(`BE Year: b='${b}', br='${br}'`); 36 | // Just ensure one is khmer and one is latin version of the same number 37 | // Simple check: b contains Khmer digits, br contains Latin digits 38 | assert.match(b, /^[០-៩]+$/, 'b should contain Khmer digits'); 39 | assert.match(br, /^[0-9]+$/, 'br should contain Latin digits'); 40 | 41 | // Test Case 4: CE Year (c vs cr) 42 | const c = format(date, 'c'); 43 | const cr = format(date, 'cr'); 44 | console.log(`CE Year: c='${c}', cr='${cr}'`); 45 | assert.strictEqual(c, '២០២៥', 'c should be Khmer numeral'); 46 | assert.strictEqual(cr, '2025', 'cr should be Latin numeral'); 47 | 48 | // Test Case 5: JS Year (j vs jr) 49 | const j = format(date, 'j'); 50 | const jr = format(date, 'jr'); 51 | console.log(`JS Year: j='${j}', jr='${jr}'`); 52 | assert.match(j, /^[០-៩]+$/, 'j should contain Khmer digits'); 53 | assert.match(jr, /^[0-9]+$/, 'jr should contain Latin digits'); 54 | 55 | // Test Case 6: Mixed usage 56 | // "Day 14 Month 14 Year 2025" 57 | // Use escaped brackets for text containing format tokens (D, a, M, o, n, e, a, r) 58 | const mixed = format(date, '[Day] dr [Month] Dr [Year] cr'); 59 | console.log(`Mixed: '${mixed}'`); 60 | assert.strictEqual(mixed, 'Day 14 Month 14 Year 2025'); 61 | 62 | console.log('SUCCESS: All raw format tests passed!'); 63 | 64 | } catch (e) { 65 | console.error('FAILURE:', e); 66 | process.exit(1); 67 | } 68 | -------------------------------------------------------------------------------- /test/verify-conversion.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Suite: Verify Gregorian to Khmer Conversion 3 | * Validates that all dates in conversion.json produce the same Khmer format 4 | */ 5 | 6 | const momentkh = require('../momentkh'); 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | 10 | console.log('='.repeat(80)); 11 | console.log('TEST SUITE: VERIFY GREGORIAN TO KHMER CONVERSION'); 12 | console.log('='.repeat(80)); 13 | console.log(); 14 | 15 | // Load conversion data 16 | const dataPath = path.join(__dirname, 'conversion.json'); 17 | const conversionData = JSON.parse(fs.readFileSync(dataPath, 'utf8')); 18 | 19 | let totalTests = 0; 20 | let passedTests = 0; 21 | let failedTests = 0; 22 | const failures = []; 23 | 24 | // Test each conversion 25 | for (const [gregorianStr, expectedKhmer] of Object.entries(conversionData)) { 26 | totalTests++; 27 | 28 | // Parse gregorian datetime string (format: YYYY-MM-DD HH:MM:SS) 29 | const [datePart, timePart] = gregorianStr.split(' '); 30 | const [year, month, day] = datePart.split('-').map(Number); 31 | const [hour, minute, second] = timePart.split(':').map(Number); 32 | 33 | try { 34 | // Convert to Khmer 35 | const result = momentkh.fromGregorian(year, month, day, hour, minute, second); 36 | const actualKhmer = momentkh.format(result); 37 | 38 | // Compare 39 | if (actualKhmer === expectedKhmer) { 40 | passedTests++; 41 | } else { 42 | failedTests++; 43 | failures.push({ 44 | gregorian: gregorianStr, 45 | expected: expectedKhmer, 46 | actual: actualKhmer 47 | }); 48 | } 49 | } catch (error) { 50 | failedTests++; 51 | failures.push({ 52 | gregorian: gregorianStr, 53 | expected: expectedKhmer, 54 | error: error.message 55 | }); 56 | } 57 | 58 | // Progress indicator 59 | if (totalTests % 500 === 0) { 60 | console.log(`Tested ${totalTests}/${Object.keys(conversionData).length} conversions...`); 61 | } 62 | } 63 | 64 | // Print summary 65 | console.log('\n' + '='.repeat(80)); 66 | console.log('TEST RESULTS SUMMARY'); 67 | console.log('='.repeat(80)); 68 | console.log(`Total Tests: ${totalTests}`); 69 | console.log(`Passed: ${passedTests} (${(passedTests / totalTests * 100).toFixed(2)}%)`); 70 | console.log(`Failed: ${failedTests} (${(failedTests / totalTests * 100).toFixed(2)}%)`); 71 | console.log('='.repeat(80)); 72 | 73 | // Print failures if any 74 | if (failures.length > 0) { 75 | console.log('\nFAILURES:'); 76 | console.log('-'.repeat(80)); 77 | failures.slice(0, 10).forEach((failure, index) => { 78 | console.log(`\n${index + 1}. ${failure.gregorian}`); 79 | console.log(` Expected: ${failure.expected}`); 80 | if (failure.actual) { 81 | console.log(` Actual: ${failure.actual}`); 82 | } 83 | if (failure.error) { 84 | console.log(` Error: ${failure.error}`); 85 | } 86 | }); 87 | if (failures.length > 10) { 88 | console.log(`\n... and ${failures.length - 10} more failures`); 89 | } 90 | console.log('\n' + '='.repeat(80)); 91 | process.exit(1); 92 | } else { 93 | console.log('\n✓ ALL TESTS PASSED!\n'); 94 | process.exit(0); 95 | } 96 | -------------------------------------------------------------------------------- /test/verify-new-year.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Suite: Verify New Year Calculations 3 | * Validates that all years in new-year.json produce the same result 4 | */ 5 | 6 | const momentkh = require('../momentkh'); 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | 10 | console.log('='.repeat(80)); 11 | console.log('TEST SUITE: VERIFY NEW YEAR CALCULATIONS'); 12 | console.log('='.repeat(80)); 13 | console.log(); 14 | 15 | // Load new year data 16 | const dataPath = path.join(__dirname, 'new-year.json'); 17 | const newYearData = JSON.parse(fs.readFileSync(dataPath, 'utf8')); 18 | 19 | let totalTests = 0; 20 | let passedTests = 0; 21 | let failedTests = 0; 22 | const failures = []; 23 | 24 | // Test each year 25 | for (const [yearStr, expectedIso] of Object.entries(newYearData)) { 26 | totalTests++; 27 | const year = parseInt(yearStr, 10); 28 | 29 | try { 30 | // Get New Year 31 | const newYear = momentkh.getNewYear(year); 32 | 33 | // Format as ISO string with +07:00 timezone 34 | const year4 = newYear.year; 35 | const month2 = String(newYear.month).padStart(2, '0'); 36 | const day2 = String(newYear.day).padStart(2, '0'); 37 | const hour2 = String(newYear.hour).padStart(2, '0'); 38 | const minute2 = String(newYear.minute).padStart(2, '0'); 39 | const actualIso = `${year4}-${month2}-${day2}T${hour2}:${minute2}:00+07:00`; 40 | 41 | // Compare 42 | if (actualIso === expectedIso) { 43 | passedTests++; 44 | } else { 45 | failedTests++; 46 | failures.push({ 47 | year: year, 48 | expected: expectedIso, 49 | actual: actualIso 50 | }); 51 | } 52 | } catch (error) { 53 | failedTests++; 54 | failures.push({ 55 | year: year, 56 | expected: expectedIso, 57 | error: error.message 58 | }); 59 | } 60 | 61 | // Progress indicator 62 | if (totalTests % 100 === 0) { 63 | console.log(`Tested ${totalTests}/${Object.keys(newYearData).length} years...`); 64 | } 65 | } 66 | 67 | // Print summary 68 | console.log('\n' + '='.repeat(80)); 69 | console.log('TEST RESULTS SUMMARY'); 70 | console.log('='.repeat(80)); 71 | console.log(`Total Tests: ${totalTests}`); 72 | console.log(`Passed: ${passedTests} (${(passedTests / totalTests * 100).toFixed(2)}%)`); 73 | console.log(`Failed: ${failedTests} (${(failedTests / totalTests * 100).toFixed(2)}%)`); 74 | console.log('='.repeat(80)); 75 | 76 | // Print failures if any 77 | if (failures.length > 0) { 78 | console.log('\nFAILURES:'); 79 | console.log('-'.repeat(80)); 80 | failures.slice(0, 10).forEach((failure, index) => { 81 | console.log(`\n${index + 1}. Year ${failure.year}`); 82 | console.log(` Expected: ${failure.expected}`); 83 | if (failure.actual) { 84 | console.log(` Actual: ${failure.actual}`); 85 | } 86 | if (failure.error) { 87 | console.log(` Error: ${failure.error}`); 88 | } 89 | }); 90 | if (failures.length > 10) { 91 | console.log(`\n... and ${failures.length - 10} more failures`); 92 | } 93 | console.log('\n' + '='.repeat(80)); 94 | process.exit(1); 95 | } else { 96 | console.log('\n✓ ALL TESTS PASSED!\n'); 97 | process.exit(0); 98 | } 99 | -------------------------------------------------------------------------------- /test/generate-conversions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Generate test data: 2000 random dates converted to Khmer format 3 | * Output: test/conversion.json 4 | */ 5 | 6 | const momentkh = require('../momentkh'); 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | 10 | console.log('Generating 2000 random date conversions...\n'); 11 | 12 | // Helper function to generate random date between 1900 and 2200 13 | function randomDate() { 14 | const year = Math.floor(Math.random() * (2200 - 1900 + 1)) + 1900; 15 | const month = Math.floor(Math.random() * 12) + 1; 16 | 17 | // Get valid day range for the month/year 18 | const daysInMonth = new Date(year, month, 0).getDate(); 19 | const day = Math.floor(Math.random() * daysInMonth) + 1; 20 | 21 | const hour = Math.floor(Math.random() * 24); 22 | const minute = Math.floor(Math.random() * 60); 23 | const second = Math.floor(Math.random() * 60); 24 | 25 | return { year, month, day, hour, minute, second }; 26 | } 27 | 28 | // Format Gregorian date as string 29 | function formatGregorianDateTime(date) { 30 | const year = String(date.year).padStart(4, '0'); 31 | const month = String(date.month).padStart(2, '0'); 32 | const day = String(date.day).padStart(2, '0'); 33 | const hour = String(date.hour).padStart(2, '0'); 34 | const minute = String(date.minute).padStart(2, '0'); 35 | const second = String(date.second).padStart(2, '0'); 36 | 37 | return `${year}-${month}-${day} ${hour}:${minute}:${second}`; 38 | } 39 | 40 | // Generate conversions 41 | const conversions = {}; 42 | const generated = new Set(); // Track unique dates 43 | 44 | let count = 0; 45 | while (count < 2000) { 46 | const date = randomDate(); 47 | const gregorianStr = formatGregorianDateTime(date); 48 | 49 | // Skip if we've already generated this date 50 | if (generated.has(gregorianStr)) { 51 | continue; 52 | } 53 | 54 | try { 55 | // Convert to Khmer 56 | const khmer = momentkh.fromGregorian( 57 | date.year, 58 | date.month, 59 | date.day, 60 | date.hour, 61 | date.minute, 62 | date.second 63 | ); 64 | 65 | // Format Khmer date 66 | const khmerFormatted = momentkh.format(khmer); 67 | 68 | // Add to conversions 69 | conversions[gregorianStr] = khmerFormatted; 70 | generated.add(gregorianStr); 71 | count++; 72 | 73 | // Progress indicator 74 | if (count % 100 === 0) { 75 | console.log(`Generated ${count}/2000 conversions...`); 76 | } 77 | } catch (error) { 78 | // Skip invalid dates (shouldn't happen with our validation) 79 | console.error(`Error converting ${gregorianStr}: ${error.message}`); 80 | } 81 | } 82 | 83 | // Sort the conversions by date for easier reading 84 | const sortedConversions = {}; 85 | Object.keys(conversions) 86 | .sort() 87 | .forEach(key => { 88 | sortedConversions[key] = conversions[key]; 89 | }); 90 | 91 | // Write to file 92 | const outputPath = path.join(__dirname, 'conversion.json'); 93 | fs.writeFileSync( 94 | outputPath, 95 | JSON.stringify(sortedConversions, null, 2), 96 | 'utf8' 97 | ); 98 | 99 | console.log(`\n✓ Successfully generated 2000 conversions`); 100 | console.log(`✓ Saved to: ${outputPath}`); 101 | console.log(`\nSample entries:`); 102 | 103 | // Show first 5 entries 104 | const entries = Object.entries(sortedConversions).slice(0, 5); 105 | entries.forEach(([gregorian, khmer]) => { 106 | console.log(` ${gregorian} => ${khmer}`); 107 | }); 108 | -------------------------------------------------------------------------------- /test/verify-roundtrip.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Suite: Verify Khmer to Gregorian Round-Trip 3 | * Validates that converting Khmer dates back to Gregorian produces the original date 4 | */ 5 | 6 | const momentkh = require('../momentkh'); 7 | const fs = require('fs'); 8 | const path = require('path'); 9 | 10 | console.log('='.repeat(80)); 11 | console.log('TEST SUITE: VERIFY KHMER TO GREGORIAN ROUND-TRIP'); 12 | console.log('='.repeat(80)); 13 | console.log(); 14 | 15 | // Load conversion data 16 | const dataPath = path.join(__dirname, 'conversion.json'); 17 | const conversionData = JSON.parse(fs.readFileSync(dataPath, 'utf8')); 18 | 19 | let totalTests = 0; 20 | let passedTests = 0; 21 | let failedTests = 0; 22 | const failures = []; 23 | 24 | // Test each conversion (round-trip) 25 | for (const [gregorianStr, khmerFormatted] of Object.entries(conversionData)) { 26 | totalTests++; 27 | 28 | // Parse original gregorian datetime 29 | const [datePart, timePart] = gregorianStr.split(' '); 30 | const [origYear, origMonth, origDay] = datePart.split('-').map(Number); 31 | const [origHour, origMinute, origSecond] = timePart.split(':').map(Number); 32 | 33 | try { 34 | // Convert to Khmer first 35 | const khmerResult = momentkh.fromGregorian(origYear, origMonth, origDay, origHour, origMinute, origSecond); 36 | 37 | // Extract Khmer date components 38 | const khmerDay = khmerResult.khmer.day; 39 | const khmerMoonPhase = khmerResult.khmer.moonPhase; 40 | const khmerMonthIndex = khmerResult.khmer.monthIndex; 41 | const khmerBeYear = khmerResult.khmer.beYear; 42 | 43 | // Convert back to Gregorian 44 | const gregorianResult = momentkh.fromKhmer(khmerDay, khmerMoonPhase, khmerMonthIndex, khmerBeYear); 45 | 46 | // Compare dates (ignore time for round-trip since fromKhmer doesn't preserve time) 47 | if ( 48 | gregorianResult.year === origYear && 49 | gregorianResult.month === origMonth && 50 | gregorianResult.day === origDay 51 | ) { 52 | passedTests++; 53 | } else { 54 | failedTests++; 55 | failures.push({ 56 | original: `${origYear}-${String(origMonth).padStart(2, '0')}-${String(origDay).padStart(2, '0')}`, 57 | khmer: `${khmerDay}${khmerMoonPhase === 0 ? 'កើត' : 'រោច'} month ${khmerMonthIndex} BE ${khmerBeYear}`, 58 | roundTrip: `${gregorianResult.year}-${String(gregorianResult.month).padStart(2, '0')}-${String(gregorianResult.day).padStart(2, '0')}` 59 | }); 60 | } 61 | } catch (error) { 62 | failedTests++; 63 | failures.push({ 64 | original: gregorianStr, 65 | error: error.message 66 | }); 67 | } 68 | 69 | // Progress indicator 70 | if (totalTests % 500 === 0) { 71 | console.log(`Tested ${totalTests}/${Object.keys(conversionData).length} round-trips...`); 72 | } 73 | } 74 | 75 | // Print summary 76 | console.log('\n' + '='.repeat(80)); 77 | console.log('TEST RESULTS SUMMARY'); 78 | console.log('='.repeat(80)); 79 | console.log(`Total Tests: ${totalTests}`); 80 | console.log(`Passed: ${passedTests} (${(passedTests / totalTests * 100).toFixed(2)}%)`); 81 | console.log(`Failed: ${failedTests} (${(failedTests / totalTests * 100).toFixed(2)}%)`); 82 | console.log('='.repeat(80)); 83 | 84 | // Print failures if any 85 | if (failures.length > 0) { 86 | console.log('\nFAILURES:'); 87 | console.log('-'.repeat(80)); 88 | failures.slice(0, 10).forEach((failure, index) => { 89 | console.log(`\n${index + 1}. Original: ${failure.original}`); 90 | if (failure.khmer) { 91 | console.log(` Khmer: ${failure.khmer}`); 92 | console.log(` Round-trip: ${failure.roundTrip}`); 93 | } 94 | if (failure.error) { 95 | console.log(` Error: ${failure.error}`); 96 | } 97 | }); 98 | if (failures.length > 10) { 99 | console.log(`\n... and ${failures.length - 10} more failures`); 100 | } 101 | console.log('\n' + '='.repeat(80)); 102 | process.exit(1); 103 | } else { 104 | console.log('\n✓ ALL TESTS PASSED!\n'); 105 | process.exit(0); 106 | } 107 | -------------------------------------------------------------------------------- /dist/momentkh.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"momentkh.d.ts","sourceRoot":"","sources":["../momentkh.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAQH,oBAAY,SAAS;IACnB,MAAM,IAAI,CAAG,MAAM;IACnB,MAAM,IAAI;CACX;AAED,oBAAY,UAAU;IACpB,OAAO,IAAI,CAAO,SAAS;IAC3B,IAAI,IAAI,CAAW,QAAQ;IAC3B,IAAI,IAAI,CAAU,MAAM;IACxB,OAAO,IAAI,CAAO,SAAS;IAC3B,KAAK,IAAI,CAAS,QAAQ;IAC1B,MAAM,IAAI,CAAQ,QAAQ;IAC1B,KAAK,IAAI,CAAS,QAAQ;IAC1B,KAAK,IAAI,CAAS,QAAQ;IAC1B,IAAI,IAAI,CAAU,UAAU;IAC5B,SAAS,IAAI,CAAK,SAAS;IAC3B,MAAM,KAAK,CAAO,SAAS;IAC3B,MAAM,KAAK,CAAQ,SAAS;IAC5B,WAAW,KAAK,CAAI,UAAU;IAC9B,UAAU,KAAK;CAChB;AAED,oBAAY,UAAU;IACpB,KAAK,IAAI,CAAK,YAAY;IAC1B,KAAK,IAAI,CAAK,aAAa;IAC3B,IAAI,IAAI,CAAM,cAAc;IAC5B,IAAI,IAAI,CAAM,eAAe;IAC7B,IAAI,IAAI,CAAM,eAAe;IAC7B,MAAM,IAAI,CAAI,iBAAiB;IAC/B,KAAK,IAAI,CAAK,cAAc;IAC5B,KAAK,IAAI,CAAK,aAAa;IAC3B,GAAG,IAAI,CAAO,cAAc;IAC5B,IAAI,IAAI,CAAM,gBAAgB;IAC9B,GAAG,KAAK,CAAM,UAAU;IACxB,GAAG,KAAK;CACT;AAED,oBAAY,GAAG;IACb,WAAW,IAAI,CAAK,cAAc;IAClC,MAAM,IAAI,CAAU,QAAQ;IAC5B,KAAK,IAAI,CAAW,QAAQ;IAC5B,OAAO,IAAI,CAAU,UAAU;IAC/B,UAAU,IAAI,CAAO,WAAW;IAChC,SAAS,IAAI,CAAO,UAAU;IAC9B,OAAO,IAAI,CAAS,OAAO;IAC3B,QAAQ,IAAI,CAAQ,UAAU;IAC9B,QAAQ,IAAI,CAAQ,UAAU;IAC9B,QAAQ,IAAI;CACb;AAED,oBAAY,SAAS;IACnB,MAAM,IAAI,CAAM,UAAU;IAC1B,MAAM,IAAI,CAAM,OAAO;IACvB,OAAO,IAAI,CAAK,SAAS;IACzB,SAAS,IAAI,CAAG,MAAM;IACtB,QAAQ,IAAI,CAAI,aAAa;IAC7B,MAAM,IAAI,CAAM,QAAQ;IACxB,QAAQ,IAAI;CACb;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,SAAS,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,UAAU,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,UAAU,CAAC;IACvB,cAAc,EAAE,MAAM,CAAC;IACvB,GAAG,EAAE,GAAG,CAAC;IACT,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,SAAS,CAAC;IACrB,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,KAAK,EAAE,aAAa,CAAC;IACrB,aAAa,EAAE,SAAS,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,OAAO;IACtB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,mBAAmB;IAClC,aAAa,EAAE,QAAQ,CAAC;IACxB,mBAAmB,EAAE,MAAM,CAAC;IAC5B,iBAAiB,EAAE,OAAO,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,eAAe;IAC9B,aAAa,EAAE,IAAI,CAAC;IACpB,cAAc,EAAE,IAAI,CAAC;IACrB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,2BAA2B,EAAE,MAAM,EAAE,CAAC;IACtC,2BAA2B,EAAE,MAAM,EAAE,CAAC;IACtC,eAAe,EAAE,MAAM,EAAE,CAAC;IAC1B,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C;AA6kBD,cAAM,SAAS;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,UAAU,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;gBAEH,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM;IAQrF,YAAY,IAAI,MAAM;IAQtB,MAAM,CAAC,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW;IASjD,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS;IAoCjC,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS;IAmCtC,QAAQ,IAAI,MAAM;CAGnB;AAgcD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE,MAAU,EAAE,MAAM,GAAE,MAAU,EAAE,MAAM,GAAE,MAAU,GAAG,qBAAqB,CAEvJ;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,aAAa,CAEpI;AAGD,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAEtD;AAGD,wBAAgB,MAAM,CAAC,SAAS,EAAE,qBAAqB,EAAE,YAAY,CAAC,EAAE,MAAM,GAAG,MAAM,CAEtF;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,IAAI,GAAG,qBAAqB,CAW1D;AAGD,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAG,MAAM,EAAE,UAAU,EAAE,UAAU,GAAG,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAGxH;AAGD,eAAO,MAAM,SAAS;;;;;;;;;;;CAWrB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;AAGF,wBAaE"} -------------------------------------------------------------------------------- /example/enum-usage.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example: Using MomentKH with Enums 3 | * 4 | * This example demonstrates how to use the new enum features in MomentKH 3.0 5 | */ 6 | 7 | const momentkh = require('@thyrith/momentkh'); 8 | const { MoonPhase, MonthIndex, AnimalYear, Sak, DayOfWeek } = momentkh; 9 | 10 | console.log('='.repeat(80)); 11 | console.log('MOMENTKH 3.0 - ENUM USAGE EXAMPLES'); 12 | console.log('='.repeat(80)); 13 | console.log(); 14 | 15 | // Example 1: Convert Gregorian to Khmer 16 | console.log('1. GREGORIAN TO KHMER CONVERSION'); 17 | console.log('-'.repeat(80)); 18 | const result = momentkh.fromGregorian(2024, 12, 16); 19 | console.log('Input: December 16, 2024'); 20 | console.log(); 21 | console.log('Output:'); 22 | console.log(' Day:', result.khmer.day); 23 | console.log(' Moon Phase (enum):', result.khmer.moonPhase, '→', result.khmer.moonPhaseName); 24 | console.log(' Month Index (enum):', result.khmer.monthIndex, '→', result.khmer.monthName); 25 | console.log(' Animal Year (enum):', result.khmer.animalYear, '→', result.khmer.animalYearName); 26 | console.log(' Sak (enum):', result.khmer.sak, '→', result.khmer.sakName); 27 | console.log(' Day of Week (enum):', result.khmer.dayOfWeek, '→', result.khmer.dayOfWeekName); 28 | console.log(' BE Year:', result.khmer.beYear); 29 | console.log(); 30 | 31 | // Example 2: Compare using enums 32 | console.log('2. USING ENUMS FOR COMPARISON'); 33 | console.log('-'.repeat(80)); 34 | if (result.khmer.moonPhase === MoonPhase.Waxing) { 35 | console.log('✓ It is Waxing Moon (កើត)'); 36 | } else if (result.khmer.moonPhase === MoonPhase.Waning) { 37 | console.log('✓ It is Waning Moon (រោច)'); 38 | } 39 | 40 | if (result.khmer.monthIndex === MonthIndex.Migasir) { 41 | console.log('✓ It is Migasir month (មិគសិរ)'); 42 | } else { 43 | console.log(` Current month: ${result.khmer.monthName}`); 44 | } 45 | 46 | if (result.khmer.dayOfWeek === DayOfWeek.Monday) { 47 | console.log('✓ It is Monday (ចន្ទ)'); 48 | } else { 49 | console.log(` Day of week: ${result.khmer.dayOfWeekName}`); 50 | } 51 | console.log(); 52 | 53 | // Example 3: Convert Khmer to Gregorian using enums 54 | console.log('3. KHMER TO GREGORIAN CONVERSION (USING ENUMS)'); 55 | console.log('-'.repeat(80)); 56 | const gregorianDate = momentkh.fromKhmer( 57 | 15, // day 58 | MoonPhase.Waxing, // moon phase (using enum) 59 | MonthIndex.Pisakh, // month index (using enum) 60 | 2568 // BE year 61 | ); 62 | console.log('Input: 15កើត ខែពិសាខ ព.ស.2568'); 63 | console.log('Output:', `${gregorianDate.year}-${gregorianDate.month}-${gregorianDate.day}`); 64 | console.log(); 65 | 66 | // Example 4: Convert Khmer to Gregorian using numbers (backward compatible) 67 | console.log('4. KHMER TO GREGORIAN CONVERSION (USING NUMBERS - BACKWARD COMPATIBLE)'); 68 | console.log('-'.repeat(80)); 69 | const gregorianDate2 = momentkh.fromKhmer( 70 | 15, // day 71 | 0, // moon phase (0 = Waxing) 72 | 5, // month index (5 = Pisakh) 73 | 2568 // BE year 74 | ); 75 | console.log('Input: 15កើត ខែពិសាខ ព.ស.2568'); 76 | console.log('Output:', `${gregorianDate2.year}-${gregorianDate2.month}-${gregorianDate2.day}`); 77 | console.log(); 78 | 79 | // Example 5: All available enums 80 | console.log('5. AVAILABLE ENUM VALUES'); 81 | console.log('-'.repeat(80)); 82 | console.log(); 83 | console.log('MoonPhase:'); 84 | console.log(' Waxing:', MoonPhase.Waxing, '(កើត)'); 85 | console.log(' Waning:', MoonPhase.Waning, '(រោច)'); 86 | console.log(); 87 | 88 | console.log('MonthIndex (selected):'); 89 | console.log(' Migasir:', MonthIndex.Migasir, '(មិគសិរ)'); 90 | console.log(' Pisakh:', MonthIndex.Pisakh, '(ពិសាខ)'); 91 | console.log(' Jesth:', MonthIndex.Jesth, '(ជេស្ឋ)'); 92 | console.log(' Asadh:', MonthIndex.Asadh, '(អាសាឍ)'); 93 | console.log(); 94 | 95 | console.log('AnimalYear (selected):'); 96 | console.log(' Chhut (Rat):', AnimalYear.Chhut); 97 | console.log(' Chlov (Ox):', AnimalYear.Chlov); 98 | console.log(' Khal (Tiger):', AnimalYear.Khal); 99 | console.log(' Rong (Dragon):', AnimalYear.Rong); 100 | console.log(); 101 | 102 | console.log('DayOfWeek:'); 103 | console.log(' Sunday:', DayOfWeek.Sunday, '(អាទិត្យ)'); 104 | console.log(' Monday:', DayOfWeek.Monday, '(ចន្ទ)'); 105 | console.log(' Tuesday:', DayOfWeek.Tuesday, '(អង្គារ)'); 106 | console.log(' Wednesday:', DayOfWeek.Wednesday, '(ពុធ)'); 107 | console.log(); 108 | 109 | console.log('='.repeat(80)); 110 | console.log('✓ All examples completed successfully!'); 111 | console.log('='.repeat(80)); 112 | -------------------------------------------------------------------------------- /example/roundTrip.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Example 3: Convert today's Khmer date to Gregorian date (Round-trip conversion) 3 | * 4 | * This example demonstrates bidirectional conversion: 5 | * Gregorian → Khmer → Gregorian (and verify they match) 6 | */ 7 | 8 | const momentkh = require('@thyrith/momentkh'); 9 | 10 | // Step 1: Get today's Gregorian date 11 | const today = new Date(); 12 | const gregorianDate = { 13 | year: today.getFullYear(), 14 | month: today.getMonth() + 1, 15 | day: today.getDate(), 16 | hour: today.getHours(), 17 | minute: today.getMinutes(), 18 | second: today.getSeconds() 19 | }; 20 | 21 | console.log('='.repeat(80)); 22 | console.log('Example 3: Round-Trip Conversion (Gregorian → Khmer → Gregorian)'); 23 | console.log('='.repeat(80)); 24 | console.log(); 25 | 26 | // Step 2: Convert Gregorian to Khmer 27 | console.log('STEP 1: Gregorian → Khmer'); 28 | console.log('-'.repeat(80)); 29 | console.log('Original Gregorian Date:'); 30 | console.log(` ${gregorianDate.year}-${String(gregorianDate.month).padStart(2, '0')}-${String(gregorianDate.day).padStart(2, '0')} ` + 31 | `${String(gregorianDate.hour).padStart(2, '0')}:${String(gregorianDate.minute).padStart(2, '0')}:${String(gregorianDate.second).padStart(2, '0')}`); 32 | console.log(); 33 | 34 | const khmer = momentkh.fromGregorian( 35 | gregorianDate.year, 36 | gregorianDate.month, 37 | gregorianDate.day, 38 | gregorianDate.hour, 39 | gregorianDate.minute, 40 | gregorianDate.second 41 | ); 42 | 43 | console.log('Converted to Khmer:'); 44 | console.log(` Formatted: ${momentkh.format(khmer)}`); 45 | console.log(` Day: ${khmer.khmer.day}${khmer.khmer.moonPhase === 0 ? 'កើត' : 'រោច'}`); 46 | console.log(` Month: ${khmer.khmer.monthName} (index: ${khmer.khmer.monthIndex})`); 47 | console.log(` BE Year: ${khmer.khmer.beYear}`); 48 | console.log(); 49 | 50 | // Step 3: Convert Khmer back to Gregorian 51 | console.log('STEP 2: Khmer → Gregorian'); 52 | console.log('-'.repeat(80)); 53 | console.log('Converting back to Gregorian...'); 54 | console.log(); 55 | 56 | const backToGregorian = momentkh.fromKhmer( 57 | khmer.khmer.day, 58 | khmer.khmer.moonPhase, 59 | khmer.khmer.monthIndex, 60 | khmer.khmer.beYear 61 | ); 62 | 63 | console.log('Converted back to Gregorian:'); 64 | console.log(` ${backToGregorian.year}-${String(backToGregorian.month).padStart(2, '0')}-${String(backToGregorian.day).padStart(2, '0')}`); 65 | console.log(); 66 | 67 | // Step 4: Verify they match 68 | console.log('STEP 3: Verification'); 69 | console.log('-'.repeat(80)); 70 | 71 | const isMatch = 72 | backToGregorian.year === gregorianDate.year && 73 | backToGregorian.month === gregorianDate.month && 74 | backToGregorian.day === gregorianDate.day; 75 | 76 | console.log('Comparison:'); 77 | console.log(` Original: ${gregorianDate.year}-${String(gregorianDate.month).padStart(2, '0')}-${String(gregorianDate.day).padStart(2, '0')}`); 78 | console.log(` Converted: ${backToGregorian.year}-${String(backToGregorian.month).padStart(2, '0')}-${String(backToGregorian.day).padStart(2, '0')}`); 79 | console.log(); 80 | 81 | if (isMatch) { 82 | console.log('✓ SUCCESS! Round-trip conversion is accurate.'); 83 | console.log(' The dates match perfectly!'); 84 | } else { 85 | console.log('✗ ERROR! Round-trip conversion failed.'); 86 | console.log(' The dates do not match.'); 87 | } 88 | console.log(); 89 | 90 | // Additional examples with different dates 91 | console.log('ADDITIONAL EXAMPLES:'); 92 | console.log('-'.repeat(80)); 93 | 94 | const testDates = [ 95 | { year: 2024, month: 4, day: 14 }, // Khmer New Year 96 | { year: 2024, month: 5, day: 22 }, // Pisakha Bochea 97 | { year: 2024, month: 1, day: 1 }, // New Year's Day 98 | ]; 99 | 100 | testDates.forEach((date, index) => { 101 | const khmer = momentkh.fromGregorian(date.year, date.month, date.day); 102 | const back = momentkh.fromKhmer(khmer.khmer.day, khmer.khmer.moonPhase, khmer.khmer.monthIndex, khmer.khmer.beYear); 103 | const match = back.year === date.year && back.month === date.month && back.day === date.day; 104 | 105 | console.log(`Test ${index + 1}: ${date.year}-${String(date.month).padStart(2, '0')}-${String(date.day).padStart(2, '0')}`); 106 | console.log(` Khmer: ${momentkh.format(khmer, 'dN ខែm ព.ស. b')}`); 107 | console.log(` Back: ${back.year}-${String(back.month).padStart(2, '0')}-${String(back.day).padStart(2, '0')}`); 108 | console.log(` Result: ${match ? '✓ Match' : '✗ Mismatch'}`); 109 | console.log(); 110 | }); 111 | 112 | console.log('='.repeat(80)); 113 | -------------------------------------------------------------------------------- /test/escape-format.test.js: -------------------------------------------------------------------------------- 1 | 2 | const { format, toDate } = require('../dist/momentkh'); 3 | const assert = require('assert'); 4 | 5 | // 2023-04-14 is Khmer New Year 2023 6 | const date = toDate(14, 1, 5, 2567); // 14th day, Waxing (1), Pisakh (5), 2567 BE 7 | 8 | // Create a mock object that mimics what momentkh returns for internal functions if needed, 9 | // but we are testing public API 'format', so we need a valid KhmerConversionResult or similar if we were calling internal. 10 | // However, the 'format' function usually takes the result of 'fromGregorian' or similar. 11 | // Let's use 'fromGregorian' to get a valid object. 12 | 13 | const { fromGregorian } = require('../dist/momentkh'); 14 | const khmerDate = fromGregorian(2023, 4, 14); 15 | // 14th April 2023 16 | // Gregorian: 2023-04-14 17 | // Khmer: Likely around New Year. 18 | // Just ensuring we have valid data to format. 19 | 20 | console.log('Testing Escape Support in Format...'); 21 | 22 | try { 23 | // Test Case 1: Simple text (existing behavior check - no escape chars) 24 | // 'd' is day. 25 | const result1 = format(khmerDate, 'd'); 26 | console.log(`Format 'd': ${result1}`); 27 | // Should be Khmer numeral for day. 28 | 29 | // Test Case 2: Escaped text [d] 30 | // Expected: "d" (literal 'd', not replaced by day number) 31 | // Current behavior (before fix): likely replaces 'd' with day number or keeps brackets if not matched? 32 | // Actually currently it matches 'd' inside the string and replaces it because regex is global OR it might not match brackets. 33 | // The current regex is just `Object.keys(formatRules).join('|')`. 34 | // So '[d]' becomes '[]'. 35 | 36 | const result2 = format(khmerDate, '[d]'); 37 | console.log(`Format '[d]': ${result2}`); 38 | 39 | if (result2 === 'd') { 40 | console.log('SUCCESS: [d] formatted as literal "d"'); 41 | } else { 42 | console.log(`FAILURE: [d] formatted as "${result2}", expected "d"`); 43 | } 44 | 45 | // Test Case 3: Escaped text with non-tokens [Hello] 46 | const result3 = format(khmerDate, '[Hello]'); 47 | console.log(`Format '[Hello]': ${result3}`); 48 | 49 | if (result3 === 'Hello') { 50 | console.log('SUCCESS: [Hello] formatted as literal "Hello"'); 51 | } else { 52 | // Current behavior will likely be "[Hello]" because H, e, l, o are not tokens (except maybe 'e', 'o' if they are tokens?) 53 | // Check tokens: W w d D n N o m M a e b c j 54 | // 'e' is sakName. 'o' is MoonDaySymbols. 55 | // So 'Hello' -> 'H' + sakName + 'll' + symbol + brackets? 56 | console.log(`FAILURE: [Hello] formatted as "${result3}", expected "Hello"`); 57 | } 58 | 59 | // Test Case 4: Escaped numbers [123] 60 | // Should not be converted to Khmer numerals if escaped (this is a stricter requirement, maybe not explicitly requested but implied by "escape") 61 | // Wait, `formatKhmer` applies `toKhmerNumeral` to the result of replacements. 62 | // If we escape `[123]`, it should probably return `123` in Western numerals, or `១២៣`? 63 | // Moment.js `[text]` outputs `text` without any processing. 64 | // So `[123]` should be `123`. 65 | const result4 = format(khmerDate, '[123]'); 66 | console.log(`Format '[123]': ${result4}`); 67 | if (result4 === '123') { 68 | console.log('SUCCESS: [123] formatted as literal "123"'); 69 | } else { 70 | console.log(`FAILURE: [123] formatted as "${result4}", expected "123"`); 71 | } 72 | 73 | 74 | 75 | // Test Case 5: Escaped brackets [[Day]]: 76 | // Expected: "[Day]: " (The outer brackets escape the inner string "[Day]") 77 | const result5 = format(khmerDate, '[[Day]]: '); 78 | console.log(`Format '[[Day]]: ': ${result5}`); 79 | if (result5 === '[Day]: ') { 80 | console.log('SUCCESS: [[Day]]: formatted as literal "[Day]: "'); 81 | } else { 82 | console.log(`FAILURE: [[Day]]: formatted as "${result5}", expected "[Day]: "`); 83 | } 84 | 85 | // Test Case 6: M[s] (Verify distinction between Ms token and M + escaped [s]) 86 | // M = Solar Month Name (e.g., April -> មេសា) 87 | // [s] = Literal 's' 88 | // Result should be "មេសាs" (assuming date in April) 89 | // If it wrongly matched 'Ms' (Solar Month Abbreviation), it would be "មេសា" or "មស" (depending on abbrev). 90 | 91 | const result6 = format(khmerDate, 'M[s]'); 92 | console.log(`Format 'M[s]': ${result6}`); 93 | // April in Khmer is មេសា (Mesa) 94 | // So we expect "មេសាs" 95 | if (result6.includes('s') && !result6.includes('[s]')) { 96 | console.log('SUCCESS: M[s] formatted correctly containing literal "s"'); 97 | } else { 98 | console.log(`FAILURE: M[s] formatted as "${result6}"`); 99 | } 100 | 101 | } catch (e) { 102 | console.error('Error running test:', e); 103 | } 104 | -------------------------------------------------------------------------------- /dist/momentkh.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MomentKH - Standalone Khmer Calendar Library (TypeScript) 3 | * 4 | * A simplified, standalone library for Khmer calendar conversion 5 | * No dependencies required 6 | * 7 | * Based on: 8 | * - khmer_calendar.cpp implementation 9 | * - Original momentkh library 10 | * 11 | * @version 2.0.0 12 | * @license MIT 13 | */ 14 | export declare enum MoonPhase { 15 | Waxing = 0,// កើត 16 | Waning = 1 17 | } 18 | export declare enum MonthIndex { 19 | Migasir = 0,// មិគសិរ 20 | Boss = 1,// បុស្ស 21 | Meak = 2,// មាឃ 22 | Phalkun = 3,// ផល្គុន 23 | Cheit = 4,// ចេត្រ 24 | Pisakh = 5,// ពិសាខ 25 | Jesth = 6,// ជេស្ឋ 26 | Asadh = 7,// អាសាឍ 27 | Srap = 8,// ស្រាពណ៍ 28 | Phatrabot = 9,// ភទ្របទ 29 | Assoch = 10,// អស្សុជ 30 | Kadeuk = 11,// កត្ដិក 31 | Pathamasadh = 12,// បឋមាសាឍ 32 | Tutiyasadh = 13 33 | } 34 | export declare enum AnimalYear { 35 | Chhut = 0,// ជូត - Rat 36 | Chlov = 1,// ឆ្លូវ - Ox 37 | Khal = 2,// ខាល - Tiger 38 | Thos = 3,// ថោះ - Rabbit 39 | Rong = 4,// រោង - Dragon 40 | Masagn = 5,// ម្សាញ់ - Snake 41 | Momee = 6,// មមី - Horse 42 | Momae = 7,// មមែ - Goat 43 | Vok = 8,// វក - Monkey 44 | Roka = 9,// រកា - Rooster 45 | Cho = 10,// ច - Dog 46 | Kor = 11 47 | } 48 | export declare enum Sak { 49 | SamridhiSak = 0,// សំរឹទ្ធិស័ក 50 | AekSak = 1,// ឯកស័ក 51 | ToSak = 2,// ទោស័ក 52 | TreiSak = 3,// ត្រីស័ក 53 | ChattvaSak = 4,// ចត្វាស័ក 54 | PanchaSak = 5,// បញ្ចស័ក 55 | ChhaSak = 6,// ឆស័ក 56 | SappaSak = 7,// សប្តស័ក 57 | AtthaSak = 8,// អដ្ឋស័ក 58 | NappaSak = 9 59 | } 60 | export declare enum DayOfWeek { 61 | Sunday = 0,// អាទិត្យ 62 | Monday = 1,// ចន្ទ 63 | Tuesday = 2,// អង្គារ 64 | Wednesday = 3,// ពុធ 65 | Thursday = 4,// ព្រហស្បតិ៍ 66 | Friday = 5,// សុក្រ 67 | Saturday = 6 68 | } 69 | export interface GregorianDate { 70 | year: number; 71 | month: number; 72 | day: number; 73 | hour?: number; 74 | minute?: number; 75 | second?: number; 76 | dayOfWeek?: number; 77 | } 78 | export interface KhmerDateInfo { 79 | day: number; 80 | moonPhase: MoonPhase; 81 | moonPhaseName: string; 82 | monthIndex: MonthIndex; 83 | monthName: string; 84 | beYear: number; 85 | jsYear: number; 86 | animalYear: AnimalYear; 87 | animalYearName: string; 88 | sak: Sak; 89 | sakName: string; 90 | dayOfWeek: DayOfWeek; 91 | dayOfWeekName: string; 92 | } 93 | export interface KhmerConversionResult { 94 | gregorian: { 95 | year: number; 96 | month: number; 97 | day: number; 98 | hour: number; 99 | minute: number; 100 | second: number; 101 | dayOfWeek: number; 102 | }; 103 | khmer: KhmerDateInfo; 104 | _khmerDateObj: KhmerDate; 105 | } 106 | export interface NewYearInfo { 107 | year: number; 108 | month: number; 109 | day: number; 110 | hour: number; 111 | minute: number; 112 | } 113 | export interface TimeInfo { 114 | hour: number; 115 | minute: number; 116 | } 117 | export interface SunInfo { 118 | sunInaugurationAsLibda: number; 119 | reasey: number; 120 | angsar: number; 121 | libda: number; 122 | } 123 | export interface NewYearInfoInternal { 124 | timeOfNewYear: TimeInfo; 125 | numberOfVanabatDays: number; 126 | newYearsDaySotins: SunInfo[]; 127 | } 128 | export interface NewYearFullInfo { 129 | newYearMoment: Date; 130 | lerngSakMoment: Date; 131 | newYearInfo: NewYearInfo; 132 | } 133 | export interface MoonDayInfo { 134 | day: number; 135 | moonPhase: MoonPhase; 136 | } 137 | export interface Constants { 138 | LunarMonths: Record; 139 | LunarMonthNames: string[]; 140 | SolarMonthNames: string[]; 141 | SolarMonthAbbreviationNames: string[]; 142 | LunarMonthAbbreviationNames: string[]; 143 | AnimalYearNames: string[]; 144 | AnimalYearEmojis: string[]; 145 | SakNames: string[]; 146 | WeekdayNames: string[]; 147 | WeekdayNamesShort: string[]; 148 | MoonPhaseNames: string[]; 149 | MoonPhaseShort: string[]; 150 | MoonDaySymbols: string[]; 151 | KhmerNumerals: Record; 152 | khNewYearMoments: Record; 153 | } 154 | declare class KhmerDate { 155 | day: number; 156 | moonPhase: MoonPhase; 157 | monthIndex: MonthIndex; 158 | beYear: number; 159 | constructor(day: number, moonPhase: MoonPhase, monthIndex: MonthIndex, beYear: number); 160 | getDayNumber(): number; 161 | static fromDayNumber(dayNum: number): MoonDayInfo; 162 | addDays(count: number): KhmerDate; 163 | subtractDays(count: number): KhmerDate; 164 | toString(): string; 165 | } 166 | export declare function fromGregorian(year: number, month: number, day: number, hour?: number, minute?: number, second?: number): KhmerConversionResult; 167 | export declare function fromKhmer(day: number, moonPhase: MoonPhase | number, monthIndex: MonthIndex | number, beYear: number): GregorianDate; 168 | export declare function getNewYear(ceYear: number): NewYearInfo; 169 | export declare function format(khmerData: KhmerConversionResult, formatString?: string): string; 170 | export declare function fromDate(date: Date): KhmerConversionResult; 171 | export declare function toDate(day: number, moonPhase: MoonPhase | number, monthIndex: MonthIndex | number, beYear: number): Date; 172 | export declare const constants: { 173 | LunarMonths: Record; 174 | LunarMonthNames: string[]; 175 | SolarMonthNames: string[]; 176 | SolarMonthAbbreviationNames: string[]; 177 | LunarMonthAbbreviationNames: string[]; 178 | AnimalYearNames: string[]; 179 | AnimalYearEmojis: string[]; 180 | SakNames: string[]; 181 | WeekdayNames: string[]; 182 | MoonPhaseNames: string[]; 183 | }; 184 | declare const _default: { 185 | fromGregorian: typeof fromGregorian; 186 | fromKhmer: typeof fromKhmer; 187 | getNewYear: typeof getNewYear; 188 | format: typeof format; 189 | fromDate: typeof fromDate; 190 | toDate: typeof toDate; 191 | constants: { 192 | LunarMonths: Record; 193 | LunarMonthNames: string[]; 194 | SolarMonthNames: string[]; 195 | SolarMonthAbbreviationNames: string[]; 196 | LunarMonthAbbreviationNames: string[]; 197 | AnimalYearNames: string[]; 198 | AnimalYearEmojis: string[]; 199 | SakNames: string[]; 200 | WeekdayNames: string[]; 201 | MoonPhaseNames: string[]; 202 | }; 203 | MoonPhase: typeof MoonPhase; 204 | MonthIndex: typeof MonthIndex; 205 | AnimalYear: typeof AnimalYear; 206 | Sak: typeof Sak; 207 | DayOfWeek: typeof DayOfWeek; 208 | }; 209 | export default _default; 210 | //# sourceMappingURL=momentkh.d.ts.map -------------------------------------------------------------------------------- /test/validation.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Suite: Input Validation 3 | * 4 | * This test validates that the library properly rejects invalid inputs 5 | * for both Gregorian and Khmer date conversions. 6 | */ 7 | 8 | const momentkh = require('../momentkh'); 9 | 10 | console.log('='.repeat(80)); 11 | console.log('TEST SUITE: INPUT VALIDATION'); 12 | console.log('='.repeat(80)); 13 | console.log(); 14 | 15 | let totalTests = 0; 16 | let passedTests = 0; 17 | let failedTests = 0; 18 | const failures = []; 19 | 20 | /** 21 | * Helper function to test if a function throws an error 22 | */ 23 | function expectError(testName, testFn, expectedErrorFragment) { 24 | totalTests++; 25 | try { 26 | testFn(); 27 | // If we get here, the function didn't throw 28 | failedTests++; 29 | failures.push({ 30 | test: testName, 31 | error: 'Expected error but none was thrown', 32 | expectedFragment: expectedErrorFragment 33 | }); 34 | } catch (error) { 35 | // Check if error message contains expected fragment 36 | if (error.message.includes(expectedErrorFragment)) { 37 | passedTests++; 38 | } else { 39 | failedTests++; 40 | failures.push({ 41 | test: testName, 42 | error: `Error message doesn't match expected fragment`, 43 | expectedFragment: expectedErrorFragment, 44 | actualMessage: error.message 45 | }); 46 | } 47 | } 48 | } 49 | 50 | // ============================================================================ 51 | // Gregorian Date Validation Tests 52 | // ============================================================================ 53 | 54 | console.log('GREGORIAN DATE VALIDATION'); 55 | console.log('-'.repeat(80)); 56 | 57 | // Invalid month tests 58 | expectError( 59 | 'Invalid month: 0', 60 | () => momentkh.fromGregorian(2024, 0, 15), 61 | 'Invalid month: 0' 62 | ); 63 | 64 | expectError( 65 | 'Invalid month: 13', 66 | () => momentkh.fromGregorian(2024, 13, 15), 67 | 'Invalid month: 13' 68 | ); 69 | 70 | expectError( 71 | 'Invalid month: -1', 72 | () => momentkh.fromGregorian(2024, -1, 15), 73 | 'Invalid month: -1' 74 | ); 75 | 76 | // Invalid day tests 77 | expectError( 78 | 'Invalid day: February 30', 79 | () => momentkh.fromGregorian(2023, 2, 30), 80 | 'February 2023 has 28 days' 81 | ); 82 | 83 | expectError( 84 | 'Invalid day: February 29 (non-leap year)', 85 | () => momentkh.fromGregorian(2023, 2, 29), 86 | 'February 2023 has 28 days' 87 | ); 88 | 89 | expectError( 90 | 'Invalid day: April 31', 91 | () => momentkh.fromGregorian(2024, 4, 31), 92 | 'April 2024 has 30 days' 93 | ); 94 | 95 | expectError( 96 | 'Invalid day: 0', 97 | () => momentkh.fromGregorian(2024, 1, 0), 98 | 'Invalid day: 0' 99 | ); 100 | 101 | expectError( 102 | 'Invalid day: 32', 103 | () => momentkh.fromGregorian(2024, 1, 32), 104 | 'January 2024 has 31 days' 105 | ); 106 | 107 | // Invalid hour tests 108 | expectError( 109 | 'Invalid hour: 24', 110 | () => momentkh.fromGregorian(2024, 1, 15, 24), 111 | 'Invalid hour: 24' 112 | ); 113 | 114 | expectError( 115 | 'Invalid hour: 25', 116 | () => momentkh.fromGregorian(2024, 1, 15, 25), 117 | 'Invalid hour: 25' 118 | ); 119 | 120 | expectError( 121 | 'Invalid hour: -1', 122 | () => momentkh.fromGregorian(2024, 1, 15, -1), 123 | 'Invalid hour: -1' 124 | ); 125 | 126 | // Invalid minute tests 127 | expectError( 128 | 'Invalid minute: 60', 129 | () => momentkh.fromGregorian(2024, 1, 15, 12, 60), 130 | 'Invalid minute: 60' 131 | ); 132 | 133 | expectError( 134 | 'Invalid minute: -1', 135 | () => momentkh.fromGregorian(2024, 1, 15, 12, -1), 136 | 'Invalid minute: -1' 137 | ); 138 | 139 | // Invalid second tests 140 | expectError( 141 | 'Invalid second: 60', 142 | () => momentkh.fromGregorian(2024, 1, 15, 12, 30, 60), 143 | 'Invalid second: 60' 144 | ); 145 | 146 | expectError( 147 | 'Invalid second: -1', 148 | () => momentkh.fromGregorian(2024, 1, 15, 12, 30, -1), 149 | 'Invalid second: -1' 150 | ); 151 | 152 | console.log(`Gregorian validation tests: ${passedTests}/${totalTests} passed\n`); 153 | 154 | // ============================================================================ 155 | // Date Object Validation Tests 156 | // ============================================================================ 157 | 158 | console.log('DATE OBJECT VALIDATION'); 159 | console.log('-'.repeat(80)); 160 | 161 | expectError( 162 | 'Invalid Date object', 163 | () => momentkh.fromDate(new Date('invalid')), 164 | 'Invalid Date object' 165 | ); 166 | 167 | expectError( 168 | 'Not a Date object (null)', 169 | () => momentkh.fromDate(null), 170 | 'Expected a Date object' 171 | ); 172 | 173 | expectError( 174 | 'Not a Date object (string)', 175 | () => momentkh.fromDate('2024-01-15'), 176 | 'Expected a Date object' 177 | ); 178 | 179 | console.log(`Date object validation tests: ${passedTests}/${totalTests} passed\n`); 180 | 181 | // ============================================================================ 182 | // Khmer Date Validation Tests 183 | // ============================================================================ 184 | 185 | console.log('KHMER DATE VALIDATION'); 186 | console.log('-'.repeat(80)); 187 | 188 | // Invalid day tests 189 | expectError( 190 | 'Invalid day: 0', 191 | () => momentkh.fromKhmer(0, 0, 5, 2568), 192 | 'Invalid day: 0' 193 | ); 194 | 195 | expectError( 196 | 'Invalid day: 16', 197 | () => momentkh.fromKhmer(16, 0, 5, 2568), 198 | 'Invalid day: 16' 199 | ); 200 | 201 | expectError( 202 | 'Invalid day: -1', 203 | () => momentkh.fromKhmer(-1, 0, 5, 2568), 204 | 'Invalid day: -1' 205 | ); 206 | 207 | // Invalid moonPhase tests 208 | expectError( 209 | 'Invalid moonPhase: 2', 210 | () => momentkh.fromKhmer(15, 2, 5, 2568), 211 | 'Invalid moonPhase: 2' 212 | ); 213 | 214 | expectError( 215 | 'Invalid moonPhase: -1', 216 | () => momentkh.fromKhmer(15, -1, 5, 2568), 217 | 'Invalid moonPhase: -1' 218 | ); 219 | 220 | expectError( 221 | 'Invalid moonPhase: 3', 222 | () => momentkh.fromKhmer(15, 3, 5, 2568), 223 | 'Invalid moonPhase: 3' 224 | ); 225 | 226 | // Invalid monthIndex tests 227 | expectError( 228 | 'Invalid monthIndex: -1', 229 | () => momentkh.fromKhmer(15, 0, -1, 2568), 230 | 'Invalid monthIndex: -1' 231 | ); 232 | 233 | expectError( 234 | 'Invalid monthIndex: 14', 235 | () => momentkh.fromKhmer(15, 0, 14, 2568), 236 | 'Invalid monthIndex: 14' 237 | ); 238 | 239 | // Invalid beYear tests 240 | expectError( 241 | 'Invalid beYear: 1999', 242 | () => momentkh.fromKhmer(15, 0, 5, 1999), 243 | 'Invalid beYear: 1999' 244 | ); 245 | 246 | expectError( 247 | 'Invalid beYear: 3001', 248 | () => momentkh.fromKhmer(15, 0, 5, 3001), 249 | 'Invalid beYear: 3001' 250 | ); 251 | 252 | console.log(`Khmer validation tests: ${passedTests}/${totalTests} passed\n`); 253 | 254 | // ============================================================================ 255 | // Valid Input Tests (should NOT throw errors) 256 | // ============================================================================ 257 | 258 | console.log('VALID INPUT TESTS (should not throw)'); 259 | console.log('-'.repeat(80)); 260 | 261 | // Test some valid inputs to ensure validation doesn't break normal usage 262 | const validTests = [ 263 | { name: 'Valid Gregorian date', fn: () => momentkh.fromGregorian(2024, 2, 29) }, // Leap year 264 | { name: 'Valid Gregorian date with time', fn: () => momentkh.fromGregorian(2024, 12, 16, 23, 59, 59) }, 265 | { name: 'Valid Date object', fn: () => momentkh.fromDate(new Date()) }, 266 | { name: 'Valid Khmer date', fn: () => momentkh.fromKhmer(15, 0, 5, 2568) }, 267 | { name: 'Valid Khmer date (waning)', fn: () => momentkh.fromKhmer(14, 1, 11, 2568) }, 268 | { name: 'Valid toDate', fn: () => momentkh.toDate(15, 0, 5, 2568) } 269 | ]; 270 | 271 | for (const test of validTests) { 272 | totalTests++; 273 | try { 274 | test.fn(); 275 | passedTests++; 276 | } catch (error) { 277 | failedTests++; 278 | failures.push({ 279 | test: test.name, 280 | error: `Valid input threw error: ${error.message}` 281 | }); 282 | } 283 | } 284 | 285 | console.log(`Valid input tests: ${passedTests - (totalTests - validTests.length)}/${validTests.length} passed\n`); 286 | 287 | // ============================================================================ 288 | // Results Summary 289 | // ============================================================================ 290 | 291 | console.log('='.repeat(80)); 292 | console.log('TEST RESULTS SUMMARY'); 293 | console.log('='.repeat(80)); 294 | console.log(`Total Tests: ${totalTests}`); 295 | console.log(`Passed: ${passedTests} (${(passedTests / totalTests * 100).toFixed(2)}%)`); 296 | console.log(`Failed: ${failedTests} (${(failedTests / totalTests * 100).toFixed(2)}%)`); 297 | console.log('='.repeat(80)); 298 | 299 | // Print failures if any 300 | if (failures.length > 0) { 301 | console.log('\nFAILURES:'); 302 | console.log('-'.repeat(80)); 303 | failures.forEach((failure, index) => { 304 | console.log(`\n${index + 1}. ${failure.test}`); 305 | console.log(` Error: ${failure.error}`); 306 | if (failure.expectedFragment) { 307 | console.log(` Expected fragment: "${failure.expectedFragment}"`); 308 | } 309 | if (failure.actualMessage) { 310 | console.log(` Actual message: "${failure.actualMessage}"`); 311 | } 312 | }); 313 | console.log('\n' + '='.repeat(80)); 314 | process.exit(1); 315 | } else { 316 | console.log('\n✓ ALL TESTS PASSED!\n'); 317 | process.exit(0); 318 | } 319 | -------------------------------------------------------------------------------- /test/gregorian-to-khmer.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Suite: Gregorian to Khmer Conversion 3 | * 4 | * This test validates the conversion from Gregorian calendar dates to Khmer calendar dates. 5 | * It runs 500 test cases covering various date ranges and edge cases. 6 | */ 7 | 8 | const momentkh = require('../momentkh'); 9 | 10 | // Helper function to generate random date 11 | function randomDate(startYear = 1800, endYear = 2300) { 12 | const year = Math.floor(Math.random() * (endYear - startYear + 1)) + startYear; 13 | const month = Math.floor(Math.random() * 12) + 1; 14 | const day = Math.floor(Math.random() * 28) + 1; // Use 28 to avoid invalid dates 15 | const hour = Math.floor(Math.random() * 24); 16 | const minute = Math.floor(Math.random() * 60); 17 | const second = Math.floor(Math.random() * 60); 18 | return { year, month, day, hour, minute, second }; 19 | } 20 | 21 | // Helper function to validate Khmer date 22 | function isValidKhmerDate(khmerData) { 23 | if (!khmerData || !khmerData.khmer) return false; 24 | 25 | const { day, moonPhase, monthIndex, beYear } = khmerData.khmer; 26 | 27 | // Validate day (1-15) 28 | if (day < 1 || day > 15) return false; 29 | 30 | // Validate moon phase (0 = កើត, 1 = រោច) 31 | if (moonPhase !== 0 && moonPhase !== 1) return false; 32 | 33 | // Validate month index (0-13) 34 | if (monthIndex < 0 || monthIndex > 13) return false; 35 | 36 | // Validate BE year (reasonable range) 37 | if (beYear < 2000 || beYear > 3000) return false; 38 | 39 | return true; 40 | } 41 | 42 | // Helper function to format date for output 43 | function formatGregorianDate(date) { 44 | return `${date.year}-${String(date.month).padStart(2, '0')}-${String(date.day).padStart(2, '0')} ${String(date.hour).padStart(2, '0')}:${String(date.minute).padStart(2, '0')}:${String(date.second).padStart(2, '0')}`; 45 | } 46 | 47 | // Test categories 48 | const testCategories = [ 49 | { name: 'Random dates', count: 400 }, 50 | { name: 'Edge cases - Pisakha Bochea', count: 50 }, 51 | { name: 'Edge cases - New Year period', count: 30 }, 52 | { name: 'Leap year dates', count: 20 } 53 | ]; 54 | 55 | console.log('='.repeat(80)); 56 | console.log('TEST SUITE: GREGORIAN TO KHMER CONVERSION'); 57 | console.log('='.repeat(80)); 58 | console.log(`Total test cases: 500\n`); 59 | 60 | let totalTests = 0; 61 | let passedTests = 0; 62 | let failedTests = 0; 63 | const failures = []; 64 | 65 | // Category 1: Random dates (400 tests) 66 | console.log(`\n[1/${testCategories.length}] ${testCategories[0].name} (${testCategories[0].count} tests)...`); 67 | for (let i = 0; i < testCategories[0].count; i++) { 68 | totalTests++; 69 | const date = randomDate(1800, 2300); 70 | 71 | try { 72 | const result = momentkh.fromGregorian(date.year, date.month, date.day, date.hour, date.minute, date.second); 73 | 74 | if (isValidKhmerDate(result)) { 75 | passedTests++; 76 | } else { 77 | failedTests++; 78 | failures.push({ 79 | category: testCategories[0].name, 80 | date: formatGregorianDate(date), 81 | error: 'Invalid Khmer date structure', 82 | result 83 | }); 84 | } 85 | } catch (error) { 86 | failedTests++; 87 | failures.push({ 88 | category: testCategories[0].name, 89 | date: formatGregorianDate(date), 90 | error: error.message 91 | }); 92 | } 93 | } 94 | console.log(` Completed: ${testCategories[0].count} tests`); 95 | 96 | // Category 2: Edge cases - Pisakha Bochea dates (50 tests) 97 | // Test around the Pisakha Bochea day (15th waxing Pisakh) where BE year changes 98 | console.log(`\n[2/${testCategories.length}] ${testCategories[1].name} (${testCategories[1].count} tests)...`); 99 | const visakhaBocheaYears = []; 100 | for (let i = 0; i < testCategories[1].count; i++) { 101 | const year = Math.floor(Math.random() * (2200 - 1900 + 1)) + 1900; 102 | visakhaBocheaYears.push(year); 103 | } 104 | 105 | for (const year of visakhaBocheaYears) { 106 | totalTests++; 107 | 108 | // Test Pisakha Bochea period (late April to late May) 109 | const month = Math.random() > 0.5 ? 4 : 5; 110 | const day = Math.floor(Math.random() * 20) + 10; // Days 10-29 111 | const hour = Math.floor(Math.random() * 24); 112 | const minute = Math.floor(Math.random() * 60); 113 | 114 | try { 115 | const result = momentkh.fromGregorian(year, month, day, hour, minute, 0); 116 | 117 | if (isValidKhmerDate(result)) { 118 | // Additional validation: if it's Pisakh month (index 5), check the date 119 | if (result.khmer.monthIndex === 5) { 120 | if (result.khmer.day >= 1 && result.khmer.day <= 15) { 121 | passedTests++; 122 | } else { 123 | failedTests++; 124 | failures.push({ 125 | category: testCategories[1].name, 126 | date: formatGregorianDate({ year, month, day, hour, minute, second: 0 }), 127 | error: 'Invalid day in Pisakh month', 128 | result 129 | }); 130 | } 131 | } else { 132 | passedTests++; 133 | } 134 | } else { 135 | failedTests++; 136 | failures.push({ 137 | category: testCategories[1].name, 138 | date: formatGregorianDate({ year, month, day, hour, minute, second: 0 }), 139 | error: 'Invalid Khmer date structure', 140 | result 141 | }); 142 | } 143 | } catch (error) { 144 | failedTests++; 145 | failures.push({ 146 | category: testCategories[1].name, 147 | date: formatGregorianDate({ year, month, day, hour, minute, second: 0 }), 148 | error: error.message 149 | }); 150 | } 151 | } 152 | console.log(` Completed: ${testCategories[1].count} tests`); 153 | 154 | // Category 3: Edge cases - New Year period (30 tests) 155 | // Test around Khmer New Year (typically April 13-17) 156 | console.log(`\n[3/${testCategories.length}] ${testCategories[2].name} (${testCategories[2].count} tests)...`); 157 | for (let i = 0; i < testCategories[2].count; i++) { 158 | totalTests++; 159 | 160 | const year = Math.floor(Math.random() * (2200 - 1900 + 1)) + 1900; 161 | const month = 4; // April 162 | const day = Math.floor(Math.random() * 10) + 10; // Days 10-19 163 | const hour = Math.floor(Math.random() * 24); 164 | const minute = Math.floor(Math.random() * 60); 165 | 166 | try { 167 | const result = momentkh.fromGregorian(year, month, day, hour, minute, 0); 168 | 169 | if (isValidKhmerDate(result)) { 170 | passedTests++; 171 | } else { 172 | failedTests++; 173 | failures.push({ 174 | category: testCategories[2].name, 175 | date: formatGregorianDate({ year, month, day, hour, minute, second: 0 }), 176 | error: 'Invalid Khmer date structure', 177 | result 178 | }); 179 | } 180 | } catch (error) { 181 | failedTests++; 182 | failures.push({ 183 | category: testCategories[2].name, 184 | date: formatGregorianDate({ year, month, day, hour, minute, second: 0 }), 185 | error: error.message 186 | }); 187 | } 188 | } 189 | console.log(` Completed: ${testCategories[2].count} tests`); 190 | 191 | // Category 4: Leap year dates (20 tests) 192 | console.log(`\n[4/${testCategories.length}] ${testCategories[3].name} (${testCategories[3].count} tests)...`); 193 | // Only include actual leap years. Years divisible by 100 but not 400 are NOT leap years. 194 | // 1800, 1900, 2100, 2200, 2300 are NOT leap years 195 | const leapYears = [1804, 1808, 1996, 2000, 2004, 2008, 2012, 2016, 2020, 2024, 2028, 2032, 2036, 2040, 2044, 2048, 2052, 2096, 2296, 2400]; 196 | for (let i = 0; i < testCategories[3].count; i++) { 197 | totalTests++; 198 | 199 | const year = leapYears[i % leapYears.length]; 200 | const month = 2; // February 201 | const day = 29; // Leap day 202 | const hour = Math.floor(Math.random() * 24); 203 | const minute = Math.floor(Math.random() * 60); 204 | 205 | try { 206 | const result = momentkh.fromGregorian(year, month, day, hour, minute, 0); 207 | 208 | if (isValidKhmerDate(result)) { 209 | passedTests++; 210 | } else { 211 | failedTests++; 212 | failures.push({ 213 | category: testCategories[3].name, 214 | date: formatGregorianDate({ year, month, day, hour, minute, second: 0 }), 215 | error: 'Invalid Khmer date structure', 216 | result 217 | }); 218 | } 219 | } catch (error) { 220 | failedTests++; 221 | failures.push({ 222 | category: testCategories[3].name, 223 | date: formatGregorianDate({ year, month, day, hour, minute, second: 0 }), 224 | error: error.message 225 | }); 226 | } 227 | } 228 | console.log(` Completed: ${testCategories[3].count} tests`); 229 | 230 | // Print summary 231 | console.log('\n' + '='.repeat(80)); 232 | console.log('TEST RESULTS SUMMARY'); 233 | console.log('='.repeat(80)); 234 | console.log(`Total Tests: ${totalTests}`); 235 | console.log(`Passed: ${passedTests} (${(passedTests / totalTests * 100).toFixed(2)}%)`); 236 | console.log(`Failed: ${failedTests} (${(failedTests / totalTests * 100).toFixed(2)}%)`); 237 | console.log('='.repeat(80)); 238 | 239 | // Print failures if any 240 | if (failures.length > 0) { 241 | console.log('\nFAILURES:'); 242 | console.log('-'.repeat(80)); 243 | failures.slice(0, 10).forEach((failure, index) => { 244 | console.log(`\n${index + 1}. ${failure.category}`); 245 | console.log(` Date: ${failure.date}`); 246 | console.log(` Error: ${failure.error}`); 247 | if (failure.result) { 248 | console.log(` Result: ${JSON.stringify(failure.result.khmer, null, 2)}`); 249 | } 250 | }); 251 | if (failures.length > 10) { 252 | console.log(`\n... and ${failures.length - 10} more failures`); 253 | } 254 | console.log('\n' + '='.repeat(80)); 255 | process.exit(1); 256 | } else { 257 | console.log('\n✓ ALL TESTS PASSED!\n'); 258 | process.exit(0); 259 | } 260 | -------------------------------------------------------------------------------- /test/new-year.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Suite: Khmer New Year Calculation 3 | * 4 | * This test validates the calculation of Khmer New Year (Moha Songkran) dates. 5 | * It runs 500 test cases covering various years and validates the results. 6 | */ 7 | 8 | const momentkh = require('../momentkh'); 9 | 10 | // Helper function to validate New Year info 11 | function isValidNewYearInfo(info) { 12 | if (!info || typeof info !== 'object') return false; 13 | 14 | const { year, month, day, hour, minute } = info; 15 | 16 | // Validate year (reasonable range) 17 | if (typeof year !== 'number' || year < 1700 || year > 2400) return false; 18 | 19 | // Validate month (should be April = 4) 20 | if (typeof month !== 'number' || month < 3 || month > 5) return false; 21 | 22 | // Validate day (should be around 12-15 April) 23 | if (typeof day !== 'number' || day < 1 || day > 31) return false; 24 | 25 | // Validate hour (0-23) 26 | if (typeof hour !== 'number' || hour < 0 || hour > 23) return false; 27 | 28 | // Validate minute (0-59) 29 | if (typeof minute !== 'number' || minute < 0 || minute > 59) return false; 30 | 31 | return true; 32 | } 33 | 34 | // Helper function to format New Year info 35 | function formatNewYearInfo(info) { 36 | return `${info.year}-${String(info.month).padStart(2, '0')}-${String(info.day).padStart(2, '0')} ${String(info.hour).padStart(2, '0')}:${String(info.minute).padStart(2, '0')}`; 37 | } 38 | 39 | // Known Khmer New Year dates for validation (from historical data) 40 | const knownNewYears = { 41 | 2011: { year: 2011, month: 4, day: 14, hour: 13, minute: 12 }, 42 | 2012: { year: 2012, month: 4, day: 14, hour: 19, minute: 11 }, 43 | 2013: { year: 2013, month: 4, day: 14, hour: 2, minute: 12 }, 44 | 2014: { year: 2014, month: 4, day: 14, hour: 8, minute: 7 }, 45 | 2015: { year: 2015, month: 4, day: 14, hour: 14, minute: 2 }, 46 | 1879: { year: 1879, month: 4, day: 12, hour: 11, minute: 36 }, 47 | 1897: { year: 1897, month: 4, day: 13, hour: 2, minute: 0 } 48 | }; 49 | 50 | // Test categories 51 | const testCategories = [ 52 | { name: 'Historical years with known dates', count: 7 }, 53 | { name: 'Random years (1800-1900)', count: 100 }, 54 | { name: 'Random years (1901-2000)', count: 100 }, 55 | { name: 'Random years (2001-2100)', count: 100 }, 56 | { name: 'Random years (2101-2200)', count: 100 }, 57 | { name: 'Random years (2201-2300)', count: 93 } 58 | ]; 59 | 60 | console.log('='.repeat(80)); 61 | console.log('TEST SUITE: KHMER NEW YEAR CALCULATION'); 62 | console.log('='.repeat(80)); 63 | console.log(`Total test cases: 500\n`); 64 | 65 | let totalTests = 0; 66 | let passedTests = 0; 67 | let failedTests = 0; 68 | const failures = []; 69 | 70 | // Category 1: Historical years with known dates (7 tests) 71 | console.log(`\n[1/${testCategories.length}] ${testCategories[0].name} (${testCategories[0].count} tests)...`); 72 | for (const [yearStr, expected] of Object.entries(knownNewYears)) { 73 | totalTests++; 74 | const year = parseInt(yearStr); 75 | 76 | try { 77 | const result = momentkh.getNewYear(year); 78 | 79 | if (isValidNewYearInfo(result)) { 80 | // Verify it matches the known date 81 | if (result.year === expected.year && 82 | result.month === expected.month && 83 | result.day === expected.day && 84 | result.hour === expected.hour && 85 | result.minute === expected.minute) { 86 | passedTests++; 87 | } else { 88 | failedTests++; 89 | failures.push({ 90 | category: testCategories[0].name, 91 | year, 92 | expected: formatNewYearInfo(expected), 93 | result: formatNewYearInfo(result), 94 | error: 'Date/time mismatch with known historical data' 95 | }); 96 | } 97 | } else { 98 | failedTests++; 99 | failures.push({ 100 | category: testCategories[0].name, 101 | year, 102 | error: 'Invalid New Year info structure', 103 | result 104 | }); 105 | } 106 | } catch (error) { 107 | failedTests++; 108 | failures.push({ 109 | category: testCategories[0].name, 110 | year, 111 | error: error.message 112 | }); 113 | } 114 | } 115 | console.log(` Completed: ${testCategories[0].count} tests`); 116 | 117 | // Category 2-6: Random years in different ranges 118 | for (let categoryIndex = 1; categoryIndex < testCategories.length; categoryIndex++) { 119 | const category = testCategories[categoryIndex]; 120 | console.log(`\n[${categoryIndex + 1}/${testCategories.length}] ${category.name} (${category.count} tests)...`); 121 | 122 | // Determine year range 123 | let startYear, endYear; 124 | if (categoryIndex === 1) { 125 | startYear = 1800; 126 | endYear = 1900; 127 | } else if (categoryIndex === 2) { 128 | startYear = 1901; 129 | endYear = 2000; 130 | } else if (categoryIndex === 3) { 131 | startYear = 2001; 132 | endYear = 2100; 133 | } else if (categoryIndex === 4) { 134 | startYear = 2101; 135 | endYear = 2200; 136 | } else { 137 | startYear = 2201; 138 | endYear = 2300; 139 | } 140 | 141 | // Generate random years in this range 142 | const years = []; 143 | for (let i = 0; i < category.count; i++) { 144 | const year = Math.floor(Math.random() * (endYear - startYear + 1)) + startYear; 145 | years.push(year); 146 | } 147 | 148 | for (const year of years) { 149 | totalTests++; 150 | 151 | try { 152 | const result = momentkh.getNewYear(year); 153 | 154 | if (isValidNewYearInfo(result)) { 155 | // Additional validation: New Year should be in March-April-May 156 | if (result.month >= 3 && result.month <= 5) { 157 | // Most commonly April 13-14, but can be 12-15 158 | if (result.month === 4 && result.day >= 12 && result.day <= 15) { 159 | passedTests++; 160 | } else if (result.month === 3 && result.day >= 20) { 161 | // Late March is also possible in some years 162 | passedTests++; 163 | } else if (result.month === 5 && result.day <= 5) { 164 | // Early May is possible in some years 165 | passedTests++; 166 | } else if (result.month === 4) { 167 | // Any day in April is acceptable (edge cases) 168 | passedTests++; 169 | } else { 170 | failedTests++; 171 | failures.push({ 172 | category: category.name, 173 | year, 174 | result: formatNewYearInfo(result), 175 | error: 'New Year date outside expected range' 176 | }); 177 | } 178 | } else { 179 | failedTests++; 180 | failures.push({ 181 | category: category.name, 182 | year, 183 | result: formatNewYearInfo(result), 184 | error: 'New Year month outside expected range (should be March-May)' 185 | }); 186 | } 187 | } else { 188 | failedTests++; 189 | failures.push({ 190 | category: category.name, 191 | year, 192 | error: 'Invalid New Year info structure', 193 | result 194 | }); 195 | } 196 | } catch (error) { 197 | failedTests++; 198 | failures.push({ 199 | category: category.name, 200 | year, 201 | error: error.message 202 | }); 203 | } 204 | } 205 | 206 | console.log(` Completed: ${category.count} tests`); 207 | } 208 | 209 | // Additional validation: Check consistency across consecutive years 210 | console.log('\n[Extra] Consistency check: Consecutive years...'); 211 | let consecutiveTestsPassed = 0; 212 | let consecutiveTestsFailed = 0; 213 | const consecutiveFailures = []; 214 | 215 | for (let year = 1900; year <= 2100; year += 10) { 216 | try { 217 | const result1 = momentkh.getNewYear(year); 218 | const result2 = momentkh.getNewYear(year + 1); 219 | 220 | // New Year should not jump by more than a few days between consecutive years 221 | const date1 = new Date(result1.year, result1.month - 1, result1.day, result1.hour, result1.minute); 222 | const date2 = new Date(result2.year, result2.month - 1, result2.day, result2.hour, result2.minute); 223 | const daysDiff = Math.abs((date2 - date1) / (1000 * 60 * 60 * 24)); 224 | 225 | if (daysDiff >= 360 && daysDiff <= 370) { 226 | consecutiveTestsPassed++; 227 | } else { 228 | consecutiveTestsFailed++; 229 | consecutiveFailures.push({ 230 | year1: year, 231 | year2: year + 1, 232 | date1: formatNewYearInfo(result1), 233 | date2: formatNewYearInfo(result2), 234 | daysDiff: daysDiff.toFixed(2) 235 | }); 236 | } 237 | } catch (error) { 238 | consecutiveTestsFailed++; 239 | } 240 | } 241 | 242 | console.log(` Passed: ${consecutiveTestsPassed}, Failed: ${consecutiveTestsFailed}`); 243 | 244 | // Print summary 245 | console.log('\n' + '='.repeat(80)); 246 | console.log('TEST RESULTS SUMMARY'); 247 | console.log('='.repeat(80)); 248 | console.log(`Total Tests: ${totalTests}`); 249 | console.log(`Passed: ${passedTests} (${(passedTests / totalTests * 100).toFixed(2)}%)`); 250 | console.log(`Failed: ${failedTests} (${(failedTests / totalTests * 100).toFixed(2)}%)`); 251 | console.log('='.repeat(80)); 252 | 253 | // Print failures if any 254 | if (failures.length > 0) { 255 | console.log('\nFAILURES:'); 256 | console.log('-'.repeat(80)); 257 | failures.slice(0, 10).forEach((failure, index) => { 258 | console.log(`\n${index + 1}. ${failure.category}`); 259 | console.log(` Year: ${failure.year}`); 260 | if (failure.expected) { 261 | console.log(` Expected: ${failure.expected}`); 262 | } 263 | if (failure.result) { 264 | console.log(` Result: ${typeof failure.result === 'string' ? failure.result : JSON.stringify(failure.result)}`); 265 | } 266 | console.log(` Error: ${failure.error}`); 267 | }); 268 | if (failures.length > 10) { 269 | console.log(`\n... and ${failures.length - 10} more failures`); 270 | } 271 | } 272 | 273 | if (consecutiveFailures.length > 0) { 274 | console.log('\nCONSECUTIVE YEAR FAILURES:'); 275 | console.log('-'.repeat(80)); 276 | consecutiveFailures.slice(0, 5).forEach((failure, index) => { 277 | console.log(`\n${index + 1}. Years ${failure.year1} → ${failure.year2}`); 278 | console.log(` ${failure.year1}: ${failure.date1}`); 279 | console.log(` ${failure.year2}: ${failure.date2}`); 280 | console.log(` Days difference: ${failure.daysDiff} (expected ~365)`); 281 | }); 282 | if (consecutiveFailures.length > 5) { 283 | console.log(`\n... and ${consecutiveFailures.length - 5} more consecutive failures`); 284 | } 285 | } 286 | 287 | if (failures.length > 0 || consecutiveFailures.length > 0) { 288 | console.log('\n' + '='.repeat(80)); 289 | process.exit(1); 290 | } else { 291 | console.log('\n✓ ALL TESTS PASSED!\n'); 292 | process.exit(0); 293 | } 294 | -------------------------------------------------------------------------------- /test/khmer-to-gregorian.test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Test Suite: Khmer to Gregorian Conversion 3 | * 4 | * This test validates the conversion from Khmer calendar dates to Gregorian calendar dates. 5 | * It runs 500 test cases covering various Khmer dates and validates round-trip conversion. 6 | */ 7 | 8 | const momentkh = require('../momentkh'); 9 | 10 | // Helper function to generate random Khmer date 11 | function randomKhmerDate() { 12 | const beYear = Math.floor(Math.random() * (2850 - 2350 + 1)) + 2350; // BE years 2350-2850 13 | // Avoid month index 7 (អាសាឍ) as it doesn't exist in leap month years 14 | // Use months 0-6, 8-11 (skip 7) 15 | let monthIndex = Math.floor(Math.random() * 11); 16 | if (monthIndex >= 7) monthIndex++; // Skip index 7 17 | 18 | const moonPhase = Math.floor(Math.random() * 2); // 0 = កើត, 1 = រោច 19 | 20 | // For កើត (waxing): 1-15 days 21 | // For រោច (waning): 1-14 days (to be safe, since some months have 29 days = 15កើត + 14រោច) 22 | const maxDay = moonPhase === 0 ? 15 : 14; 23 | const day = Math.floor(Math.random() * maxDay) + 1; 24 | 25 | return { day, moonPhase, monthIndex, beYear }; 26 | } 27 | 28 | // Helper function to validate Gregorian date 29 | function isValidGregorianDate(date) { 30 | if (!date || typeof date.year !== 'number' || typeof date.month !== 'number' || typeof date.day !== 'number') { 31 | return false; 32 | } 33 | 34 | const { year, month, day } = date; 35 | 36 | // Validate year (reasonable range) 37 | if (year < 1700 || year > 2400) return false; 38 | 39 | // Validate month (1-12) 40 | if (month < 1 || month > 12) return false; 41 | 42 | // Validate day (1-31) 43 | if (day < 1 || day > 31) return false; 44 | 45 | // Check days in month 46 | const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 47 | const isLeapYear = (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); 48 | const maxDay = (month === 2 && isLeapYear) ? 29 : daysInMonth[month - 1]; 49 | 50 | if (day > maxDay) return false; 51 | 52 | return true; 53 | } 54 | 55 | // Helper function to format Khmer date 56 | function formatKhmerDate(khmerDate) { 57 | const moonPhaseNames = ['កើត', 'រោច']; 58 | const monthNames = [ 59 | 'មិគសិរ', 'បុស្ស', 'មាឃ', 'ផល្គុន', 'ចេត្រ', 'ពិសាខ', 60 | 'ជេស្ឋ', 'អាសាឍ', 'ស្រាពណ៍', 'ភទ្របទ', 'អស្សុជ', 'កត្ដិក' 61 | ]; 62 | return `${khmerDate.day}${moonPhaseNames[khmerDate.moonPhase]} ${monthNames[khmerDate.monthIndex]} BE ${khmerDate.beYear}`; 63 | } 64 | 65 | // Helper function to format Gregorian date 66 | function formatGregorianDate(date) { 67 | return `${date.year}-${String(date.month).padStart(2, '0')}-${String(date.day).padStart(2, '0')}`; 68 | } 69 | 70 | // Test categories 71 | const testCategories = [ 72 | { name: 'Random Khmer dates', count: 350 }, 73 | { name: 'Round-trip validation', count: 100 }, 74 | { name: 'Edge case - Pisakha Bochea dates', count: 30 }, 75 | { name: 'Edge case - Month transitions', count: 20 } 76 | ]; 77 | 78 | console.log('='.repeat(80)); 79 | console.log('TEST SUITE: KHMER TO GREGORIAN CONVERSION'); 80 | console.log('='.repeat(80)); 81 | console.log(`Total test cases: 500\n`); 82 | 83 | let totalTests = 0; 84 | let passedTests = 0; 85 | let failedTests = 0; 86 | const failures = []; 87 | 88 | // Category 1: Random Khmer dates (350 tests) 89 | console.log(`\n[1/${testCategories.length}] ${testCategories[0].name} (${testCategories[0].count} tests)...`); 90 | for (let i = 0; i < testCategories[0].count; i++) { 91 | totalTests++; 92 | const khmerDate = randomKhmerDate(); 93 | 94 | try { 95 | const result = momentkh.fromKhmer(khmerDate.day, khmerDate.moonPhase, khmerDate.monthIndex, khmerDate.beYear); 96 | 97 | if (isValidGregorianDate(result)) { 98 | passedTests++; 99 | } else { 100 | failedTests++; 101 | failures.push({ 102 | category: testCategories[0].name, 103 | khmerDate: formatKhmerDate(khmerDate), 104 | error: 'Invalid Gregorian date structure', 105 | result 106 | }); 107 | } 108 | } catch (error) { 109 | failedTests++; 110 | failures.push({ 111 | category: testCategories[0].name, 112 | khmerDate: formatKhmerDate(khmerDate), 113 | error: error.message 114 | }); 115 | } 116 | } 117 | console.log(` Completed: ${testCategories[0].count} tests`); 118 | 119 | // Category 2: Round-trip validation (100 tests) 120 | // Convert Gregorian → Khmer → Gregorian and verify they match 121 | console.log(`\n[2/${testCategories.length}] ${testCategories[1].name} (${testCategories[1].count} tests)...`); 122 | for (let i = 0; i < testCategories[1].count; i++) { 123 | totalTests++; 124 | 125 | // Generate random Gregorian date 126 | const year = Math.floor(Math.random() * (2200 - 1900 + 1)) + 1900; 127 | const month = Math.floor(Math.random() * 12) + 1; 128 | const day = Math.floor(Math.random() * 28) + 1; 129 | const hour = Math.floor(Math.random() * 24); 130 | const minute = Math.floor(Math.random() * 60); 131 | 132 | try { 133 | // Step 1: Gregorian → Khmer 134 | const khmerResult = momentkh.fromGregorian(year, month, day, hour, minute, 0); 135 | 136 | // Step 2: Khmer → Gregorian 137 | const gregorianResult = momentkh.fromKhmer( 138 | khmerResult.khmer.day, 139 | khmerResult.khmer.moonPhase, 140 | khmerResult.khmer.monthIndex, 141 | khmerResult.khmer.beYear 142 | ); 143 | 144 | // Step 3: Verify dates match 145 | if (gregorianResult.year === year && gregorianResult.month === month && gregorianResult.day === day) { 146 | passedTests++; 147 | } else { 148 | failedTests++; 149 | failures.push({ 150 | category: testCategories[1].name, 151 | original: formatGregorianDate({ year, month, day }), 152 | khmerDate: formatKhmerDate({ 153 | day: khmerResult.khmer.day, 154 | moonPhase: khmerResult.khmer.moonPhase, 155 | monthIndex: khmerResult.khmer.monthIndex, 156 | beYear: khmerResult.khmer.beYear 157 | }), 158 | result: formatGregorianDate(gregorianResult), 159 | error: 'Round-trip conversion mismatch' 160 | }); 161 | } 162 | } catch (error) { 163 | failedTests++; 164 | failures.push({ 165 | category: testCategories[1].name, 166 | original: formatGregorianDate({ year, month, day }), 167 | error: error.message 168 | }); 169 | } 170 | } 171 | console.log(` Completed: ${testCategories[1].count} tests`); 172 | 173 | // Category 3: Edge case - Pisakha Bochea dates (30 tests) 174 | // Test 15កើត ពិសាខ (Pisakha Bochea) for various BE years 175 | console.log(`\n[3/${testCategories.length}] ${testCategories[2].name} (${testCategories[2].count} tests)...`); 176 | for (let i = 0; i < testCategories[2].count; i++) { 177 | totalTests++; 178 | 179 | const beYear = Math.floor(Math.random() * (2850 - 2400 + 1)) + 2400; 180 | const day = 15; 181 | const moonPhase = 0; // កើត 182 | const monthIndex = 5; // ពិសាខ 183 | 184 | try { 185 | const result = momentkh.fromKhmer(day, moonPhase, monthIndex, beYear); 186 | 187 | if (isValidGregorianDate(result)) { 188 | // Verify it converts back correctly 189 | const verifyKhmer = momentkh.fromGregorian(result.year, result.month, result.day, 12, 0, 0); 190 | 191 | if (verifyKhmer.khmer.beYear === beYear && 192 | verifyKhmer.khmer.monthIndex === monthIndex && 193 | verifyKhmer.khmer.day === day && 194 | verifyKhmer.khmer.moonPhase === moonPhase) { 195 | passedTests++; 196 | } else { 197 | failedTests++; 198 | failures.push({ 199 | category: testCategories[2].name, 200 | khmerDate: formatKhmerDate({ day, moonPhase, monthIndex, beYear }), 201 | error: 'Pisakha Bochea round-trip mismatch', 202 | result: formatGregorianDate(result), 203 | verifyKhmer: verifyKhmer.khmer 204 | }); 205 | } 206 | } else { 207 | failedTests++; 208 | failures.push({ 209 | category: testCategories[2].name, 210 | khmerDate: formatKhmerDate({ day, moonPhase, monthIndex, beYear }), 211 | error: 'Invalid Gregorian date structure', 212 | result 213 | }); 214 | } 215 | } catch (error) { 216 | failedTests++; 217 | failures.push({ 218 | category: testCategories[2].name, 219 | khmerDate: formatKhmerDate({ day, moonPhase, monthIndex, beYear }), 220 | error: error.message 221 | }); 222 | } 223 | } 224 | console.log(` Completed: ${testCategories[2].count} tests`); 225 | 226 | // Category 4: Edge case - Month transitions (20 tests) 227 | // Test last day of waxing and first/last day of waning 228 | console.log(`\n[4/${testCategories.length}] ${testCategories[3].name} (${testCategories[3].count} tests)...`); 229 | for (let i = 0; i < testCategories[3].count; i++) { 230 | totalTests++; 231 | 232 | const beYear = Math.floor(Math.random() * (2800 - 2400 + 1)) + 2400; 233 | // Avoid month index 7 (អាសាឍ) as it doesn't exist in leap month years 234 | let monthIndex = Math.floor(Math.random() * 11); 235 | if (monthIndex >= 7) monthIndex++; // Skip index 7 236 | 237 | const moonPhase = i % 2; // Alternate between កើត and រោច 238 | // For កើត: use day 15 (last day), for រោច: use day 1 or 14 (first/last day) 239 | const day = moonPhase === 0 ? 15 : (i % 4 < 2 ? 1 : 14); 240 | 241 | try { 242 | const result = momentkh.fromKhmer(day, moonPhase, monthIndex, beYear); 243 | 244 | if (isValidGregorianDate(result)) { 245 | passedTests++; 246 | } else { 247 | failedTests++; 248 | failures.push({ 249 | category: testCategories[3].name, 250 | khmerDate: formatKhmerDate({ day, moonPhase, monthIndex, beYear }), 251 | error: 'Invalid Gregorian date structure', 252 | result 253 | }); 254 | } 255 | } catch (error) { 256 | failedTests++; 257 | failures.push({ 258 | category: testCategories[3].name, 259 | khmerDate: formatKhmerDate({ day, moonPhase, monthIndex, beYear }), 260 | error: error.message 261 | }); 262 | } 263 | } 264 | console.log(` Completed: ${testCategories[3].count} tests`); 265 | 266 | // Print summary 267 | console.log('\n' + '='.repeat(80)); 268 | console.log('TEST RESULTS SUMMARY'); 269 | console.log('='.repeat(80)); 270 | console.log(`Total Tests: ${totalTests}`); 271 | console.log(`Passed: ${passedTests} (${(passedTests / totalTests * 100).toFixed(2)}%)`); 272 | console.log(`Failed: ${failedTests} (${(failedTests / totalTests * 100).toFixed(2)}%)`); 273 | console.log('='.repeat(80)); 274 | 275 | // Print failures if any 276 | if (failures.length > 0) { 277 | console.log('\nFAILURES:'); 278 | console.log('-'.repeat(80)); 279 | failures.slice(0, 10).forEach((failure, index) => { 280 | console.log(`\n${index + 1}. ${failure.category}`); 281 | if (failure.khmerDate) { 282 | console.log(` Khmer Date: ${failure.khmerDate}`); 283 | } 284 | if (failure.original) { 285 | console.log(` Original: ${failure.original}`); 286 | } 287 | console.log(` Error: ${failure.error}`); 288 | if (failure.result) { 289 | console.log(` Result: ${typeof failure.result === 'string' ? failure.result : JSON.stringify(failure.result)}`); 290 | } 291 | }); 292 | if (failures.length > 10) { 293 | console.log(`\n... and ${failures.length - 10} more failures`); 294 | } 295 | console.log('\n' + '='.repeat(80)); 296 | process.exit(1); 297 | } else { 298 | console.log('\n✓ ALL TESTS PASSED!\n'); 299 | process.exit(0); 300 | } 301 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | MomentKH v3 - Khmer Calendar Library 7 | 8 | 9 | 10 | 11 | 34 | 35 | 36 | 55 | 56 |
57 |
58 |

🇰🇭 ប្រតិទិនខ្មែរ

59 |

Khmer Calendar Library v3.0.2

60 |

Zero Dependencies • TypeScript Support • Full Bidirectional Conversion

61 |
62 | 63 | 64 |
65 |
66 |

📅 Date Conversion

67 |
68 |
69 |
70 | 71 | 72 |
73 |
74 |
Loading...
75 |

Loading...

76 |
77 |
78 | Show Code 79 |
const selectedDate = new Date('2024-12-17');
 80 | const khmer = momentkh.fromDate(selectedDate);
 81 | console.log(momentkh.format(khmer));
82 |
83 |
84 |
85 | 86 | 87 |
88 |
89 |

🎊 Khmer New Year

90 |
91 |
92 |
93 | 94 | 95 |
96 |
97 |
Loading...
98 |

Loading...

99 |
100 |
101 | Show Code 102 |
const selectedYear = 2025;
103 | const newYear = momentkh.getNewYear(selectedYear);
104 | console.log(`${newYear.year}-${newYear.month}-${newYear.day} ${newYear.hour}:${newYear.minute}`);
105 |
106 |
107 |
108 | 109 | 110 |
111 |
112 |

🔄 Round-Trip Conversion

113 |
114 |
115 |
116 |

Original Gregorian: Loading...

117 |

Converted to Khmer: Loading...

118 |

Back to Gregorian: Loading...

119 |

Result: Loading...

120 |
121 |
122 | Show Code 123 |
// Gregorian → Khmer
124 | const today = new Date();
125 | const khmer = momentkh.fromDate(today);
126 | 
127 | // Khmer → Gregorian
128 | const backToGregorian = momentkh.fromKhmer(
129 |     khmer.khmer.day,
130 |     khmer.khmer.moonPhase,
131 |     khmer.khmer.monthIndex,
132 |     khmer.khmer.beYear
133 | );
134 | 
135 | // Verify they match
136 | console.log('Match:', backToGregorian.year === today.getFullYear());
137 |
138 |
139 |
140 | 141 | 142 |
143 |
144 |

⏰ Live Khmer Calendar

145 |
146 |
147 |
148 |
Loading...
149 |

Loading...

150 |
151 |
152 |
153 | 154 | 155 |
156 |

157 | MomentKH v3.0.2 • 158 | GitHub • 159 | NPM 160 |

161 |
162 |
163 | 164 | 165 | 166 | 318 | 319 | 320 | -------------------------------------------------------------------------------- /test/new-year.json: -------------------------------------------------------------------------------- 1 | { 2 | "1800": "1800-04-10T23:36:00+07:00", 3 | "1801": "1801-04-11T05:36:00+07:00", 4 | "1802": "1802-04-11T11:36:00+07:00", 5 | "1803": "1803-04-11T17:36:00+07:00", 6 | "1804": "1804-04-11T00:24:00+07:00", 7 | "1805": "1805-04-11T06:24:00+07:00", 8 | "1806": "1806-04-11T12:24:00+07:00", 9 | "1807": "1807-04-11T18:24:00+07:00", 10 | "1808": "1808-04-11T01:12:00+07:00", 11 | "1809": "1809-04-11T07:12:00+07:00", 12 | "1810": "1810-04-11T13:12:00+07:00", 13 | "1811": "1811-04-11T19:12:00+07:00", 14 | "1812": "1812-04-11T02:00:00+07:00", 15 | "1813": "1813-04-11T08:00:00+07:00", 16 | "1814": "1814-04-11T14:00:00+07:00", 17 | "1815": "1815-04-11T20:00:00+07:00", 18 | "1816": "1816-04-11T02:48:00+07:00", 19 | "1817": "1817-04-11T08:48:00+07:00", 20 | "1818": "1818-04-11T14:48:00+07:00", 21 | "1819": "1819-04-11T20:48:00+07:00", 22 | "1820": "1820-04-11T03:36:00+07:00", 23 | "1821": "1821-04-11T09:36:00+07:00", 24 | "1822": "1822-04-11T15:36:00+07:00", 25 | "1823": "1823-04-11T22:24:00+07:00", 26 | "1824": "1824-04-11T04:24:00+07:00", 27 | "1825": "1825-04-11T10:24:00+07:00", 28 | "1826": "1826-04-11T16:24:00+07:00", 29 | "1827": "1827-04-11T23:12:00+07:00", 30 | "1828": "1828-04-11T05:12:00+07:00", 31 | "1829": "1829-04-11T11:12:00+07:00", 32 | "1830": "1830-04-11T17:12:00+07:00", 33 | "1831": "1831-04-11T00:00:00+07:00", 34 | "1832": "1832-04-11T06:00:00+07:00", 35 | "1833": "1833-04-11T12:00:00+07:00", 36 | "1834": "1834-04-11T18:00:00+07:00", 37 | "1835": "1835-04-12T01:12:00+07:00", 38 | "1836": "1836-04-11T06:48:00+07:00", 39 | "1837": "1837-04-11T12:48:00+07:00", 40 | "1838": "1838-04-11T18:48:00+07:00", 41 | "1839": "1839-04-12T02:00:00+07:00", 42 | "1840": "1840-04-11T07:36:00+07:00", 43 | "1841": "1841-04-11T13:36:00+07:00", 44 | "1842": "1842-04-11T19:36:00+07:00", 45 | "1843": "1843-04-12T02:48:00+07:00", 46 | "1844": "1844-04-11T08:24:00+07:00", 47 | "1845": "1845-04-11T14:24:00+07:00", 48 | "1846": "1846-04-11T20:24:00+07:00", 49 | "1847": "1847-04-12T03:36:00+07:00", 50 | "1848": "1848-04-11T09:12:00+07:00", 51 | "1849": "1849-04-11T15:12:00+07:00", 52 | "1850": "1850-04-11T22:00:00+07:00", 53 | "1851": "1851-04-12T04:24:00+07:00", 54 | "1852": "1852-04-11T10:00:00+07:00", 55 | "1853": "1853-04-11T16:00:00+07:00", 56 | "1854": "1854-04-11T22:48:00+07:00", 57 | "1855": "1855-04-12T05:12:00+07:00", 58 | "1856": "1856-04-11T10:48:00+07:00", 59 | "1857": "1857-04-11T16:48:00+07:00", 60 | "1858": "1858-04-11T23:36:00+07:00", 61 | "1859": "1859-04-12T06:00:00+07:00", 62 | "1860": "1860-04-11T11:36:00+07:00", 63 | "1861": "1861-04-11T17:36:00+07:00", 64 | "1862": "1862-04-12T00:48:00+07:00", 65 | "1863": "1863-04-12T06:48:00+07:00", 66 | "1864": "1864-04-11T12:24:00+07:00", 67 | "1865": "1865-04-11T18:24:00+07:00", 68 | "1866": "1866-04-12T01:36:00+07:00", 69 | "1867": "1867-04-12T07:36:00+07:00", 70 | "1868": "1868-04-11T13:12:00+07:00", 71 | "1869": "1869-04-11T19:12:00+07:00", 72 | "1870": "1870-04-12T02:24:00+07:00", 73 | "1871": "1871-04-12T08:24:00+07:00", 74 | "1872": "1872-04-11T14:00:00+07:00", 75 | "1873": "1873-04-11T20:00:00+07:00", 76 | "1874": "1874-04-12T03:12:00+07:00", 77 | "1875": "1875-04-12T09:12:00+07:00", 78 | "1876": "1876-04-11T14:48:00+07:00", 79 | "1877": "1877-04-11T20:48:00+07:00", 80 | "1878": "1878-04-12T04:00:00+07:00", 81 | "1879": "1879-04-12T11:36:00+07:00", 82 | "1880": "1880-04-11T15:36:00+07:00", 83 | "1881": "1881-04-11T22:24:00+07:00", 84 | "1882": "1882-04-12T04:48:00+07:00", 85 | "1883": "1883-04-12T10:48:00+07:00", 86 | "1884": "1884-04-11T16:24:00+07:00", 87 | "1885": "1885-04-11T23:12:00+07:00", 88 | "1886": "1886-04-12T05:36:00+07:00", 89 | "1887": "1887-04-12T11:36:00+07:00", 90 | "1888": "1888-04-11T17:12:00+07:00", 91 | "1889": "1889-04-11T00:00:00+07:00", 92 | "1890": "1890-04-12T06:24:00+07:00", 93 | "1891": "1891-04-12T12:24:00+07:00", 94 | "1892": "1892-04-11T18:00:00+07:00", 95 | "1893": "1893-04-12T01:12:00+07:00", 96 | "1894": "1894-04-12T07:12:00+07:00", 97 | "1895": "1895-04-12T13:12:00+07:00", 98 | "1896": "1896-04-11T18:48:00+07:00", 99 | "1897": "1897-04-13T02:00:00+07:00", 100 | "1898": "1898-04-12T08:00:00+07:00", 101 | "1899": "1899-04-12T14:00:00+07:00", 102 | "1900": "1900-04-12T19:36:00+07:00", 103 | "1901": "1901-04-13T02:48:00+07:00", 104 | "1902": "1902-04-13T08:48:00+07:00", 105 | "1903": "1903-04-13T14:48:00+07:00", 106 | "1904": "1904-04-12T20:24:00+07:00", 107 | "1905": "1905-04-13T03:36:00+07:00", 108 | "1906": "1906-04-13T09:36:00+07:00", 109 | "1907": "1907-04-13T15:36:00+07:00", 110 | "1908": "1908-04-12T22:24:00+07:00", 111 | "1909": "1909-04-13T04:24:00+07:00", 112 | "1910": "1910-04-13T10:24:00+07:00", 113 | "1911": "1911-04-13T16:24:00+07:00", 114 | "1912": "1912-04-12T23:12:00+07:00", 115 | "1913": "1913-04-13T05:12:00+07:00", 116 | "1914": "1914-04-13T11:12:00+07:00", 117 | "1915": "1915-04-13T17:12:00+07:00", 118 | "1916": "1916-04-12T00:00:00+07:00", 119 | "1917": "1917-04-13T06:00:00+07:00", 120 | "1918": "1918-04-13T12:00:00+07:00", 121 | "1919": "1919-04-13T18:00:00+07:00", 122 | "1920": "1920-04-13T00:48:00+07:00", 123 | "1921": "1921-04-13T06:48:00+07:00", 124 | "1922": "1922-04-13T12:48:00+07:00", 125 | "1923": "1923-04-13T18:48:00+07:00", 126 | "1924": "1924-04-13T01:36:00+07:00", 127 | "1925": "1925-04-13T07:36:00+07:00", 128 | "1926": "1926-04-13T13:36:00+07:00", 129 | "1927": "1927-04-13T19:36:00+07:00", 130 | "1928": "1928-04-13T02:24:00+07:00", 131 | "1929": "1929-04-13T08:24:00+07:00", 132 | "1930": "1930-04-13T14:24:00+07:00", 133 | "1931": "1931-04-13T20:24:00+07:00", 134 | "1932": "1932-04-13T03:12:00+07:00", 135 | "1933": "1933-04-13T09:12:00+07:00", 136 | "1934": "1934-04-13T15:12:00+07:00", 137 | "1935": "1935-04-13T22:00:00+07:00", 138 | "1936": "1936-04-13T04:00:00+07:00", 139 | "1937": "1937-04-13T10:00:00+07:00", 140 | "1938": "1938-04-13T16:00:00+07:00", 141 | "1939": "1939-04-13T22:48:00+07:00", 142 | "1940": "1940-04-13T04:48:00+07:00", 143 | "1941": "1941-04-13T10:48:00+07:00", 144 | "1942": "1942-04-13T16:48:00+07:00", 145 | "1943": "1943-04-13T23:36:00+07:00", 146 | "1944": "1944-04-13T05:36:00+07:00", 147 | "1945": "1945-04-13T11:36:00+07:00", 148 | "1946": "1946-04-13T17:36:00+07:00", 149 | "1947": "1947-04-14T00:48:00+07:00", 150 | "1948": "1948-04-13T06:24:00+07:00", 151 | "1949": "1949-04-13T12:24:00+07:00", 152 | "1950": "1950-04-13T18:24:00+07:00", 153 | "1951": "1951-04-14T01:36:00+07:00", 154 | "1952": "1952-04-13T07:12:00+07:00", 155 | "1953": "1953-04-13T13:12:00+07:00", 156 | "1954": "1954-04-13T19:12:00+07:00", 157 | "1955": "1955-04-14T02:24:00+07:00", 158 | "1956": "1956-04-13T08:00:00+07:00", 159 | "1957": "1957-04-13T14:00:00+07:00", 160 | "1958": "1958-04-13T20:00:00+07:00", 161 | "1959": "1959-04-14T03:12:00+07:00", 162 | "1960": "1960-04-13T08:48:00+07:00", 163 | "1961": "1961-04-13T14:48:00+07:00", 164 | "1962": "1962-04-13T20:48:00+07:00", 165 | "1963": "1963-04-14T04:00:00+07:00", 166 | "1964": "1964-04-13T09:36:00+07:00", 167 | "1965": "1965-04-13T15:36:00+07:00", 168 | "1966": "1966-04-13T22:24:00+07:00", 169 | "1967": "1967-04-14T04:48:00+07:00", 170 | "1968": "1968-04-13T10:24:00+07:00", 171 | "1969": "1969-04-13T16:24:00+07:00", 172 | "1970": "1970-04-13T23:12:00+07:00", 173 | "1971": "1971-04-14T05:36:00+07:00", 174 | "1972": "1972-04-13T11:12:00+07:00", 175 | "1973": "1973-04-13T17:12:00+07:00", 176 | "1974": "1974-04-13T00:00:00+07:00", 177 | "1975": "1975-04-14T06:24:00+07:00", 178 | "1976": "1976-04-13T12:00:00+07:00", 179 | "1977": "1977-04-13T18:00:00+07:00", 180 | "1978": "1978-04-14T01:12:00+07:00", 181 | "1979": "1979-04-14T07:12:00+07:00", 182 | "1980": "1980-04-13T12:48:00+07:00", 183 | "1981": "1981-04-13T18:48:00+07:00", 184 | "1982": "1982-04-14T02:00:00+07:00", 185 | "1983": "1983-04-14T08:00:00+07:00", 186 | "1984": "1984-04-13T13:36:00+07:00", 187 | "1985": "1985-04-13T19:36:00+07:00", 188 | "1986": "1986-04-14T02:48:00+07:00", 189 | "1987": "1987-04-14T08:48:00+07:00", 190 | "1988": "1988-04-13T14:24:00+07:00", 191 | "1989": "1989-04-13T20:24:00+07:00", 192 | "1990": "1990-04-14T03:36:00+07:00", 193 | "1991": "1991-04-14T09:36:00+07:00", 194 | "1992": "1992-04-13T15:12:00+07:00", 195 | "1993": "1993-04-13T22:00:00+07:00", 196 | "1994": "1994-04-14T04:24:00+07:00", 197 | "1995": "1995-04-14T10:24:00+07:00", 198 | "1996": "1996-04-13T16:00:00+07:00", 199 | "1997": "1997-04-13T22:48:00+07:00", 200 | "1998": "1998-04-14T05:12:00+07:00", 201 | "1999": "1999-04-14T11:12:00+07:00", 202 | "2000": "2000-04-13T16:48:00+07:00", 203 | "2001": "2001-04-13T23:36:00+07:00", 204 | "2002": "2002-04-14T06:00:00+07:00", 205 | "2003": "2003-04-14T12:00:00+07:00", 206 | "2004": "2004-04-13T17:36:00+07:00", 207 | "2005": "2005-04-14T00:48:00+07:00", 208 | "2006": "2006-04-14T06:48:00+07:00", 209 | "2007": "2007-04-14T12:48:00+07:00", 210 | "2008": "2008-04-13T18:24:00+07:00", 211 | "2009": "2009-04-14T01:36:00+07:00", 212 | "2010": "2010-04-14T07:36:00+07:00", 213 | "2011": "2011-04-14T13:12:00+07:00", 214 | "2012": "2012-04-14T19:11:00+07:00", 215 | "2013": "2013-04-14T02:12:00+07:00", 216 | "2014": "2014-04-14T08:07:00+07:00", 217 | "2015": "2015-04-14T14:02:00+07:00", 218 | "2016": "2016-04-13T20:00:00+07:00", 219 | "2017": "2017-04-14T03:12:00+07:00", 220 | "2018": "2018-04-14T09:12:00+07:00", 221 | "2019": "2019-04-14T15:12:00+07:00", 222 | "2020": "2020-04-13T20:48:00+07:00", 223 | "2021": "2021-04-14T04:00:00+07:00", 224 | "2022": "2022-04-14T10:00:00+07:00", 225 | "2023": "2023-04-14T16:00:00+07:00", 226 | "2024": "2024-04-13T22:17:00+07:00", 227 | "2025": "2025-04-14T04:48:00+07:00", 228 | "2026": "2026-04-14T10:48:00+07:00", 229 | "2027": "2027-04-14T16:48:00+07:00", 230 | "2028": "2028-04-13T23:12:00+07:00", 231 | "2029": "2029-04-14T05:36:00+07:00", 232 | "2030": "2030-04-14T11:36:00+07:00", 233 | "2031": "2031-04-14T17:36:00+07:00", 234 | "2032": "2032-04-13T00:00:00+07:00", 235 | "2033": "2033-04-14T06:24:00+07:00", 236 | "2034": "2034-04-14T12:24:00+07:00", 237 | "2035": "2035-04-14T18:24:00+07:00", 238 | "2036": "2036-04-14T01:12:00+07:00", 239 | "2037": "2037-04-14T07:12:00+07:00", 240 | "2038": "2038-04-14T13:12:00+07:00", 241 | "2039": "2039-04-14T19:12:00+07:00", 242 | "2040": "2040-04-14T02:00:00+07:00", 243 | "2041": "2041-04-14T08:00:00+07:00", 244 | "2042": "2042-04-14T14:00:00+07:00", 245 | "2043": "2043-04-14T20:00:00+07:00", 246 | "2044": "2044-04-14T02:48:00+07:00", 247 | "2045": "2045-04-14T08:48:00+07:00", 248 | "2046": "2046-04-14T14:48:00+07:00", 249 | "2047": "2047-04-14T20:48:00+07:00", 250 | "2048": "2048-04-14T03:36:00+07:00", 251 | "2049": "2049-04-14T09:36:00+07:00", 252 | "2050": "2050-04-14T15:36:00+07:00", 253 | "2051": "2051-04-14T22:24:00+07:00", 254 | "2052": "2052-04-14T04:24:00+07:00", 255 | "2053": "2053-04-14T10:24:00+07:00", 256 | "2054": "2054-04-14T16:24:00+07:00", 257 | "2055": "2055-04-14T23:12:00+07:00", 258 | "2056": "2056-04-14T05:12:00+07:00", 259 | "2057": "2057-04-14T11:12:00+07:00", 260 | "2058": "2058-04-14T17:12:00+07:00", 261 | "2059": "2059-04-14T00:00:00+07:00", 262 | "2060": "2060-04-14T06:00:00+07:00", 263 | "2061": "2061-04-14T12:00:00+07:00", 264 | "2062": "2062-04-14T18:00:00+07:00", 265 | "2063": "2063-04-15T00:48:00+07:00", 266 | "2064": "2064-04-14T06:48:00+07:00", 267 | "2065": "2065-04-14T12:48:00+07:00", 268 | "2066": "2066-04-14T18:48:00+07:00", 269 | "2067": "2067-04-15T01:36:00+07:00", 270 | "2068": "2068-04-14T07:36:00+07:00", 271 | "2069": "2069-04-14T13:36:00+07:00", 272 | "2070": "2070-04-14T19:36:00+07:00", 273 | "2071": "2071-04-15T02:24:00+07:00", 274 | "2072": "2072-04-14T08:24:00+07:00", 275 | "2073": "2073-04-14T14:24:00+07:00", 276 | "2074": "2074-04-14T20:24:00+07:00", 277 | "2075": "2075-04-15T03:12:00+07:00", 278 | "2076": "2076-04-14T09:12:00+07:00", 279 | "2077": "2077-04-14T15:12:00+07:00", 280 | "2078": "2078-04-14T22:00:00+07:00", 281 | "2079": "2079-04-15T04:00:00+07:00", 282 | "2080": "2080-04-14T10:00:00+07:00", 283 | "2081": "2081-04-14T16:00:00+07:00", 284 | "2082": "2082-04-14T22:48:00+07:00", 285 | "2083": "2083-04-15T04:48:00+07:00", 286 | "2084": "2084-04-14T10:48:00+07:00", 287 | "2085": "2085-04-14T16:48:00+07:00", 288 | "2086": "2086-04-14T23:36:00+07:00", 289 | "2087": "2087-04-15T05:36:00+07:00", 290 | "2088": "2088-04-14T11:36:00+07:00", 291 | "2089": "2089-04-14T17:36:00+07:00", 292 | "2090": "2090-04-15T00:48:00+07:00", 293 | "2091": "2091-04-15T06:24:00+07:00", 294 | "2092": "2092-04-14T12:24:00+07:00", 295 | "2093": "2093-04-14T18:24:00+07:00", 296 | "2094": "2094-04-15T01:36:00+07:00", 297 | "2095": "2095-04-15T07:12:00+07:00", 298 | "2096": "2096-04-14T13:12:00+07:00", 299 | "2097": "2097-04-14T19:12:00+07:00", 300 | "2098": "2098-04-15T02:24:00+07:00", 301 | "2099": "2099-04-15T08:00:00+07:00", 302 | "2100": "2100-04-15T14:00:00+07:00", 303 | "2101": "2101-04-15T20:00:00+07:00", 304 | "2102": "2102-04-16T03:12:00+07:00", 305 | "2103": "2103-04-16T08:48:00+07:00", 306 | "2104": "2104-04-15T14:48:00+07:00", 307 | "2105": "2105-04-15T20:48:00+07:00", 308 | "2106": "2106-04-16T04:00:00+07:00", 309 | "2107": "2107-04-16T09:36:00+07:00", 310 | "2108": "2108-04-15T15:36:00+07:00", 311 | "2109": "2109-04-15T22:24:00+07:00", 312 | "2110": "2110-04-16T04:48:00+07:00", 313 | "2111": "2111-04-16T10:24:00+07:00", 314 | "2112": "2112-04-15T16:24:00+07:00", 315 | "2113": "2113-04-15T23:12:00+07:00", 316 | "2114": "2114-04-16T05:36:00+07:00", 317 | "2115": "2115-04-16T11:12:00+07:00", 318 | "2116": "2116-04-15T17:12:00+07:00", 319 | "2117": "2117-04-15T00:00:00+07:00", 320 | "2118": "2118-04-16T06:24:00+07:00", 321 | "2119": "2119-04-16T12:00:00+07:00", 322 | "2120": "2120-04-15T18:00:00+07:00", 323 | "2121": "2121-04-16T01:12:00+07:00", 324 | "2122": "2122-04-16T07:12:00+07:00", 325 | "2123": "2123-04-16T12:48:00+07:00", 326 | "2124": "2124-04-15T18:48:00+07:00", 327 | "2125": "2125-04-16T02:00:00+07:00", 328 | "2126": "2126-04-16T08:00:00+07:00", 329 | "2127": "2127-04-16T13:36:00+07:00", 330 | "2128": "2128-04-15T19:36:00+07:00", 331 | "2129": "2129-04-16T02:48:00+07:00", 332 | "2130": "2130-04-16T08:48:00+07:00", 333 | "2131": "2131-04-16T14:24:00+07:00", 334 | "2132": "2132-04-15T20:24:00+07:00", 335 | "2133": "2133-04-16T03:36:00+07:00", 336 | "2134": "2134-04-16T09:36:00+07:00", 337 | "2135": "2135-04-16T15:12:00+07:00", 338 | "2136": "2136-04-15T22:00:00+07:00", 339 | "2137": "2137-04-16T04:24:00+07:00", 340 | "2138": "2138-04-16T10:24:00+07:00", 341 | "2139": "2139-04-16T16:00:00+07:00", 342 | "2140": "2140-04-15T22:48:00+07:00", 343 | "2141": "2141-04-16T05:12:00+07:00", 344 | "2142": "2142-04-16T11:12:00+07:00", 345 | "2143": "2143-04-16T16:48:00+07:00", 346 | "2144": "2144-04-15T23:36:00+07:00", 347 | "2145": "2145-04-16T06:00:00+07:00", 348 | "2146": "2146-04-16T12:00:00+07:00", 349 | "2147": "2147-04-16T17:36:00+07:00", 350 | "2148": "2148-04-16T00:48:00+07:00", 351 | "2149": "2149-04-16T06:48:00+07:00", 352 | "2150": "2150-04-16T12:48:00+07:00", 353 | "2151": "2151-04-16T18:24:00+07:00", 354 | "2152": "2152-04-16T01:36:00+07:00", 355 | "2153": "2153-04-16T07:36:00+07:00", 356 | "2154": "2154-04-16T13:36:00+07:00", 357 | "2155": "2155-04-16T19:12:00+07:00", 358 | "2156": "2156-04-16T02:24:00+07:00", 359 | "2157": "2157-04-16T08:24:00+07:00", 360 | "2158": "2158-04-16T14:24:00+07:00", 361 | "2159": "2159-04-16T20:00:00+07:00", 362 | "2160": "2160-04-16T03:12:00+07:00", 363 | "2161": "2161-04-16T09:12:00+07:00", 364 | "2162": "2162-04-16T15:12:00+07:00", 365 | "2163": "2163-04-16T20:48:00+07:00", 366 | "2164": "2164-04-16T04:00:00+07:00", 367 | "2165": "2165-04-16T10:00:00+07:00", 368 | "2166": "2166-04-16T16:00:00+07:00", 369 | "2167": "2167-04-16T22:24:00+07:00", 370 | "2168": "2168-04-16T04:48:00+07:00", 371 | "2169": "2169-04-16T10:48:00+07:00", 372 | "2170": "2170-04-16T16:48:00+07:00", 373 | "2171": "2171-04-16T23:12:00+07:00", 374 | "2172": "2172-04-16T05:36:00+07:00", 375 | "2173": "2173-04-16T11:36:00+07:00", 376 | "2174": "2174-04-16T17:36:00+07:00", 377 | "2175": "2175-04-16T00:00:00+07:00", 378 | "2176": "2176-04-16T06:24:00+07:00", 379 | "2177": "2177-04-16T12:24:00+07:00", 380 | "2178": "2178-04-16T18:24:00+07:00", 381 | "2179": "2179-04-17T01:12:00+07:00", 382 | "2180": "2180-04-16T07:12:00+07:00", 383 | "2181": "2181-04-16T13:12:00+07:00", 384 | "2182": "2182-04-16T19:12:00+07:00", 385 | "2183": "2183-04-17T02:00:00+07:00", 386 | "2184": "2184-04-16T08:00:00+07:00", 387 | "2185": "2185-04-16T14:00:00+07:00", 388 | "2186": "2186-04-16T20:00:00+07:00", 389 | "2187": "2187-04-17T02:48:00+07:00", 390 | "2188": "2188-04-16T08:48:00+07:00", 391 | "2189": "2189-04-16T14:48:00+07:00", 392 | "2190": "2190-04-16T20:48:00+07:00", 393 | "2191": "2191-04-17T03:36:00+07:00", 394 | "2192": "2192-04-16T09:36:00+07:00", 395 | "2193": "2193-04-16T15:36:00+07:00", 396 | "2194": "2194-04-16T22:24:00+07:00", 397 | "2195": "2195-04-17T04:24:00+07:00", 398 | "2196": "2196-04-16T10:24:00+07:00", 399 | "2197": "2197-04-16T16:24:00+07:00", 400 | "2198": "2198-04-16T23:12:00+07:00", 401 | "2199": "2199-04-17T05:12:00+07:00", 402 | "2200": "2200-04-17T11:12:00+07:00", 403 | "2201": "2201-04-17T17:12:00+07:00", 404 | "2202": "2202-04-17T00:00:00+07:00", 405 | "2203": "2203-04-18T06:00:00+07:00", 406 | "2204": "2204-04-17T12:00:00+07:00", 407 | "2205": "2205-04-17T18:00:00+07:00", 408 | "2206": "2206-04-18T00:48:00+07:00", 409 | "2207": "2207-04-18T06:48:00+07:00", 410 | "2208": "2208-04-17T12:48:00+07:00", 411 | "2209": "2209-04-17T18:48:00+07:00", 412 | "2210": "2210-04-18T01:36:00+07:00", 413 | "2211": "2211-04-18T07:36:00+07:00", 414 | "2212": "2212-04-17T13:36:00+07:00", 415 | "2213": "2213-04-17T19:36:00+07:00", 416 | "2214": "2214-04-18T02:24:00+07:00", 417 | "2215": "2215-04-18T08:24:00+07:00", 418 | "2216": "2216-04-17T14:24:00+07:00", 419 | "2217": "2217-04-17T20:24:00+07:00", 420 | "2218": "2218-04-18T03:12:00+07:00", 421 | "2219": "2219-04-18T09:12:00+07:00", 422 | "2220": "2220-04-17T15:12:00+07:00", 423 | "2221": "2221-04-17T22:00:00+07:00", 424 | "2222": "2222-04-18T04:00:00+07:00", 425 | "2223": "2223-04-18T10:00:00+07:00", 426 | "2224": "2224-04-17T16:00:00+07:00", 427 | "2225": "2225-04-17T22:48:00+07:00", 428 | "2226": "2226-04-18T04:48:00+07:00", 429 | "2227": "2227-04-18T10:48:00+07:00", 430 | "2228": "2228-04-17T16:48:00+07:00", 431 | "2229": "2229-04-17T23:36:00+07:00", 432 | "2230": "2230-04-18T05:36:00+07:00", 433 | "2231": "2231-04-18T11:36:00+07:00", 434 | "2232": "2232-04-17T17:36:00+07:00", 435 | "2233": "2233-04-18T00:48:00+07:00", 436 | "2234": "2234-04-18T06:24:00+07:00", 437 | "2235": "2235-04-18T12:24:00+07:00", 438 | "2236": "2236-04-17T18:24:00+07:00", 439 | "2237": "2237-04-18T01:36:00+07:00", 440 | "2238": "2238-04-18T07:12:00+07:00", 441 | "2239": "2239-04-18T13:12:00+07:00", 442 | "2240": "2240-04-17T19:12:00+07:00", 443 | "2241": "2241-04-18T02:24:00+07:00", 444 | "2242": "2242-04-18T08:00:00+07:00", 445 | "2243": "2243-04-18T14:00:00+07:00", 446 | "2244": "2244-04-17T20:00:00+07:00", 447 | "2245": "2245-04-18T03:12:00+07:00", 448 | "2246": "2246-04-18T08:48:00+07:00", 449 | "2247": "2247-04-18T14:48:00+07:00", 450 | "2248": "2248-04-17T20:48:00+07:00", 451 | "2249": "2249-04-18T04:00:00+07:00", 452 | "2250": "2250-04-18T09:36:00+07:00", 453 | "2251": "2251-04-18T15:36:00+07:00", 454 | "2252": "2252-04-17T22:24:00+07:00", 455 | "2253": "2253-04-18T04:48:00+07:00", 456 | "2254": "2254-04-18T10:24:00+07:00", 457 | "2255": "2255-04-18T16:24:00+07:00", 458 | "2256": "2256-04-17T23:12:00+07:00", 459 | "2257": "2257-04-18T05:36:00+07:00", 460 | "2258": "2258-04-18T11:12:00+07:00", 461 | "2259": "2259-04-18T17:12:00+07:00", 462 | "2260": "2260-04-17T00:00:00+07:00", 463 | "2261": "2261-04-18T06:24:00+07:00", 464 | "2262": "2262-04-18T12:00:00+07:00", 465 | "2263": "2263-04-18T18:00:00+07:00", 466 | "2264": "2264-04-18T01:12:00+07:00", 467 | "2265": "2265-04-18T07:12:00+07:00", 468 | "2266": "2266-04-18T12:48:00+07:00", 469 | "2267": "2267-04-18T18:48:00+07:00", 470 | "2268": "2268-04-18T02:00:00+07:00", 471 | "2269": "2269-04-18T08:00:00+07:00", 472 | "2270": "2270-04-18T13:36:00+07:00", 473 | "2271": "2271-04-18T19:36:00+07:00", 474 | "2272": "2272-04-18T02:48:00+07:00", 475 | "2273": "2273-04-18T08:48:00+07:00", 476 | "2274": "2274-04-18T14:24:00+07:00", 477 | "2275": "2275-04-18T20:24:00+07:00", 478 | "2276": "2276-04-18T03:36:00+07:00", 479 | "2277": "2277-04-18T09:36:00+07:00", 480 | "2278": "2278-04-18T15:12:00+07:00", 481 | "2279": "2279-04-18T22:00:00+07:00", 482 | "2280": "2280-04-18T04:24:00+07:00", 483 | "2281": "2281-04-18T10:24:00+07:00", 484 | "2282": "2282-04-18T16:00:00+07:00", 485 | "2283": "2283-04-18T22:48:00+07:00", 486 | "2284": "2284-04-18T05:12:00+07:00", 487 | "2285": "2285-04-18T11:12:00+07:00", 488 | "2286": "2286-04-18T16:48:00+07:00", 489 | "2287": "2287-04-18T23:36:00+07:00", 490 | "2288": "2288-04-18T06:00:00+07:00", 491 | "2289": "2289-04-18T12:00:00+07:00", 492 | "2290": "2290-04-18T17:36:00+07:00", 493 | "2291": "2291-04-19T00:48:00+07:00", 494 | "2292": "2292-04-18T06:48:00+07:00", 495 | "2293": "2293-04-18T12:48:00+07:00", 496 | "2294": "2294-04-18T18:24:00+07:00", 497 | "2295": "2295-04-19T01:36:00+07:00", 498 | "2296": "2296-04-18T07:36:00+07:00", 499 | "2297": "2297-04-18T13:36:00+07:00", 500 | "2298": "2298-04-18T19:12:00+07:00", 501 | "2299": "2299-04-19T02:24:00+07:00", 502 | "2300": "2300-04-19T08:24:00+07:00" 503 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🇰🇭 MomentKH - Complete Khmer Calendar Library 2 | 3 | **MomentKH** is a lightweight, zero-dependency JavaScript/TypeScript library for accurate Khmer (Cambodian) Lunar Calendar conversions. It provides a modern, standalone implementation with full TypeScript support. 4 | 5 | [🎮 **Live Demo Playground**](https://thyrithsor.github.io/momentkh/) 6 | 7 | [![Version](https://img.shields.io/badge/version-3.0.2-blue.svg)](https://github.com/ThyrithSor/momentkh) 8 | [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) 9 | [![No Dependencies](https://img.shields.io/badge/dependencies-none-success.svg)](https://github.com/ThyrithSor/momentkh) 10 | 11 | --- 12 | 13 | ## ⚡ TLDR - Quick Start 14 | 15 | ```javascript 16 | // Import 17 | const momentkh = require("@thyrith/momentkh"); 18 | 19 | // Convert date to Khmer format (default) 20 | const khmer = momentkh.fromDate(new Date()); 21 | console.log(momentkh.format(khmer)); 22 | // Output: ថ្ងៃពុធ ១២រោច ខែមិគសិរ ឆ្នាំម្សាញ់ សប្តស័ក ពុទ្ធសករាជ ២៥៦៩ 23 | 24 | // Convert date to Khmer format (custom) 25 | console.log(momentkh.format(khmer, "dN ខែm ឆ្នាំa")); 26 | // Output: ១២រោច ខែមិគសិរ ឆ្នាំម្សាញ់ 27 | 28 | // Convert Khmer date to Gregorian 29 | const gregorian = momentkh.fromKhmer(15, 0, 5, 2568); // 15កើត ខែពិសាខ ព.ស.២៥៦៨ 30 | console.log(gregorian); 31 | // Output: { year: 2025, month: 5, day: 11 } 32 | 33 | // Get Khmer New Year 34 | const newYear = momentkh.getNewYear(2025); 35 | console.log(newYear); 36 | // Output: { year: 2025, month: 4, day: 14, hour: 4, minute: 48 } 37 | ``` 38 | 39 | --- 40 | 41 | ## 📑 Table of Contents 42 | 43 | - [Features](#-features) 44 | - [Installation](#-installation) 45 | - [Quick Start](#-quick-start) 46 | - [API Reference](#-api-reference) 47 | - [fromGregorian()](#fromgregorianyear-month-day-hour-minute-second) 48 | - [fromKhmer()](#fromkhmerday-moonphase-monthindex-beyear) 49 | - [fromDate()](#fromdatedateobject) 50 | - [toDate()](#todateday-moonphase-monthindex-beyear) 51 | - [getNewYear()](#getnewyearyear) 52 | - [format()](#formatkhmerdata-formatstring) 53 | - [Using Enums (NEW in v3.0)](#-using-enums-new-in-v30) 54 | - [Understanding Khmer Calendar](#-understanding-khmer-calendar) 55 | - [Buddhist Era (BE) Year](#buddhist-era-be-year) 56 | - [Animal Year](#animal-year) 57 | - [Sak](#sak-year-sak) 58 | - [When Each Year Type Increases](#when-each-year-type-increases) 59 | - [Format Codes](#-format-codes) 60 | - [Constants](#-constants) 61 | - [Migration Guide](#-migration-guide-from-momentkh-v1) 62 | - [Examples](#-examples) 63 | - [Browser Support](#-browser-support) 64 | 65 | --- 66 | 67 | ## ✨ Features 68 | 69 | - ✅ **Zero Dependencies** - Pure JavaScript, no external libraries required 70 | - ✅ **TypeScript Support** - Full type definitions included for excellent IDE experience 71 | - ✅ **Type-Safe Enums** - NEW in v3.0! Use enums for moonPhase, monthIndex, animalYear, sak, and dayOfWeek 72 | - ✅ **Bidirectional Conversion** - Convert between Gregorian ↔ Khmer Lunar dates 73 | - ✅ **Accurate Calculations** - Based on traditional Khmer astronomical algorithms 74 | - ✅ **Khmer New Year** - Precise calculation of Moha Songkran timing 75 | - ✅ **Flexible Formatting** - Customizable output with format tokens 76 | - ✅ **Universal** - Works in Node.js, Browsers (ES5+), AMD, and ES Modules 77 | - ✅ **Lightweight** - Single file (~36KB), no build step required 78 | - ✅ **Well-Tested** - Comprehensive test suite with 1500+ test cases (100% pass rate) 79 | 80 | --- 81 | 82 | ## 📦 Installation 83 | 84 | ### NPM (Recommended) 85 | 86 | ```bash 87 | npm install @thyrith/momentkh 88 | ``` 89 | 90 | ### TypeScript 91 | 92 | Type definitions are included automatically when you install via NPM. For direct downloads, you can also use `momentkh.ts` or the compiled `.d.ts` files from the `dist/` folder. 93 | 94 | --- 95 | 96 | ## 🚀 Quick Start 97 | 98 | ### Browser (HTML) 99 | 100 | ```html 101 | 102 | 103 | 110 | ``` 111 | 112 | > **Note:** Use `momentkh.js` (UMD bundle) for browsers. The `dist/momentkh.js` is CommonJS format for Node.js. 113 | 114 | ### Node.js (CommonJS) 115 | 116 | ```javascript 117 | // Use the CommonJS module from dist/ 118 | const momentkh = require("@thyrith/momentkh"); 119 | 120 | // Convert specific date 121 | const khmer = momentkh.fromGregorian(2024, 4, 14, 10, 30); 122 | console.log(momentkh.format(khmer)); 123 | 124 | // Get Khmer New Year 125 | const newYear = momentkh.getNewYear(2024); 126 | console.log(newYear); // { year: 2024, month: 4, day: 13, hour: 22, minute: 17 } 127 | ``` 128 | 129 | ### ES Modules 130 | 131 | ```javascript 132 | import momentkh from "@thyrith/momentkh"; 133 | 134 | const khmer = momentkh.fromDate(new Date()); 135 | console.log(momentkh.format(khmer)); 136 | ``` 137 | 138 | ### TypeScript 139 | 140 | Full TypeScript support with complete type definitions and enums: 141 | 142 | ```typescript 143 | import momentkh, { 144 | KhmerConversionResult, 145 | NewYearInfo, 146 | GregorianDate, 147 | MoonPhase, 148 | MonthIndex, 149 | AnimalYear, 150 | Sak, 151 | DayOfWeek, 152 | } from "@thyrith/momentkh"; 153 | 154 | // Convert with full type safety 155 | const khmer: KhmerConversionResult = momentkh.fromGregorian( 156 | 2024, 157 | 4, 158 | 14, 159 | 10, 160 | 30 161 | ); 162 | console.log(momentkh.format(khmer)); 163 | 164 | // Access enum values (NEW in v3.0!) 165 | console.log(khmer.khmer.moonPhase === MoonPhase.Waxing); // Type-safe comparison 166 | console.log(khmer.khmer.monthIndex === MonthIndex.Cheit); // Enum comparison 167 | console.log(khmer.khmer.dayOfWeek === DayOfWeek.Sunday); // Autocomplete support! 168 | 169 | // Reverse conversion with enums (type-safe!) 170 | const gregorianDate: GregorianDate = momentkh.fromKhmer( 171 | 15, 172 | MoonPhase.Waxing, // Use enum instead of 0 173 | MonthIndex.Pisakh, // Use enum instead of 5 174 | 2568 175 | ); 176 | console.log( 177 | `${gregorianDate.year}-${gregorianDate.month}-${gregorianDate.day}` 178 | ); 179 | 180 | // Still supports numbers for backward compatibility 181 | const gregorianDate2: GregorianDate = momentkh.fromKhmer(15, 0, 5, 2568); 182 | 183 | // Get New Year with typed result 184 | const newYear: NewYearInfo = momentkh.getNewYear(2024); 185 | console.log( 186 | `${newYear.year}-${newYear.month}-${newYear.day} ${newYear.hour}:${newYear.minute}` 187 | ); 188 | 189 | // Access constants with full autocomplete 190 | const monthName = momentkh.constants.LunarMonthNames[4]; // "ចេត្រ" 191 | ``` 192 | 193 | **Available Types:** 194 | 195 | - `KhmerConversionResult` - Full conversion result object 196 | - `GregorianDate` - Gregorian date object 197 | - `KhmerDateInfo` - Khmer date information (now with enum fields!) 198 | - `NewYearInfo` - New Year timing information 199 | - `Constants` - Calendar constants interface 200 | 201 | **Available Enums (NEW in v3.0):** 202 | 203 | - 🌙 `MoonPhase` - Waxing (កើត) and Waning (រោច) 204 | - 📅 `MonthIndex` - All 14 Khmer lunar months 205 | - 🐉 `AnimalYear` - All 12 animal years 206 | - ⭐ `Sak` - All 10 Saks 207 | - 📆 `DayOfWeek` - Sunday through Saturday 208 | 209 | --- 210 | 211 | ## 📖 API Reference 212 | 213 | ### `fromGregorian(year, month, day, [hour], [minute], [second])` 214 | 215 | Converts a Gregorian (Western) date to a Khmer Lunar date. 216 | 217 | **Parameters:** 218 | | Parameter | Type | Required | Range | Description | 219 | |-----------|------|----------|-------|-------------| 220 | | `year` | Number | ✅ Yes | Any | 📅 Gregorian year (e.g., 2024) | 221 | | `month` | Number | ✅ Yes | 1-12 | 📅 **1-based** month (1=January, 12=December) | 222 | | `day` | Number | ✅ Yes | 1-31 | 📅 Day of month | 223 | | `hour` | Number | ⚪ No | 0-23 | ⏰ Hour (default: 0) | 224 | | `minute` | Number | ⚪ No | 0-59 | ⏰ Minute (default: 0) | 225 | | `second` | Number | ⚪ No | 0-59 | ⏰ Second (default: 0) | 226 | 227 | **Returns:** Object 228 | 229 | ```javascript 230 | { 231 | gregorian: { 232 | year: 2024, // Number: Gregorian year 233 | month: 4, // Number: Gregorian month (1-12) 234 | day: 14, // Number: Day of month 235 | hour: 10, // Number: Hour (0-23) 236 | minute: 30, // Number: Minute (0-59) 237 | second: 0, // Number: Second (0-59) 238 | dayOfWeek: 0 // Number: 0=Sunday, 1=Monday, ..., 6=Saturday 239 | }, 240 | khmer: { 241 | day: 6, // Number: Lunar day (1-15) 242 | moonPhase: 0, // MoonPhase enum: 0=Waxing (កើត), 1=Waning (រោច) 243 | moonPhaseName: 'កើត', // String: Moon phase name (NEW in v3.0) 244 | monthIndex: 4, // MonthIndex enum: 0-13 (see table below) 245 | monthName: 'ចេត្រ', // String: Khmer month name 246 | beYear: 2568, // Number: Buddhist Era year 247 | jsYear: 1386, // Number: Jolak Sakaraj (Chula Sakaraj) year 248 | animalYear: 4, // AnimalYear enum: 0-11 (NEW in v3.0) 249 | animalYearName: 'រោង', // String: Animal year name 250 | sak: 6, // Sak enum: 0-9 (NEW in v3.0) 251 | sakName: 'ឆស័ក', // String: Sak name 252 | dayOfWeek: 0, // DayOfWeek enum: 0=Sunday, 6=Saturday (NEW in v3.0) 253 | dayOfWeekName: 'អាទិត្យ' // String: Khmer weekday name 254 | }, 255 | _khmerDateObj: KhmerDate // Internal: KhmerDate object (for advanced use) 256 | } 257 | ``` 258 | 259 | **✨ NEW in v3.0:** The `khmer` object now includes both enum values AND string names for easier usage: 260 | 261 | - 🔢 Use enum values (e.g., `moonPhase`, `monthIndex`) for type-safe comparisons 262 | - 📝 Use string names (e.g., `moonPhaseName`, `monthName`) for display purposes 263 | 264 | **Example:** 265 | 266 | ```javascript 267 | const result = momentkh.fromGregorian(2024, 4, 14); 268 | console.log(result.khmer.beYear); // 2567 269 | console.log(result.khmer.monthName); // 'ចេត្រ' 270 | console.log(result.khmer.animalYear); // 4 (រោង) 271 | ``` 272 | 273 | --- 274 | 275 | ### `fromKhmer(day, moonPhase, monthIndex, beYear)` 276 | 277 | Converts a Khmer Lunar date to a Gregorian date. 278 | 279 | **Parameters:** 280 | | Parameter | Type | Required | Range | Description | 281 | |-----------|------|----------|-------|-------------| 282 | | `day` | Number | ✅ Yes | 1-15 | 📅 Lunar day number within the phase | 283 | | `moonPhase` | Number \| MoonPhase | ✅ Yes | 0 or 1 | 🌙 0 = កើត (waxing), 1 = រោច (waning). ✨ NEW: Can use `MoonPhase.Waxing` or `MoonPhase.Waning` | 284 | | `monthIndex` | Number \| MonthIndex | ✅ Yes | 0-13 | 📅 Khmer month index (see table below). ✨ NEW: Can use `MonthIndex` enum | 285 | | `beYear` | Number | ✅ Yes | Any | 🙏 Buddhist Era year (e.g., 2568) | 286 | 287 | **Lunar Month Indices:** 288 | | Index | Khmer Name | Notes | 289 | |-------|------------|-------| 290 | | 0 | មិគសិរ (Migasir) | | 291 | | 1 | បុស្ស (Boss) | | 292 | | 2 | មាឃ (Meak) | | 293 | | 3 | ផល្គុន (Phalkun) | | 294 | | 4 | ចេត្រ (Cheit) | | 295 | | 5 | ពិសាខ (Pisakh) | 🙏 Contains Visakha Bochea (15កើត) | 296 | | 6 | ជេស្ឋ (Jesth) | ➕ Can have leap day (30 days instead of 29) | 297 | | 7 | អាសាឍ (Asadh) | | 298 | | 8 | ស្រាពណ៍ (Srap) | | 299 | | 9 | ភទ្របទ (Phatrabot) | | 300 | | 10 | អស្សុជ (Assoch) | | 301 | | 11 | កត្ដិក (Kadeuk) | | 302 | | 12 | បឋមាសាឍ (Pathamasadh) | 🌟 Only exists in leap month years | 303 | | 13 | ទុតិយាសាឍ (Tutiyasadh) | 🌟 Only exists in leap month years | 304 | 305 | **Returns:** Object 306 | 307 | ```javascript 308 | { 309 | year: 2024, // Number: Gregorian year 310 | month: 4, // Number: Gregorian month (1-12) 311 | day: 14 // Number: Day of month 312 | } 313 | ``` 314 | 315 | **Example:** 316 | 317 | ```javascript 318 | // Using numbers (backward compatible) 319 | const gregorian1 = momentkh.fromKhmer(6, 0, 4, 2568); 320 | console.log(gregorian1); // { year: 2025, month: 4, day: 3 } 321 | 322 | // Using enums (NEW in v3.0 - type-safe!) 323 | const { MoonPhase, MonthIndex } = momentkh; 324 | const gregorian2 = momentkh.fromKhmer( 325 | 6, 326 | MoonPhase.Waxing, 327 | MonthIndex.Cheit, 328 | 2568 329 | ); 330 | console.log(gregorian2); // { year: 2024, month: 4, day: 14 } 331 | 332 | // Mixed: numbers and enums work together 333 | const gregorian3 = momentkh.fromKhmer(15, MoonPhase.Waxing, 5, 2568); 334 | console.log(gregorian3); // Works perfectly! 335 | ``` 336 | 337 | **Important Notes:** 338 | 339 | - 📌 `day` represents the day number within the moon phase (always 1-15) 340 | - 🌙 `moonPhase` 0 = កើត (waxing, days 1-15), 1 = រោច (waning, days 1-14 or 1-15) 341 | - ✨ **NEW:** Use `MoonPhase.Waxing` or `MoonPhase.Waning` for better code readability 342 | - 📅 A full lunar month is typically 29-30 days total 343 | - 💡 Example: "៨រោច" means day=8, moonPhase=1 (or MoonPhase.Waning) 344 | 345 | --- 346 | 347 | ### `fromDate(dateObject)` 348 | 349 | Convenience method to convert a JavaScript `Date` object to Khmer date. 350 | 351 | **Parameters:** 352 | | Parameter | Type | Required | Description | 353 | |-----------|------|----------|-------------| 354 | | `dateObject` | Date | Yes | JavaScript Date object | 355 | 356 | **Returns:** Same object structure as `fromGregorian()` 357 | 358 | **Example:** 359 | 360 | ```javascript 361 | const now = new Date(); 362 | const khmer = momentkh.fromDate(now); 363 | console.log(momentkh.format(khmer)); 364 | ``` 365 | 366 | --- 367 | 368 | ### `toDate(day, moonPhase, monthIndex, beYear)` 369 | 370 | Converts a Khmer Lunar date directly to a JavaScript `Date` object. 371 | 372 | **Parameters:** Same as `fromKhmer()` 373 | 374 | **Returns:** JavaScript `Date` object 375 | 376 | **Example:** 377 | 378 | ```javascript 379 | // Convert 1កើត ខែបុស្ស ព.ស.២៤៤៣ to Date object 380 | const date = momentkh.toDate(1, 0, 1, 2443); 381 | console.log(date); // JavaScript Date for 1900-01-01 382 | ``` 383 | 384 | --- 385 | 386 | ### `getNewYear(year)` 387 | 388 | Calculates the exact date and time of **Moha Songkran** (មហាសង្រ្កាន្ត) - the Khmer New Year - for a given Gregorian year. 389 | 390 | **Parameters:** 391 | | Parameter | Type | Required | Description | 392 | |-----------|------|----------|-------------| 393 | | `year` | Number | Yes | Gregorian year (e.g., 2024) | 394 | 395 | **Returns:** Object 396 | 397 | ```javascript 398 | { 399 | year: 2024, // Number: Gregorian year 400 | month: 4, // Number: Gregorian month (1-12) 401 | day: 13, // Number: Day of month 402 | hour: 22, // Number: Hour (0-23) 403 | minute: 24 // Number: Minute (0-59) 404 | } 405 | ``` 406 | 407 | **Example:** 408 | 409 | ```javascript 410 | const ny2024 = momentkh.getNewYear(2024); 411 | console.log( 412 | `Khmer New Year 2024: ${ny2024.day}/${ny2024.month}/${ny2024.year} at ${ 413 | ny2024.hour 414 | }:${String(ny2024.minute).padStart(2, "0")}` 415 | ); 416 | // Output: Khmer New Year 2024: 13/4/2024 at 22:17 417 | 418 | // Loop through multiple years 419 | for (let year = 2020; year <= 2025; year++) { 420 | const ny = momentkh.getNewYear(year); 421 | console.log( 422 | `${year}: ${ny.day}/${ny.month} ${ny.hour}:${String(ny.minute).padStart( 423 | 2, 424 | "0" 425 | )}` 426 | ); 427 | } 428 | ``` 429 | 430 | --- 431 | 432 | ### `format(khmerData, [formatString])` 433 | 434 | Formats a Khmer date object into a string with optional custom formatting. 435 | 436 | **Parameters:** 437 | | Parameter | Type | Required | Description | 438 | |-----------|------|----------|-------------| 439 | | `khmerData` | Object | Yes | Result from `fromGregorian()` or `fromDate()` | 440 | | `formatString` | String | No | Custom format (see tokens below). If omitted, uses default format | 441 | 442 | **Default Format:** 443 | 444 | ``` 445 | ថ្ងៃ{weekday} {day}{moonPhase} ខែ{month} ឆ្នាំ{animalYear} {sak} ពុទ្ធសករាជ {beYear} 446 | ``` 447 | 448 | **Escaping Characters:** 449 | To escape characters in the format string (so they are not interpreted as format codes), wrap them in square brackets `[]`. 450 | 451 | Example: `[Week] w` -> "Week អា" 452 | 453 | **Returns:** String (formatted Khmer date) 454 | 455 | **Example:** 456 | 457 | ```javascript 458 | const khmer = momentkh.fromGregorian(2024, 4, 14); 459 | 460 | // Default format 461 | console.log(momentkh.format(khmer)); 462 | // ថ្ងៃអាទិត្យ ៦កើត ខែចេត្រ ឆ្នាំរោង ឆស័ក ពុទ្ធសករាជ ២៥៦៨ 463 | 464 | // Custom formats 465 | console.log(momentkh.format(khmer, "dN ថ្ងៃW ខែm")); 466 | // ៦កើត ថ្ងៃអាទិត្យ ខែចេត្រ 467 | 468 | console.log(momentkh.format(khmer, "c/M/D")); 469 | // ២០២៤/មេសា/១៤ 470 | 471 | console.log(momentkh.format(khmer, "ថ្ងៃw dN m ឆ្នាំa e ព.ស.b")); 472 | // ថ្ងៃអា ៦កើត ចេត្រ ឆ្នាំរោង ឆស័ក ព.ស.២៥៦៨ 473 | 474 | // Escaping characters (use brackets []) 475 | console.log(momentkh.format(khmer, "[Day:] d [Month:] m")); 476 | // Day: ៦ Month: ចេត្រ 477 | ``` 478 | 479 | --- 480 | 481 | ## 🔢 Using Enums (NEW in v3.0) 482 | 483 | MomentKH 3.0 introduces TypeScript enums for better type safety and code readability. Use enums instead of magic numbers for clearer, more maintainable code. 484 | 485 | ### Available Enums 486 | 487 | #### 🌙 MoonPhase 488 | 489 | Represents the moon phase in the lunar calendar. 490 | 491 | ```javascript 492 | const { MoonPhase } = momentkh; 493 | 494 | MoonPhase.Waxing; // 0 - 🌒 កើត (waxing moon, days 1-15) 495 | MoonPhase.Waning; // 1 - 🌘 រោច (waning moon, days 1-15) 496 | ``` 497 | 498 | #### 📅 MonthIndex 499 | 500 | All 14 Khmer lunar months (including leap months). 501 | 502 | ```javascript 503 | const { MonthIndex } = momentkh; 504 | 505 | MonthIndex.Migasir; // 0 - មិគសិរ 506 | MonthIndex.Bos; // 1 - បុស្ស 507 | MonthIndex.Meak; // 2 - មាឃ 508 | MonthIndex.Phalkun; // 3 - ផល្គុន 509 | MonthIndex.Cheit; // 4 - ចេត្រ 510 | MonthIndex.Pisakh; // 5 - ពិសាខ 511 | MonthIndex.Jesth; // 6 - ជេស្ឋ 512 | MonthIndex.Asadh; // 7 - អាសាឍ 513 | MonthIndex.Srap; // 8 - ស្រាពណ៍ 514 | MonthIndex.Phatrabot; // 9 - ភទ្របទ 515 | MonthIndex.Assoch; // 10 - អស្សុជ 516 | MonthIndex.Kadeuk; // 11 - កត្ដិក 517 | MonthIndex.Pathamasadh; // 12 - បឋមាសាឍ (leap month only) 518 | MonthIndex.Tutiyasadh; // 13 - ទុតិយាសាឍ (leap month only) 519 | ``` 520 | 521 | #### 🐉 AnimalYear 522 | 523 | The 12 animal years in the zodiac cycle. 524 | 525 | ```javascript 526 | const { AnimalYear } = momentkh; 527 | 528 | AnimalYear.Chhut; // 0 - 🐀 ជូត (Rat) 529 | AnimalYear.Chlov; // 1 - 🐂 ឆ្លូវ (Ox) 530 | AnimalYear.Khal; // 2 - 🐅 ខាល (Tiger) 531 | AnimalYear.Thos; // 3 - 🐇 ថោះ (Rabbit) 532 | AnimalYear.Rong; // 4 - 🐉 រោង (Dragon) 533 | AnimalYear.Masagn; // 5 - 🐍 ម្សាញ់ (Snake) 534 | AnimalYear.Momee; // 6 - 🐎 មមី (Horse) 535 | AnimalYear.Momae; // 7 - 🐐 មមែ (Goat) 536 | AnimalYear.Vok; // 8 - 🐒 វក (Monkey) 537 | AnimalYear.Roka; // 9 - 🐓 រកា (Rooster) 538 | AnimalYear.Cho; // 10 - 🐕 ច (Dog) 539 | AnimalYear.Kor; // 11 - 🐖 កុរ (Pig) 540 | ``` 541 | 542 | #### ⭐ Sak 543 | 544 | The 10 Saks (ស័ក) cycle. 545 | 546 | ```javascript 547 | const { Sak } = momentkh; 548 | 549 | Sak.SamridhiSak; // 0 - 🔟 សំរឹទ្ធិស័ក 550 | Sak.AekSak; // 1 - 1️⃣ ឯកស័ក 551 | Sak.ToSak; // 2 - 2️⃣ ទោស័ក 552 | Sak.TreiSak; // 3 - 3️⃣ ត្រីស័ក 553 | Sak.ChattvaSak; // 4 - 4️⃣ ចត្វាស័ក 554 | Sak.PanchaSak; // 5 - 5️⃣ បញ្ចស័ក 555 | Sak.ChhaSak; // 6 - 6️⃣ ឆស័ក 556 | Sak.SappaSak; // 7 - 7️⃣ សប្តស័ក 557 | Sak.AtthaSak; // 8 - 8️⃣ អដ្ឋស័ក 558 | Sak.NappaSak; // 9 - 9️⃣ នព្វស័ក 559 | ``` 560 | 561 | #### 📆 DayOfWeek 562 | 563 | Days of the week. 564 | 565 | ```javascript 566 | const { DayOfWeek } = momentkh; 567 | 568 | DayOfWeek.Sunday; // 0 - ☀️ អាទិត្យ 569 | DayOfWeek.Monday; // 1 - 🌙 ចន្ទ 570 | DayOfWeek.Tuesday; // 2 - 🔥 អង្គារ 571 | DayOfWeek.Wednesday; // 3 - 🪐 ពុធ 572 | DayOfWeek.Thursday; // 4 - ⚡ ព្រហស្បតិ៍ 573 | DayOfWeek.Friday; // 5 - 💎 សុក្រ 574 | DayOfWeek.Saturday; // 6 - 💀 សៅរ៍ 575 | ``` 576 | 577 | ### Usage Examples 578 | 579 | #### Example 1: Type-Safe Comparisons 580 | 581 | ```javascript 582 | const { MoonPhase, MonthIndex, DayOfWeek } = momentkh; 583 | const khmer = momentkh.fromGregorian(2024, 12, 16); 584 | 585 | // Check moon phase 586 | if (khmer.khmer.moonPhase === MoonPhase.Waxing) { 587 | console.log("Waxing moon (កើត)"); 588 | } else { 589 | console.log("Waning moon (រោច)"); 590 | } 591 | 592 | // Check specific month 593 | if (khmer.khmer.monthIndex === MonthIndex.Migasir) { 594 | console.log("It is Migasir month!"); 595 | } 596 | 597 | // Check day of week 598 | if (khmer.khmer.dayOfWeek === DayOfWeek.Monday) { 599 | console.log("It is Monday!"); 600 | } 601 | ``` 602 | 603 | #### Example 2: Converting with Enums 604 | 605 | ```javascript 606 | const { MoonPhase, MonthIndex } = momentkh; 607 | 608 | // Convert Khmer to Gregorian using enums (much clearer!) 609 | const date1 = momentkh.fromKhmer( 610 | 15, // day 611 | MoonPhase.Waxing, // instead of 0 612 | MonthIndex.Pisakh, // instead of 5 613 | 2568 614 | ); 615 | 616 | // Still works with numbers for backward compatibility 617 | const date2 = momentkh.fromKhmer(15, 0, 5, 2568); 618 | 619 | // Both give the same result 620 | console.log(date1); // { year: 2025, month: 5, day: 11 } 621 | console.log(date2); // { year: 2025, month: 5, day: 11 } 622 | ``` 623 | 624 | #### Example 3: Switch Statements with Enums 625 | 626 | ```javascript 627 | const { MonthIndex, AnimalYear } = momentkh; 628 | const khmer = momentkh.fromGregorian(2024, 12, 16); 629 | 630 | // Switch on month 631 | switch (khmer.khmer.monthIndex) { 632 | case MonthIndex.Migasir: 633 | case MonthIndex.Bos: 634 | case MonthIndex.Meak: 635 | console.log("Winter months"); 636 | break; 637 | case MonthIndex.Phalkun: 638 | case MonthIndex.Cheit: 639 | case MonthIndex.Pisakh: 640 | console.log("Spring months"); 641 | break; 642 | // ... more cases 643 | } 644 | 645 | // Switch on animal year 646 | switch (khmer.khmer.animalYear) { 647 | case AnimalYear.Rong: 648 | console.log("Year of the Dragon!"); 649 | break; 650 | case AnimalYear.Masagn: 651 | console.log("Year of the Snake!"); 652 | break; 653 | // ... more cases 654 | } 655 | ``` 656 | 657 | #### Example 4: TypeScript Benefits 658 | 659 | ```typescript 660 | import momentkh, { MoonPhase, MonthIndex, KhmerConversionResult } from './momentkh'; 661 | 662 | // Full autocomplete and type checking! 663 | const result: KhmerConversionResult = momentkh.fromGregorian(2024, 12, 16); 664 | 665 | // TypeScript knows these are enums 666 | const phase: MoonPhase = result.khmer.moonPhase; 667 | const month: MonthIndex = result.khmer.monthIndex; 668 | 669 | // Type error if you try to use invalid value 670 | // const date = momentkh.fromKhmer(15, 3, 5, 2568); // Error! 3 is not a valid MoonPhase 671 | 672 | // Autocomplete shows all enum options 673 | const date = momentkh.fromKhmer( 674 | 15, 675 | MoonPhase. // ← IDE shows: Waxing, Waning 676 | MonthIndex. // ← IDE shows: Migasir, Bos, Meak, etc. 677 | 2568 678 | ); 679 | ``` 680 | 681 | ### Benefits of Using Enums 682 | 683 | 1. 📖 **Readability**: `MonthIndex.Pisakh` is clearer than `5` 684 | 2. 🛡️ **Type Safety**: TypeScript catches invalid values at compile time 685 | 3. ⚡ **Autocomplete**: IDEs show all available options 686 | 4. 🔧 **Maintainability**: Easier to understand code months later 687 | 5. ♻️ **Refactoring**: Safer to change enum values (single source of truth) 688 | 6. 📚 **Documentation**: Enums serve as inline documentation 689 | 690 | ### 🔄 Backward Compatibility 691 | 692 | ✅ All functions accept both enums and numbers: 693 | 694 | ```javascript 695 | // All of these work: 696 | momentkh.fromKhmer(15, MoonPhase.Waxing, MonthIndex.Pisakh, 2568); // ✨ New enum way 697 | momentkh.fromKhmer(15, 0, MonthIndex.Pisakh, 2568); // 🔀 Mixed 698 | momentkh.fromKhmer(15, MoonPhase.Waxing, 5, 2568); // 🔀 Mixed 699 | momentkh.fromKhmer(15, 0, 5, 2568); // 👍 Old way still works! 700 | ``` 701 | 702 | 🎯 **Existing code using numbers continues to work without changes!** 703 | 704 | --- 705 | 706 | ## 🧮 Understanding Khmer Calendar 707 | 708 | The Khmer calendar is a **lunisolar calendar** that tracks both the moon phases and the solar year. It uses **three different year numbering systems** that change at different times: 709 | 710 | ### Buddhist Era (BE) Year 711 | 712 | **Full Name:** ពុទ្ធសករាជ (Putthsak, Buddhist Era) 713 | **Offset from Gregorian:** +543 or +544 714 | **When it increases:** At midnight (00:00) on the **1st waning day of Pisakh month** (១រោច ខែពិសាខ) 715 | 716 | **Example Timeline:** 717 | 718 | ``` 719 | 2024-05-22 23:59 → 15កើត Pisakh, BE 2567 720 | 2024-05-23 00:00 → 1រោច Pisakh, BE 2568 (NEW year starts!) 721 | 2024-05-23 23:59 → 1រោច Pisakh, BE 2568 722 | 2024-05-24 00:00 → 2រោច Pisakh, BE 2568 723 | ``` 724 | 725 | **Important:** 726 | 727 | - 🙏 The 15th waxing day of Pisakh is **Visakha Bochea** (ពិសាខបូជា), celebrating Buddha's birth, enlightenment, and death 728 | - ⏰ At midnight (00:00) when this sacred day begins, the new BE year starts 729 | - 📍 The year changes exactly at the start of the 15th waxing day of Pisakh 730 | 731 | **Code Example:** 732 | 733 | ```javascript 734 | // Check BE year transition 735 | const before = momentkh.fromGregorian(2024, 5, 22, 23, 59); // 23:59 on May 22 736 | const at = momentkh.fromGregorian(2024, 5, 23, 0, 0); // Midnight on May 23 737 | 738 | console.log(before.khmer.beYear); // 2567 (old year) 739 | console.log(at.khmer.beYear); // 2568 (new year starts at midnight!) 740 | ``` 741 | 742 | --- 743 | 744 | ### Animal Year 745 | 746 | **Full Name:** ឆ្នាំ + Animal name (Year of the [Animal]) 747 | **Cycle:** 12 years 748 | **When it increases:** At the exact moment of **Moha Songkran** (មហាសង្រ្កាន្ត) - Khmer New Year 749 | 750 | **The 12 Animals (in order):** 751 | | Index | Khmer | Pronunciation | Animal | Emoji | 752 | |-------|-------|---------------|--------|-------| 753 | | 0 | ជូត | Chhūt | Rat | 🐀 | 754 | | 1 | ឆ្លូវ | Chhlūv | Ox | 🐂 | 755 | | 2 | ខាល | Khāl | Tiger | 🐅 | 756 | | 3 | ថោះ | Thaŏh | Rabbit | 🐇 | 757 | | 4 | រោង | Rōng | Dragon | 🐉 | 758 | | 5 | ម្សាញ់ | Msanh | Snake | 🐍 | 759 | | 6 | មមី | Momi | Horse | 🐎 | 760 | | 7 | មមែ | Momè | Goat | 🐐 | 761 | | 8 | វក | Vŏk | Monkey | 🐒 | 762 | | 9 | រកា | Rŏka | Rooster | 🐓 | 763 | | 10 | ច | Châ | Dog | 🐕 | 764 | | 11 | កុរ | Kŏr | Pig | 🐖 | 765 | 766 | **Example Timeline:** 767 | 768 | ``` 769 | 2024-04-13 22:23 → Cheit month, BE 2567, Animal Year: វក (Monkey) 770 | 2024-04-13 22:24 → Cheit month, BE 2567, Animal Year: រកា (Rooster) ← NEW YEAR! 771 | 2024-04-13 22:25 → Cheit month, BE 2567, Animal Year: រកា (Rooster) 772 | ``` 773 | 774 | **Code Example:** 775 | 776 | ```javascript 777 | const ny = momentkh.getNewYear(2024); 778 | console.log(ny); // { year: 2024, month: 4, day: 13, hour: 22, minute: 24 } 779 | 780 | // Just before New Year 781 | const before = momentkh.fromGregorian(2024, 4, 13, 22, 23); 782 | console.log(before.khmer.animalYear); // 'វក' (Monkey) 783 | 784 | // Right at New Year 785 | const at = momentkh.fromGregorian(2024, 4, 13, 22, 24); 786 | console.log(at.khmer.animalYear); // 'រកា' (Rooster) - Changed! 787 | ``` 788 | 789 | --- 790 | 791 | ### Sak 792 | 793 | **Full Name:** ស័ក (Sak, Era) 794 | **Cycle:** 10 years 795 | **When it increases:** At **midnight (00:00) of the last day** of Khmer New Year celebration (Lerng Sak - ថ្ងៃឡើងស័ក) 796 | 797 | **The 10 Saks (in order):** 798 | | Index | Khmer | Romanization | 799 | |-------|-------|--------------| 800 | | 0 | សំរឹទ្ធិស័ក | Samridhi Sak | 801 | | 1 | ឯកស័ក | Aek Sak | 802 | | 2 | ទោស័ក | To Sak | 803 | | 3 | ត្រីស័ក | Trei Sak | 804 | | 4 | ចត្វាស័ក | Chattva Sak | 805 | | 5 | បញ្ចស័ក | Pañcha Sak | 806 | | 6 | ឆស័ក | Chha Sak | 807 | | 7 | សប្តស័ក | Sapta Sak | 808 | | 8 | អដ្ឋស័ក | Attha Sak | 809 | | 9 | នព្វស័ក | Nappa Sak | 810 | 811 | **New Year Celebration Days:** 812 | 813 | - 🎉 **Day 1:** Moha Songkran (មហាសង្រ្កាន្ត) - New Year's Day 814 | - 🎊 **Day 2:** Virak Wanabat (វីរៈវ័នបត) - Second day 815 | - ⭐ **Day 3 or 4:** Lerng Sak (ថ្ងៃឡើងស័ក) - Last day & Sak change day 816 | 817 | **Example:** 818 | 819 | ```javascript 820 | // 2024 New Year is on April 13, 22:24 821 | // Lerng Sak (Sak change) is typically 3-4 days later at midnight 822 | 823 | const newYearDay = momentkh.fromGregorian(2024, 4, 13, 23, 0); 824 | console.log(newYearDay.khmer.sak); // 'ឆស័ក' (still old sak) 825 | 826 | const lerngSakDay = momentkh.fromGregorian(2024, 4, 17, 0, 0); // Midnight of Lerng Sak 827 | console.log(lerngSakDay.khmer.sak); // 'សប្តស័ក' (new sak!) 828 | ``` 829 | 830 | --- 831 | 832 | ### When Each Year Type Increases 833 | 834 | **Summary Table:** 835 | 836 | | Year Type | Changes At | Example Date/Time | 837 | | --------------- | ------------------------ | -------------------- | 838 | | **BE Year** | 00:00 នៅថ្ងៃ១រោច ខែពិសាខ | May 23, 2024 00:00 | 839 | | **Animal Year** | ម៉ោង និង នាទីទេវតាចុះ | April 13, 2024 22:17 | 840 | | **Sak** | 00:00 នៅថ្ងៃឡើងស័ក | April 16, 2024 00:00 | 841 | 842 | **Visual Timeline for 2024:** 843 | 844 | ``` 845 | April 13, 22:23 → BE 2567, Monkey (វក), Old Sak (ឆស័ក) 846 | April 13, 22:24 → BE 2567, Rooster (រកា), Old Sak (ឆស័ក) ← Animal Year changes 847 | April 17, 00:00 → BE 2567, Rooster (រកា), New Sak (សប្តស័ក) ← Sak changes 848 | May 22, 23:59 → BE 2567, Rooster (រកា), New Sak (សប្តស័ក) 849 | May 23, 00:00 → BE 2568, Rooster (រកា), New Sak (សប្តស័ក) ← BE Year changes 850 | ``` 851 | 852 | --- 853 | 854 | ## 🎨 Format Codes 855 | 856 | Complete list of format tokens for the `format()` function: 857 | 858 | | Token | Output | Description | Example | 859 | | ---------------------- | ----------------- | ------------------------------ | --------------------- | 860 | | **📅 Date Components** | 861 | | `W` | ថ្ងៃនៃសប្តាហ៍ពេញ | Weekday name (full) | អាទិត្យ, ចន្ទ, អង្គារ | 862 | | `w` | ថ្ងៃនៃសប្តាហ៍ខ្លី | Weekday name (short) | អា, ច, អ | 863 | | `d` | ថ្ងៃទី | Lunar day number | ១, ៥, ១៥ | 864 | | `D` | ថ្ងៃទី (២ខ្ទង់) | Lunar day (zero-padded) | ០១, ០៥, ១៥ | 865 | | **🌙 Moon Phase** | 866 | | `n` | កើត/រោច (ខ្លី) | Moon phase (short) | ក, រ | 867 | | `N` | កើត/រោច (ពេញ) | Moon phase (full) | កើត, រោច | 868 | | `o` | និមិត្តសញ្ញា | Moon day symbol | ᧡, ᧢, ᧣ ... ᧿ | 869 | | **📆 Month Names** | 870 | | `m` | ខែចន្ទគតិ | Lunar month name | មិគសិរ, បុស្ស, ចេត្រ | 871 | | `ms` | ខែ (សង្ខេប) | Lunar month name (abbreviated) | មិ, បុ | 872 | | `Ms` | ខែ (សង្ខេប) | Solar month name (abbreviated) | មក, កម | 873 | | `M` | ខែសុរិយគតិ | Solar month name | មករា, កុម្ភៈ, មេសា | 874 | | **⏰ Year Components** | 875 | | `a` | ឆ្នាំសត្វ | Animal year | ជូត, ឆ្លូវ, រោង | 876 | | `as` | ឆ្នាំ (រូប) | Animal year emoji | 🐀, 🐂, 🐉 | 877 | | `e` | ស័ក | Sak | ឯកស័ក, ទោស័ក | 878 | | `b` | ព.ស. | Buddhist Era year | ២៥៦៨ | 879 | | `br` | BE | Buddhist Era year (Latin) | 2568 | 880 | | `c` | គ.ស. | Common Era (Gregorian) year | ២០២៤ | 881 | | `cr` | CE | Common Era year (Latin) | 2024 | 882 | | `j` | ច.ស. | Jolak Sakaraj year | ១៣៨៦ | 883 | | `jr` | JS | Jolak Sakaraj year (Latin) | 1386 | 884 | | **📅 Day Components** | 885 | | `d` | ថ្ងៃទី | Day of month | ១, ២, ១៤ | 886 | | `dr` | Day | Day of month (Latin) | 1, 2, 14 | 887 | | `D` | ថ្ងៃទី (មាន០) | Day of month (padded) | ០១, ០២, ១៤ | 888 | | `Dr` | Day (0) | Day of month (padded Latin) | 01, 02, 14 | 889 | | `W` | ថ្ងៃនៃសប្តាហ៍ | Day of week (full) | អាទិត្យ, ចន្ទ | 890 | | `w` | ថ្ងៃ (សង្ខេប) | Day of week (short) | អា, ច, អ | 891 | 892 | **Format Examples:** 893 | 894 | ```javascript 895 | const khmer = momentkh.fromGregorian(2024, 4, 14); 896 | 897 | console.log(momentkh.format(khmer, "W, dN ខែm ព.ស.b")); 898 | // អាទិត្យ, ៦កើត ខែចេត្រ ព.ស.២៥៦៨ 899 | 900 | console.log(momentkh.format(khmer, "c/M/D ថ្ងៃw")); 901 | // ២០២៤/មេសា/១៤ ថ្ងៃអា 902 | 903 | console.log(momentkh.format(khmer, "ឆ្នាំa e ខែm ថ្ងៃទីd មានព្រះចន្ទN")); 904 | // ឆ្នាំរោង ឆស័ក ខែចេត្រ ថ្ងៃទី៦ មានព្រះចន្ទកើត 905 | 906 | console.log(momentkh.format(khmer, "ថ្ងៃទី o")); 907 | // ថ្ងៃទី ᧦ (moon symbol for day 6 waxing) 908 | ``` 909 | 910 | --- 911 | 912 | ## 📚 Constants 913 | 914 | Access Khmer calendar constants through `momentkh.constants`: 915 | 916 | **✨ NEW in v3.0:** For type-safe access, use the enums instead! See [🔢 Using Enums](#-using-enums-new-in-v30) section. 917 | 918 | ```javascript 919 | // Lunar month names array (indices 0-13) 920 | momentkh.constants.LunarMonthNames; 921 | // ['មិគសិរ', 'បុស្ស', 'មាឃ', 'ផល្គុន', 'ចេត្រ', 'ពិសាខ', 'ជេស្ឋ', 'អាសាឍ', 922 | // 'ស្រាពណ៍', 'ភទ្របទ', 'អស្សុជ', 'កត្ដិក', 'បឋមាសាឍ', 'ទុតិយាសាឍ'] 923 | 924 | // Solar month names array (indices 0-11) 925 | momentkh.constants.SolarMonthNames; 926 | // ['មករា', 'កុម្ភៈ', 'មីនា', 'មេសា', 'ឧសភា', 'មិថុនា', 927 | // 'កក្កដា', 'សីហា', 'កញ្ញា', 'តុលា', 'វិច្ឆិកា', 'ធ្នូ'] 928 | 929 | // Animal year names array (indices 0-11) 930 | momentkh.constants.AnimalYearNames; 931 | // ['ជូត', 'ឆ្លូវ', 'ខាល', 'ថោះ', 'រោង', 'ម្សាញ់', 932 | // 'មមី', 'មមែ', 'វក', 'រកា', 'ច', 'កុរ'] 933 | 934 | // Animal year emojis array (indices 0-11) 935 | momentkh.constants.AnimalYearEmojis; 936 | // ['🐀', '🐂', '🐅', '🐇', '🐉', '🐍', 937 | // '🐎', '🐐', '🐒', '🐓', '🐕', '🐖'] 938 | 939 | // Sak names array (indices 0-9) 940 | momentkh.constants.SakNames; 941 | // ['សំរឹទ្ធិស័ក', 'ឯកស័ក', 'ទោស័ក', 'ត្រីស័ក', 'ចត្វាស័ក', 942 | // 'បញ្ចស័ក', 'ឆស័ក', 'សប្តស័ក', 'អដ្ឋស័ក', 'នព្វស័ក'] 943 | 944 | // Weekday names array (indices 0-6, Sunday-Saturday) 945 | momentkh.constants.WeekdayNames; 946 | // ['អាទិត្យ', 'ចន្ទ', 'អង្គារ', 'ពុធ', 'ព្រហស្បតិ៍', 'សុក្រ', 'សៅរ៍'] 947 | 948 | // Moon phase names array (indices 0-1) 949 | momentkh.constants.MoonPhaseNames; 950 | // ['កើត', 'រោច'] 951 | ``` 952 | 953 | **Usage Example:** 954 | 955 | ```javascript 956 | // Get month name by index 957 | const monthName = momentkh.constants.LunarMonthNames[4]; 958 | console.log(monthName); // 'ចេត្រ' 959 | 960 | // Loop through all animal years 961 | momentkh.constants.AnimalYearNames.forEach((animal, index) => { 962 | console.log(`${index}: ${animal}`); 963 | }); 964 | ``` 965 | 966 | --- 967 | 968 | ## 🔄 Migration Guide from MomentKH v1 969 | 970 | If you're using the original `momentkh` library (v1) that extends moment.js, here's how to migrate: 971 | 972 | ### Installation Changes 973 | 974 | **Before (v1):** 975 | 976 | ```bash 977 | npm install moment --save 978 | npm install @thyrith/momentkh --save 979 | ``` 980 | 981 | **After (v2):** 982 | 983 | ```bash 984 | # Just download momentkh.js - no npm dependencies! 985 | ``` 986 | 987 | ### Import Changes 988 | 989 | **Before (v1):** 990 | 991 | ```javascript 992 | const moment = require("moment"); 993 | require("@thyrith/momentkh")(moment); 994 | ``` 995 | 996 | **After (v2):** 997 | 998 | ```javascript 999 | const momentkh = require("@thyrith/momentkh"); 1000 | ``` 1001 | 1002 | ### API Migration 1003 | 1004 | #### Converting Today's Date 1005 | 1006 | **Before (v1):** 1007 | 1008 | ```javascript 1009 | const moment = require("moment"); 1010 | require("@thyrith/momentkh")(moment); 1011 | 1012 | const today = moment(); 1013 | const khmerDate = today.toKhDate(); 1014 | console.log(khmerDate); 1015 | ``` 1016 | 1017 | **After (v2):** 1018 | 1019 | ```javascript 1020 | const momentkh = require("@thyrith/momentkh"); 1021 | 1022 | const today = new Date(); 1023 | const khmer = momentkh.fromDate(today); 1024 | const khmerDate = momentkh.format(khmer); 1025 | console.log(khmerDate); 1026 | ``` 1027 | 1028 | #### Converting Specific Date 1029 | 1030 | **Before (v1):** 1031 | 1032 | ```javascript 1033 | const m = moment("2024-04-14", "YYYY-MM-DD"); 1034 | console.log(m.toKhDate()); 1035 | ``` 1036 | 1037 | **After (v2):** 1038 | 1039 | ```javascript 1040 | const khmer = momentkh.fromGregorian(2024, 4, 14); 1041 | console.log(momentkh.format(khmer)); 1042 | ``` 1043 | 1044 | #### Getting Khmer Day/Month/Year 1045 | 1046 | **Before (v1):** 1047 | 1048 | ```javascript 1049 | const m = moment(); 1050 | console.log(m.khDay()); // Day index (0-29) 1051 | console.log(m.khMonth()); // Month index (0-13) 1052 | console.log(m.khYear()); // BE year 1053 | ``` 1054 | 1055 | **After (v2):** 1056 | 1057 | ```javascript 1058 | const khmer = momentkh.fromDate(new Date()); 1059 | console.log(khmer._khmerDateObj.getDayNumber()); // Day number (0-29) 1060 | console.log(khmer.khmer.monthIndex); // Month index (0-13) 1061 | console.log(khmer.khmer.beYear); // BE year 1062 | 1063 | // Or access individual components 1064 | console.log(khmer.khmer.day); // Day in phase (1-15) 1065 | console.log(khmer.khmer.moonPhase); // 0=កើត, 1=រោច 1066 | ``` 1067 | 1068 | #### Custom Formatting 1069 | 1070 | **Before (v1):** 1071 | 1072 | ```javascript 1073 | const m = moment("1992-03-04", "YYYY-MM-DD"); 1074 | console.log(m.toLunarDate("dN ថ្ងៃW ខែm ព.ស. b")); 1075 | // ៦កើត ថ្ងៃព្រហស្បតិ៍ ខែមិគសិរ ព.ស. ២៥៦២ 1076 | ``` 1077 | 1078 | **After (v2):** 1079 | 1080 | ```javascript 1081 | const khmer = momentkh.fromGregorian(1992, 3, 4); 1082 | console.log(momentkh.format(khmer, "dN ថ្ងៃW ខែm ព.ស. b")); 1083 | // ៦កើត ថ្ងៃព្រហស្បតិ៍ ខែមិគសិរ ព.ស. ២៥៣៥ 1084 | ``` 1085 | 1086 | #### Getting Khmer New Year 1087 | 1088 | **Before (v1):** 1089 | 1090 | ```javascript 1091 | const nyMoment = moment.getKhNewYearMoment(2024); 1092 | console.log(nyMoment.format("YYYY-MM-DD HH:mm")); 1093 | ``` 1094 | 1095 | **After (v2):** 1096 | 1097 | ```javascript 1098 | const ny = momentkh.getNewYear(2024); 1099 | console.log(`${ny.year}-${ny.month}-${ny.day} ${ny.hour}:${ny.minute}`); 1100 | ``` 1101 | 1102 | ### Feature Comparison 1103 | 1104 | | Feature | MomentKH v1 | MomentKH v3 | 1105 | | --------------------- | -------------------------- | -------------------------- | 1106 | | **Dependencies** | Requires moment.js (~50KB) | Zero dependencies | 1107 | | **File Size** | Multiple files | Single file (~35KB) | 1108 | | **Setup** | Initialize with moment | Direct import/require | 1109 | | **API Style** | Extends moment.js | Standalone functions | 1110 | | **Khmer → Gregorian** | ❌ Not supported | ✅ Fully supported | 1111 | | **Browser Support** | Modern browsers | ES5+ (IE11+) | 1112 | | **TypeScript** | No types | ✅ Full TypeScript support | 1113 | 1114 | ### Quick Reference Table 1115 | 1116 | | Task | MomentKH v1 | MomentKH v3 | 1117 | | ------------------ | --------------------------------- | ------------------------------------------------------------ | 1118 | | Convert to Khmer | `moment().toKhDate()` | `momentkh.format(momentkh.fromDate(new Date()))` | 1119 | | Get BE year | `moment().khYear()` | `momentkh.fromDate(new Date()).khmer.beYear` | 1120 | | Get month | `moment().khMonth()` | `momentkh.fromDate(new Date()).khmer.monthIndex` | 1121 | | Get day number | `moment().khDay()` | `momentkh.fromDate(new Date())._khmerDateObj.getDayNumber()` | 1122 | | Custom format | `moment().toLunarDate('format')` | `momentkh.format(khmer, 'format')` | 1123 | | New Year | `moment.getKhNewYearMoment(year)` | `momentkh.getNewYear(year)` | 1124 | | Reverse conversion | ❌ Not available | `momentkh.fromKhmer(day, phase, month, year)` | 1125 | 1126 | --- 1127 | 1128 | ## 💡 Examples 1129 | 1130 | ### Example 1: Display Today's Date in Khmer 1131 | 1132 | ```javascript 1133 | const today = momentkh.fromDate(new Date()); 1134 | console.log(momentkh.format(today)); 1135 | // ថ្ងៃសុក្រ ១០កើត ខែចេត្រ ឆ្នាំរោង ឆស័ក ពុទ្ធសករាជ ២៥៦៨ 1136 | ``` 1137 | 1138 | ### Example 2: Convert Specific Date 1139 | 1140 | ```javascript 1141 | // Convert April 14, 2024 1142 | const khmer = momentkh.fromGregorian(2024, 4, 14); 1143 | 1144 | console.log( 1145 | "Gregorian:", 1146 | `${khmer.gregorian.day}/${khmer.gregorian.month}/${khmer.gregorian.year}` 1147 | ); 1148 | console.log("BE Year:", khmer.khmer.beYear); 1149 | console.log("Animal Year:", khmer.khmer.animalYear); 1150 | console.log("Sak:", khmer.khmer.sak); 1151 | console.log("Month:", khmer.khmer.monthName); 1152 | console.log( 1153 | "Day:", 1154 | khmer.khmer.day + (khmer.khmer.moonPhase === 0 ? "កើត" : "រោច") 1155 | ); 1156 | 1157 | // Output: 1158 | // Gregorian: 14/4/2024 1159 | // BE Year: 2568 1160 | // Animal Year: រោង 1161 | // Sak: ឆស័ក 1162 | // Month: ចេត្រ 1163 | // Day: 6កើត 1164 | ``` 1165 | 1166 | ### Example 3: Round-Trip Conversion 1167 | 1168 | ```javascript 1169 | // Convert Gregorian to Khmer 1170 | const gregorianDate = { year: 2024, month: 4, day: 14 }; 1171 | const khmer = momentkh.fromGregorian( 1172 | gregorianDate.year, 1173 | gregorianDate.month, 1174 | gregorianDate.day 1175 | ); 1176 | 1177 | console.log( 1178 | "Original:", 1179 | `${gregorianDate.year}-${gregorianDate.month}-${gregorianDate.day}` 1180 | ); 1181 | console.log("Khmer:", momentkh.format(khmer)); 1182 | 1183 | // Convert back to Gregorian 1184 | const backToGregorian = momentkh.fromKhmer( 1185 | khmer.khmer.day, 1186 | khmer.khmer.moonPhase, 1187 | khmer.khmer.monthIndex, 1188 | khmer.khmer.beYear 1189 | ); 1190 | 1191 | console.log( 1192 | "Converted back:", 1193 | `${backToGregorian.year}-${backToGregorian.month}-${backToGregorian.day}` 1194 | ); 1195 | console.log( 1196 | "Match:", 1197 | gregorianDate.year === backToGregorian.year && 1198 | gregorianDate.month === backToGregorian.month && 1199 | gregorianDate.day === backToGregorian.day 1200 | ? "✓" 1201 | : "✗" 1202 | ); 1203 | ``` 1204 | 1205 | ### Example 4: Find All New Years in Range 1206 | 1207 | ```javascript 1208 | console.log("Khmer New Years 2020-2025:\n"); 1209 | 1210 | for (let year = 2020; year <= 2025; year++) { 1211 | const ny = momentkh.getNewYear(year); 1212 | const khmer = momentkh.fromGregorian( 1213 | ny.year, 1214 | ny.month, 1215 | ny.day, 1216 | ny.hour, 1217 | ny.minute 1218 | ); 1219 | 1220 | console.log(`${year} (ឆ្នាំ${khmer.khmer.animalYear}):`); 1221 | console.log(` Date: ${ny.day}/${ny.month}/${ny.year}`); 1222 | console.log(` Time: ${ny.hour}:${String(ny.minute).padStart(2, "0")}`); 1223 | console.log(` Khmer: ${momentkh.format(khmer, "dN ខែm")}\n`); 1224 | } 1225 | ``` 1226 | 1227 | ### Example 5: Calendar Display for a Month 1228 | 1229 | ```javascript 1230 | function displayKhmerMonth(year, month) { 1231 | const daysInMonth = new Date(year, month, 0).getDate(); 1232 | 1233 | console.log(`\nKhmer Calendar for ${year}/${month}:\n`); 1234 | console.log("Gregorian\tKhmer Date"); 1235 | console.log("-".repeat(50)); 1236 | 1237 | for (let day = 1; day <= daysInMonth; day++) { 1238 | const khmer = momentkh.fromGregorian(year, month, day); 1239 | const formatted = momentkh.format(khmer, "dN m"); 1240 | console.log(`${year}/${month}/${day}\t\t${formatted}`); 1241 | } 1242 | } 1243 | 1244 | // Display April 2024 1245 | displayKhmerMonth(2024, 4); 1246 | ``` 1247 | 1248 | ### Example 6: Check BE Year Transition 1249 | 1250 | ```javascript 1251 | // Find the exact moment BE year changes 1252 | const year = 2024; 1253 | 1254 | // Search in May for Visakha Bochea (15កើត Pisakh) 1255 | for (let day = 20; day <= 25; day++) { 1256 | const midnight = momentkh.fromGregorian(year, 5, day, 0, 0); 1257 | 1258 | if ( 1259 | midnight.khmer.day === 15 && 1260 | midnight.khmer.moonPhase === 0 && 1261 | midnight.khmer.monthIndex === 5 1262 | ) { 1263 | const beforeMidnight = momentkh.fromGregorian(year, 5, day - 1, 23, 59); 1264 | 1265 | console.log(`Found Visakha Bochea: ${year}-05-${day}`); 1266 | console.log(`At ${day - 1} 23:59 - BE ${beforeMidnight.khmer.beYear}`); 1267 | console.log(`At ${day} 00:00 - BE ${midnight.khmer.beYear}`); 1268 | console.log( 1269 | `Year changed: ${ 1270 | beforeMidnight.khmer.beYear !== midnight.khmer.beYear ? "YES" : "NO" 1271 | }` 1272 | ); 1273 | } 1274 | } 1275 | ``` 1276 | 1277 | --- 1278 | 1279 | ## 🌐 Browser Support 1280 | 1281 | | Browser | Version | Status | 1282 | | ------- | ------------ | ----------------------------- | 1283 | | Chrome | All versions | ✅ Supported | 1284 | | Firefox | All versions | ✅ Supported | 1285 | | Safari | All versions | ✅ Supported | 1286 | | Edge | All versions | ✅ Supported | 1287 | | IE | 11+ | ✅ Supported (ES5 compatible) | 1288 | | Node.js | 8.0+ | ✅ Supported | 1289 | 1290 | **ES5 Compatibility:** The library is written in ES5-compatible JavaScript and works in older browsers including IE11. 1291 | 1292 | --- 1293 | 1294 | ## 📝 License 1295 | 1296 | MIT License - Same as original momentkh 1297 | 1298 | Copyright (c) 2025 1299 | 1300 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 1301 | 1302 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 1303 | 1304 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. 1305 | 1306 | --- 1307 | 1308 | ## 🙏 Credits & References 1309 | 1310 | - **Original momentkh library** by [Thyrith Sor](https://github.com/ThyrithSor) 1311 | - **Resources:** 1312 | - [CAM-CC: Khmer Calendar](http://www.cam-cc.org) 1313 | - [Dahlina: Khmer New Year Calculation](http://www.dahlina.com/education/khmer_new_year_time.html) 1314 | 1315 | --- 1316 | 1317 | ## 🐛 Bug Reports & Contributing 1318 | 1319 | Found a bug or have a suggestion? Please: 1320 | 1321 | 1. Check existing issues on GitHub 1322 | 2. Run the test suite: `node test_conversion_roundtrip.js` 1323 | 3. Create a detailed bug report with: 1324 | - Input date 1325 | - Expected output 1326 | - Actual output 1327 | - Steps to reproduce 1328 | 1329 | **Running Tests:** 1330 | 1331 | ```bash 1332 | # Run round-trip conversion test (1000 random dates) 1333 | node test_conversion_roundtrip.js 1334 | 1335 | # Run comparison test (compare with momentkh v1) 1336 | node test_comparision2.js 1337 | 1338 | # Run specific date tests 1339 | node test_specific_dates.js 1340 | ``` 1341 | 1342 | --- 1343 | 1344 | ## 📞 Support 1345 | 1346 | - **Issues:** [GitHub Issues](https://github.com/ThyrithSor/momentkh/issues) 1347 | - **Comparison:** Check behavior against original momentkh for compatibility 1348 | - **Contact:** [E-mail](mailto:me@thyrith.com) 1349 | 1350 | --- 1351 | 1352 | **Version:** 3.0.2 1353 | **Last Updated:** December 2025 1354 | -------------------------------------------------------------------------------- /dist/momentkh.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | /** 3 | * MomentKH - Standalone Khmer Calendar Library (TypeScript) 4 | * 5 | * A simplified, standalone library for Khmer calendar conversion 6 | * No dependencies required 7 | * 8 | * Based on: 9 | * - khmer_calendar.cpp implementation 10 | * - Original momentkh library 11 | * 12 | * @version 2.0.0 13 | * @license MIT 14 | */ 15 | Object.defineProperty(exports, "__esModule", { value: true }); 16 | exports.constants = exports.DayOfWeek = exports.Sak = exports.AnimalYear = exports.MonthIndex = exports.MoonPhase = void 0; 17 | exports.fromGregorian = fromGregorian; 18 | exports.fromKhmer = fromKhmer; 19 | exports.getNewYear = getNewYear; 20 | exports.format = format; 21 | exports.fromDate = fromDate; 22 | exports.toDate = toDate; 23 | // ============================================================================ 24 | // Type Definitions 25 | // ============================================================================ 26 | // Enums for better type safety and ease of use 27 | var MoonPhase; 28 | (function (MoonPhase) { 29 | MoonPhase[MoonPhase["Waxing"] = 0] = "Waxing"; 30 | MoonPhase[MoonPhase["Waning"] = 1] = "Waning"; // រោច 31 | })(MoonPhase || (exports.MoonPhase = MoonPhase = {})); 32 | var MonthIndex; 33 | (function (MonthIndex) { 34 | MonthIndex[MonthIndex["Migasir"] = 0] = "Migasir"; 35 | MonthIndex[MonthIndex["Boss"] = 1] = "Boss"; 36 | MonthIndex[MonthIndex["Meak"] = 2] = "Meak"; 37 | MonthIndex[MonthIndex["Phalkun"] = 3] = "Phalkun"; 38 | MonthIndex[MonthIndex["Cheit"] = 4] = "Cheit"; 39 | MonthIndex[MonthIndex["Pisakh"] = 5] = "Pisakh"; 40 | MonthIndex[MonthIndex["Jesth"] = 6] = "Jesth"; 41 | MonthIndex[MonthIndex["Asadh"] = 7] = "Asadh"; 42 | MonthIndex[MonthIndex["Srap"] = 8] = "Srap"; 43 | MonthIndex[MonthIndex["Phatrabot"] = 9] = "Phatrabot"; 44 | MonthIndex[MonthIndex["Assoch"] = 10] = "Assoch"; 45 | MonthIndex[MonthIndex["Kadeuk"] = 11] = "Kadeuk"; 46 | MonthIndex[MonthIndex["Pathamasadh"] = 12] = "Pathamasadh"; 47 | MonthIndex[MonthIndex["Tutiyasadh"] = 13] = "Tutiyasadh"; // ទុតិយាសាឍ 48 | })(MonthIndex || (exports.MonthIndex = MonthIndex = {})); 49 | var AnimalYear; 50 | (function (AnimalYear) { 51 | AnimalYear[AnimalYear["Chhut"] = 0] = "Chhut"; 52 | AnimalYear[AnimalYear["Chlov"] = 1] = "Chlov"; 53 | AnimalYear[AnimalYear["Khal"] = 2] = "Khal"; 54 | AnimalYear[AnimalYear["Thos"] = 3] = "Thos"; 55 | AnimalYear[AnimalYear["Rong"] = 4] = "Rong"; 56 | AnimalYear[AnimalYear["Masagn"] = 5] = "Masagn"; 57 | AnimalYear[AnimalYear["Momee"] = 6] = "Momee"; 58 | AnimalYear[AnimalYear["Momae"] = 7] = "Momae"; 59 | AnimalYear[AnimalYear["Vok"] = 8] = "Vok"; 60 | AnimalYear[AnimalYear["Roka"] = 9] = "Roka"; 61 | AnimalYear[AnimalYear["Cho"] = 10] = "Cho"; 62 | AnimalYear[AnimalYear["Kor"] = 11] = "Kor"; // កុរ - Pig 63 | })(AnimalYear || (exports.AnimalYear = AnimalYear = {})); 64 | var Sak; 65 | (function (Sak) { 66 | Sak[Sak["SamridhiSak"] = 0] = "SamridhiSak"; 67 | Sak[Sak["AekSak"] = 1] = "AekSak"; 68 | Sak[Sak["ToSak"] = 2] = "ToSak"; 69 | Sak[Sak["TreiSak"] = 3] = "TreiSak"; 70 | Sak[Sak["ChattvaSak"] = 4] = "ChattvaSak"; 71 | Sak[Sak["PanchaSak"] = 5] = "PanchaSak"; 72 | Sak[Sak["ChhaSak"] = 6] = "ChhaSak"; 73 | Sak[Sak["SappaSak"] = 7] = "SappaSak"; 74 | Sak[Sak["AtthaSak"] = 8] = "AtthaSak"; 75 | Sak[Sak["NappaSak"] = 9] = "NappaSak"; // នព្វស័ក 76 | })(Sak || (exports.Sak = Sak = {})); 77 | var DayOfWeek; 78 | (function (DayOfWeek) { 79 | DayOfWeek[DayOfWeek["Sunday"] = 0] = "Sunday"; 80 | DayOfWeek[DayOfWeek["Monday"] = 1] = "Monday"; 81 | DayOfWeek[DayOfWeek["Tuesday"] = 2] = "Tuesday"; 82 | DayOfWeek[DayOfWeek["Wednesday"] = 3] = "Wednesday"; 83 | DayOfWeek[DayOfWeek["Thursday"] = 4] = "Thursday"; 84 | DayOfWeek[DayOfWeek["Friday"] = 5] = "Friday"; 85 | DayOfWeek[DayOfWeek["Saturday"] = 6] = "Saturday"; // សៅរ៍ 86 | })(DayOfWeek || (exports.DayOfWeek = DayOfWeek = {})); 87 | // ============================================================================ 88 | // Constants and Locale Data 89 | // ============================================================================ 90 | const LunarMonths = { 91 | 'មិគសិរ': 0, 'បុស្ស': 1, 'មាឃ': 2, 'ផល្គុន': 3, 92 | 'ចេត្រ': 4, 'ពិសាខ': 5, 'ជេស្ឋ': 6, 'អាសាឍ': 7, 93 | 'ស្រាពណ៍': 8, 'ភទ្របទ': 9, 'អស្សុជ': 10, 'កត្ដិក': 11, 94 | 'បឋមាសាឍ': 12, 'ទុតិយាសាឍ': 13 95 | }; 96 | const LunarMonthNames = [ 97 | 'មិគសិរ', 'បុស្ស', 'មាឃ', 'ផល្គុន', 'ចេត្រ', 'ពិសាខ', 98 | 'ជេស្ឋ', 'អាសាឍ', 'ស្រាពណ៍', 'ភទ្របទ', 'អស្សុជ', 'កត្ដិក', 99 | 'បឋមាសាឍ', 'ទុតិយាសាឍ' 100 | ]; 101 | const SolarMonthNames = [ 102 | 'មករា', 'កុម្ភៈ', 'មីនា', 'មេសា', 'ឧសភា', 'មិថុនា', 103 | 'កក្កដា', 'សីហា', 'កញ្ញា', 'តុលា', 'វិច្ឆិកា', 'ធ្នូ' 104 | ]; 105 | const SolarMonthAbbreviationNames = [ 106 | 'មក', 'កម', 'មន', 'មស', 'ឧស', 'មថ', 107 | 'កដ', 'សហ', 'កញ', 'តល', 'វក', 'ធន' 108 | ]; 109 | const LunarMonthAbbreviationNames = [ 110 | 'មិ', 'បុ', 'មា', 'ផល', 'ចេ', 'ពិ', 111 | 'ជេ', 'អា', 'ស្រ', 'ភ', 'អ', 'ក', 112 | 'បឋ', 'ទុតិ' 113 | ]; 114 | const AnimalYearNames = [ 115 | 'ជូត', 'ឆ្លូវ', 'ខាល', 'ថោះ', 'រោង', 'ម្សាញ់', 116 | 'មមី', 'មមែ', 'វក', 'រកា', 'ច', 'កុរ' 117 | ]; 118 | const AnimalYearEmojis = [ 119 | '🐀', '🐂', '🐅', '🐇', '🐉', '🐍', 120 | '🐎', '🐐', '🐒', '🐓', '🐕', '🐖' 121 | ]; 122 | const SakNames = [ 123 | 'សំរឹទ្ធិស័ក', 'ឯកស័ក', 'ទោស័ក', 'ត្រីស័ក', 'ចត្វាស័ក', 124 | 'បញ្ចស័ក', 'ឆស័ក', 'សប្តស័ក', 'អដ្ឋស័ក', 'នព្វស័ក' 125 | ]; 126 | const WeekdayNames = [ 127 | 'អាទិត្យ', 'ចន្ទ', 'អង្គារ', 'ពុធ', 'ព្រហស្បតិ៍', 'សុក្រ', 'សៅរ៍' 128 | ]; 129 | const WeekdayNamesShort = ['អា', 'ច', 'អ', 'ព', 'ព្រ', 'សុ', 'ស']; 130 | const MoonPhaseNames = ['កើត', 'រោច']; 131 | const MoonPhaseShort = ['ក', 'រ']; 132 | const MoonDaySymbols = [ 133 | '᧡', '᧢', '᧣', '᧤', '᧥', '᧦', '᧧', '᧨', '᧩', '᧪', 134 | '᧫', '᧬', '᧭', '᧮', '᧯', '᧱', '᧲', '᧳', '᧴', '᧵', 135 | '᧶', '᧷', '᧸', '᧹', '᧺', '᧻', '᧼', '᧽', '᧾', '᧿' 136 | ]; 137 | const KhmerNumerals = { 138 | '0': '០', '1': '១', '2': '២', '3': '៣', '4': '៤', 139 | '5': '៥', '6': '៦', '7': '៧', '8': '៨', '9': '៩' 140 | }; 141 | // Exceptional New Year moments (cached values for specific years) 142 | const khNewYearMoments = { 143 | '1879': '12-04-1879 11:36', 144 | '1897': '13-04-1897 02:00', 145 | '2011': '14-04-2011 13:12', 146 | '2012': '14-04-2012 19:11', 147 | '2013': '14-04-2013 02:12', 148 | '2014': '14-04-2014 08:07', 149 | '2015': '14-04-2015 14:02', 150 | '2024': '13-04-2024 22:17', 151 | }; 152 | // ============================================================================ 153 | // Utility Functions 154 | // ============================================================================ 155 | function toKhmerNumeral(num) { 156 | return String(num).replace(/\d/g, d => KhmerNumerals[d]); 157 | } 158 | function isGregorianLeapYear(year) { 159 | return (year % 4 === 0 && year % 100 !== 0) || (year % 400 === 0); 160 | } 161 | function getDaysInGregorianMonth(year, month) { 162 | const daysInMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 163 | if (month === 2 && isGregorianLeapYear(year)) { 164 | return 29; 165 | } 166 | return daysInMonth[month - 1]; 167 | } 168 | // Julian Day Number conversion 169 | function gregorianToJulianDay(year, month, day) { 170 | const a = Math.floor((14 - month) / 12); 171 | const y = year + 4800 - a; 172 | const m = month + 12 * a - 3; 173 | return day + Math.floor((153 * m + 2) / 5) + 365 * y + 174 | Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400) - 32045; 175 | } 176 | function julianDayToGregorian(jdn) { 177 | const a = jdn + 32044; 178 | const b = Math.floor((4 * a + 3) / 146097); 179 | const c = a - Math.floor((146097 * b) / 4); 180 | const d = Math.floor((4 * c + 3) / 1461); 181 | const e = c - Math.floor((1461 * d) / 4); 182 | const m = Math.floor((5 * e + 2) / 153); 183 | const day = e - Math.floor((153 * m + 2) / 5) + 1; 184 | const month = m + 3 - 12 * Math.floor(m / 10); 185 | const year = 100 * b + d - 4800 + Math.floor(m / 10); 186 | return { year, month, day }; 187 | } 188 | function getDayOfWeek(year, month, day) { 189 | const jdn = gregorianToJulianDay(year, month, day); 190 | // JDN % 7: where 0=Monday, 1=Tuesday, ..., 6=Sunday 191 | // We want: 0=Sunday, 1=Monday, ..., 6=Saturday 192 | // So we need to convert: (jdn + 1) % 7 193 | return (jdn + 1) % 7; 194 | } 195 | // ============================================================================ 196 | // Input Validation Functions 197 | // ============================================================================ 198 | /** 199 | * Validates Gregorian date parameters 200 | * @throws {Error} If any parameter is invalid 201 | */ 202 | function validateGregorianDate(year, month, day, hour = 0, minute = 0, second = 0) { 203 | // Validate types 204 | if (typeof year !== 'number' || isNaN(year)) { 205 | throw new Error(`Invalid year: ${year}. Year must be a valid number.`); 206 | } 207 | if (typeof month !== 'number' || isNaN(month)) { 208 | throw new Error(`Invalid month: ${month}. Month must be a valid number.`); 209 | } 210 | if (typeof day !== 'number' || isNaN(day)) { 211 | throw new Error(`Invalid day: ${day}. Day must be a valid number.`); 212 | } 213 | if (typeof hour !== 'number' || isNaN(hour)) { 214 | throw new Error(`Invalid hour: ${hour}. Hour must be a valid number.`); 215 | } 216 | if (typeof minute !== 'number' || isNaN(minute)) { 217 | throw new Error(`Invalid minute: ${minute}. Minute must be a valid number.`); 218 | } 219 | if (typeof second !== 'number' || isNaN(second)) { 220 | throw new Error(`Invalid second: ${second}. Second must be a valid number.`); 221 | } 222 | // Validate month range (1-12) 223 | if (month < 1 || month > 12) { 224 | throw new Error(`Invalid month: ${month}. Month must be between 1 and 12.`); 225 | } 226 | // Validate day range for the specific month/year 227 | const daysInMonth = getDaysInGregorianMonth(year, month); 228 | if (day < 1 || day > daysInMonth) { 229 | const monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 230 | 'July', 'August', 'September', 'October', 'November', 'December']; 231 | throw new Error(`Invalid day: ${day}. ${monthNames[month - 1]} ${year} has ${daysInMonth} days.`); 232 | } 233 | // Validate hour (0-23) 234 | if (hour < 0 || hour > 23) { 235 | throw new Error(`Invalid hour: ${hour}. Hour must be between 0 and 23.`); 236 | } 237 | // Validate minute (0-59) 238 | if (minute < 0 || minute > 59) { 239 | throw new Error(`Invalid minute: ${minute}. Minute must be between 0 and 59.`); 240 | } 241 | // Validate second (0-59) 242 | if (second < 0 || second > 59) { 243 | throw new Error(`Invalid second: ${second}. Second must be between 0 and 59.`); 244 | } 245 | } 246 | /** 247 | * Validates Khmer date parameters 248 | * @throws {Error} If any parameter is invalid 249 | */ 250 | function validateKhmerDate(day, moonPhase, monthIndex, beYear) { 251 | // Validate types 252 | if (typeof day !== 'number' || isNaN(day)) { 253 | throw new Error(`Invalid day: ${day}. Day must be a valid number.`); 254 | } 255 | if (typeof moonPhase !== 'number' || isNaN(moonPhase)) { 256 | throw new Error(`Invalid moonPhase: ${moonPhase}. moonPhase must be a valid number.`); 257 | } 258 | if (typeof monthIndex !== 'number' || isNaN(monthIndex)) { 259 | throw new Error(`Invalid monthIndex: ${monthIndex}. monthIndex must be a valid number.`); 260 | } 261 | if (typeof beYear !== 'number' || isNaN(beYear)) { 262 | throw new Error(`Invalid beYear: ${beYear}. beYear must be a valid number.`); 263 | } 264 | // Validate day (1-15) 265 | if (day < 1 || day > 15) { 266 | throw new Error(`Invalid day: ${day}. Lunar day must be between 1 and 15.`); 267 | } 268 | // Validate moonPhase (0 = Waxing, 1 = Waning) 269 | const moonPhaseNum = typeof moonPhase === 'number' ? moonPhase : moonPhase; 270 | if (moonPhaseNum !== 0 && moonPhaseNum !== 1) { 271 | throw new Error(`Invalid moonPhase: ${moonPhase}. moonPhase must be 0 (Waxing/កើត) or 1 (Waning/រោច).`); 272 | } 273 | // Validate monthIndex (0-13) 274 | const monthIndexNum = typeof monthIndex === 'number' ? monthIndex : monthIndex; 275 | if (monthIndexNum < 0 || monthIndexNum > 13) { 276 | throw new Error(`Invalid monthIndex: ${monthIndex}. monthIndex must be between 0 and 13.`); 277 | } 278 | // Validate beYear (reasonable range: 2000-3000) 279 | if (beYear < 2000 || beYear > 3000) { 280 | throw new Error(`Invalid beYear: ${beYear}. beYear must be between 2000 and 3000.`); 281 | } 282 | // Additional validation: check if leap months (12, 13) are used in non-leap years 283 | // This is done in the conversion function since it requires more complex logic 284 | } 285 | /** 286 | * Validates JavaScript Date object 287 | * @throws {Error} If Date object is invalid 288 | */ 289 | function validateDateObject(date) { 290 | if (!(date instanceof Date)) { 291 | throw new Error('Invalid input: Expected a Date object.'); 292 | } 293 | if (isNaN(date.getTime())) { 294 | throw new Error('Invalid Date object: Date is not a valid date.'); 295 | } 296 | } 297 | // ============================================================================ 298 | // Era Conversions 299 | // ============================================================================ 300 | function adToJs(adYear) { 301 | return adYear - 638; 302 | } 303 | function adToBe(adYear) { 304 | return adYear + 544; 305 | } 306 | function beToAd(beYear) { 307 | return beYear - 544; 308 | } 309 | function jsToAd(jsYear) { 310 | return jsYear + 638; 311 | } 312 | function beToJs(beYear) { 313 | return beYear - 1182; 314 | } 315 | function jsToBe(jsYear) { 316 | return jsYear + 1182; 317 | } 318 | // ============================================================================ 319 | // Calendar Calculation Functions 320 | // ============================================================================ 321 | function getAharkun(beYear) { 322 | return Math.floor((beYear * 292207 + 499) / 800) + 4; 323 | } 324 | function getAharkunMod(beYear) { 325 | return (beYear * 292207 + 499) % 800; 326 | } 327 | function getKromthupul(beYear) { 328 | return 800 - getAharkunMod(beYear); 329 | } 330 | function getAvoman(beYear) { 331 | return (getAharkun(beYear) * 11 + 25) % 692; 332 | } 333 | function getBodithey(beYear) { 334 | const aharkun = getAharkun(beYear); 335 | return (Math.floor((aharkun * 11 + 25) / 692) + aharkun + 29) % 30; 336 | } 337 | function isKhmerSolarLeap(beYear) { 338 | return getKromthupul(beYear) <= 207; 339 | } 340 | function isKhmerLeapDayByCalculation(beYear) { 341 | const avoman = getAvoman(beYear); 342 | const isSolarLeap = isKhmerSolarLeap(beYear); 343 | if (avoman === 0 && getAvoman(beYear - 1) === 137) { 344 | return true; 345 | } 346 | else if (isSolarLeap) { 347 | return avoman < 127; 348 | } 349 | else if (avoman === 137 && getAvoman(beYear + 1) === 0) { 350 | return false; 351 | } 352 | else if (avoman < 138) { 353 | return true; 354 | } 355 | return false; 356 | } 357 | function isKhmerLeapMonth(beYear) { 358 | const bodithey = getBodithey(beYear); 359 | const boditheyNextYear = getBodithey(beYear + 1); 360 | if (bodithey === 25 && boditheyNextYear === 5) { 361 | return false; 362 | } 363 | return (bodithey === 24 && boditheyNextYear === 6) || 364 | (bodithey >= 25) || 365 | (bodithey < 6); 366 | } 367 | function getLeapType(beYear) { 368 | if (isKhmerLeapMonth(beYear)) { 369 | return 1; // Leap month (អធិកមាស) 370 | } 371 | else if (isKhmerLeapDayByCalculation(beYear)) { 372 | return 2; // Leap day (ចន្ទ្រាធិមាស) 373 | } 374 | else if (isKhmerLeapMonth(beYear - 1)) { 375 | let previousYear = beYear - 1; 376 | while (true) { 377 | if (isKhmerLeapDayByCalculation(previousYear)) { 378 | return 2; 379 | } 380 | previousYear -= 1; 381 | if (!isKhmerLeapMonth(previousYear)) { 382 | return 0; 383 | } 384 | } 385 | } 386 | return 0; // Regular year 387 | } 388 | function getNumberOfDaysInKhmerMonth(monthIndex, beYear) { 389 | const leapType = getLeapType(beYear); 390 | const idx = typeof monthIndex === 'number' ? monthIndex : monthIndex; 391 | if (idx === MonthIndex.Jesth && leapType === 2) { // ជេស្ឋ with leap day 392 | return 30; 393 | } 394 | if (idx === MonthIndex.Pathamasadh || idx === MonthIndex.Tutiyasadh) { // បឋមាសាឍ, ទុតិយាសាឍ 395 | return leapType === 1 ? 30 : 0; 396 | } 397 | // Alternating pattern: even months = 29 days, odd months = 30 days 398 | // មិគសិរ:29, បុស្ស:30, មាឃ:29, ផល្គុន:30, ចេត្រ:29, ពិសាខ:30, ជេស្ឋ:29, អាសាឍ:30, etc. 399 | return idx % 2 === 0 ? 29 : 30; 400 | } 401 | function getNumberOfDaysInKhmerYear(beYear) { 402 | const leapType = getLeapType(beYear); 403 | if (leapType === 1) 404 | return 384; // Leap month 405 | if (leapType === 2) 406 | return 355; // Leap day 407 | return 354; // Regular 408 | } 409 | function nextMonthOf(monthIndex, beYear) { 410 | const leapType = getLeapType(beYear); 411 | const idx = typeof monthIndex === 'number' ? monthIndex : monthIndex; 412 | if (idx === MonthIndex.Jesth && leapType === 1) { // ជេស្ឋ in leap month year 413 | return MonthIndex.Pathamasadh; // បឋមាសាឍ 414 | } 415 | if (idx === MonthIndex.Kadeuk) 416 | return MonthIndex.Migasir; // កត្ដិក -> មិគសិរ 417 | if (idx === MonthIndex.Pathamasadh) 418 | return MonthIndex.Tutiyasadh; // បឋមាសាឍ -> ទុតិយាសាឍ 419 | if (idx === MonthIndex.Tutiyasadh) 420 | return MonthIndex.Srap; // ទុតិយាសាឍ -> ស្រាពណ៍ 421 | return (idx + 1); 422 | } 423 | function previousMonthOf(monthIndex, beYear) { 424 | const leapType = getLeapType(beYear); 425 | const idx = typeof monthIndex === 'number' ? monthIndex : monthIndex; 426 | if (idx === MonthIndex.Migasir) 427 | return MonthIndex.Kadeuk; // មិគសិរ -> កត្ដិក 428 | if (idx === MonthIndex.Srap && leapType === 1) 429 | return MonthIndex.Tutiyasadh; // ស្រាពណ៍ -> ទុតិយាសាឍ (leap) 430 | if (idx === MonthIndex.Tutiyasadh) 431 | return MonthIndex.Pathamasadh; // ទុតិយាសាឍ -> បឋមាសាឍ 432 | if (idx === MonthIndex.Pathamasadh) 433 | return MonthIndex.Jesth; // បឋមាសាឍ -> ជេស្ឋ 434 | return (idx - 1); 435 | } 436 | // ============================================================================ 437 | // Khmer New Year Calculation (JS Year based) 438 | // ============================================================================ 439 | function getAharkunJs(jsYear) { 440 | const h = jsYear * 292207 + 373; 441 | return Math.floor(h / 800) + 1; 442 | } 443 | function getAvomanJs(jsYear) { 444 | return (getAharkunJs(jsYear) * 11 + 650) % 692; 445 | } 446 | function getKromthupulJs(jsYear) { 447 | return 800 - ((292207 * jsYear + 373) % 800); 448 | } 449 | function getBoditheyJs(jsYear) { 450 | const aharkun = getAharkunJs(jsYear); 451 | const a = 11 * aharkun + 650; 452 | return (aharkun + Math.floor(a / 692)) % 30; 453 | } 454 | function isAdhikameas(jsYear) { 455 | const bodithey = getBoditheyJs(jsYear); 456 | const boditheyNext = getBoditheyJs(jsYear + 1); 457 | if (bodithey === 24 && boditheyNext === 6) 458 | return true; 459 | if (bodithey === 25 && boditheyNext === 5) 460 | return false; 461 | return bodithey > 24 || bodithey < 6; 462 | } 463 | function isChantrathimeas(jsYear) { 464 | const avoman = getAvomanJs(jsYear); 465 | const avomanNext = getAvomanJs(jsYear + 1); 466 | const avomanPrev = getAvomanJs(jsYear - 1); 467 | const isSolarLeap = getKromthupulJs(jsYear) <= 207; 468 | if (avoman === 0 && avomanPrev === 137) 469 | return true; 470 | if (isSolarLeap) 471 | return avoman < 127; 472 | if (avoman === 137 && avomanNext === 0) 473 | return false; 474 | if (!isSolarLeap && avoman < 138) 475 | return true; 476 | if (avomanPrev === 137 && avoman === 0) 477 | return true; 478 | return false; 479 | } 480 | function has366Days(jsYear) { 481 | return getKromthupulJs(jsYear) <= 207; 482 | } 483 | function getSunInfo(jsYear, sotin) { 484 | const infoOfPrevYear = { 485 | kromathopol: getKromthupulJs(jsYear - 1) 486 | }; 487 | // Sun average as Libda 488 | const r2 = 800 * sotin + infoOfPrevYear.kromathopol; 489 | const reasey = Math.floor(r2 / 24350); 490 | const r3 = r2 % 24350; 491 | const angsar = Math.floor(r3 / 811); 492 | const r4 = r3 % 811; 493 | const l1 = Math.floor(r4 / 14); 494 | const libda = l1 - 3; 495 | const sunAverageAsLibda = (30 * 60 * reasey) + (60 * angsar) + libda; 496 | // Left over 497 | const s1 = ((30 * 60 * 2) + (60 * 20)); 498 | let leftOver = sunAverageAsLibda - s1; 499 | if (sunAverageAsLibda < s1) { 500 | leftOver += (30 * 60 * 12); 501 | } 502 | const kaen = Math.floor(leftOver / (30 * 60)); 503 | // Last left over 504 | let rs = -1; 505 | if ([0, 1, 2].includes(kaen)) { 506 | rs = kaen; 507 | } 508 | else if ([3, 4, 5].includes(kaen)) { 509 | rs = (30 * 60 * 6) - leftOver; 510 | } 511 | else if ([6, 7, 8].includes(kaen)) { 512 | rs = leftOver - (30 * 60 * 6); 513 | } 514 | else if ([9, 10, 11].includes(kaen)) { 515 | rs = ((30 * 60 * 11) + (60 * 29) + 60) - leftOver; 516 | } 517 | const lastLeftOver = { 518 | reasey: Math.floor(rs / (30 * 60)), 519 | angsar: Math.floor((rs % (30 * 60)) / 60), 520 | libda: rs % 60 521 | }; 522 | // Khan and pouichalip 523 | let khan, pouichalip; 524 | if (lastLeftOver.angsar >= 15) { 525 | khan = 2 * lastLeftOver.reasey + 1; 526 | pouichalip = 60 * (lastLeftOver.angsar - 15) + lastLeftOver.libda; 527 | } 528 | else { 529 | khan = 2 * lastLeftOver.reasey; 530 | pouichalip = 60 * lastLeftOver.angsar + lastLeftOver.libda; 531 | } 532 | // Chhaya sun 533 | const chhayaSunMap = [ 534 | { multiplicity: 35, chhaya: 0 }, 535 | { multiplicity: 32, chhaya: 35 }, 536 | { multiplicity: 27, chhaya: 67 }, 537 | { multiplicity: 22, chhaya: 94 }, 538 | { multiplicity: 13, chhaya: 116 }, 539 | { multiplicity: 5, chhaya: 129 } 540 | ]; 541 | const chhayaSun = khan <= 5 ? chhayaSunMap[khan] : { multiplicity: 0, chhaya: 134 }; 542 | const q = Math.floor((pouichalip * chhayaSun.multiplicity) / 900); 543 | const pholAsLibda = q + chhayaSun.chhaya; 544 | // Sun inauguration 545 | const sunInaugurationAsLibda = kaen <= 5 546 | ? sunAverageAsLibda - pholAsLibda 547 | : sunAverageAsLibda + pholAsLibda; 548 | return { 549 | sunInaugurationAsLibda, 550 | reasey: Math.floor(sunInaugurationAsLibda / (30 * 60)), 551 | angsar: Math.floor((sunInaugurationAsLibda % (30 * 60)) / 60), 552 | libda: sunInaugurationAsLibda % 60 553 | }; 554 | } 555 | function getNewYearInfo(jsYear) { 556 | const sotins = has366Days(jsYear - 1) 557 | ? [363, 364, 365, 366] 558 | : [362, 363, 364, 365]; 559 | const newYearsDaySotins = sotins.map(sotin => getSunInfo(jsYear, sotin)); 560 | // Find time of new year 561 | let timeOfNewYear = { hour: 0, minute: 0 }; 562 | for (const sotin of newYearsDaySotins) { 563 | if (sotin.angsar === 0) { 564 | const minutes = (24 * 60) - (sotin.libda * 24); 565 | timeOfNewYear = { 566 | hour: Math.floor(minutes / 60) % 24, 567 | minute: minutes % 60 568 | }; 569 | break; 570 | } 571 | } 572 | // Number of Vanabat days 573 | const numberOfVanabatDays = (newYearsDaySotins[0].angsar === 0) ? 2 : 1; 574 | return { 575 | timeOfNewYear, 576 | numberOfVanabatDays, 577 | newYearsDaySotins 578 | }; 579 | } 580 | // ============================================================================ 581 | // Khmer Date Class 582 | // ============================================================================ 583 | class KhmerDate { 584 | constructor(day, moonPhase, monthIndex, beYear) { 585 | this.day = day; 586 | this.moonPhase = moonPhase; 587 | this.monthIndex = monthIndex; 588 | this.beYear = beYear; 589 | } 590 | // Get day number (0-29) - converts from 1-based internal to 0-based external 591 | getDayNumber() { 592 | if (this.moonPhase === MoonPhase.Waxing) { // កើត 593 | return this.day - 1; // day 1-15 → dayNum 0-14 594 | } 595 | else { // រោច 596 | return 15 + (this.day - 1); // day 1-15 → dayNum 15-29 597 | } 598 | } 599 | static fromDayNumber(dayNum) { 600 | // Converts from 0-based dayNum to 1-based day 601 | if (dayNum < 15) { 602 | return { day: dayNum + 1, moonPhase: MoonPhase.Waxing }; // dayNum 0-14 → day 1-15 603 | } 604 | else { 605 | return { day: (dayNum - 15) + 1, moonPhase: MoonPhase.Waning }; // dayNum 15-29 → day 1-15 606 | } 607 | } 608 | addDays(count) { 609 | if (count === 0) 610 | return this; 611 | if (count < 0) 612 | return this.subtractDays(-count); 613 | let result = new KhmerDate(this.day, this.moonPhase, this.monthIndex, this.beYear); 614 | let remaining = count; 615 | while (remaining > 0) { 616 | const daysInMonth = getNumberOfDaysInKhmerMonth(result.monthIndex, result.beYear); 617 | const currentDayNum = result.getDayNumber(); 618 | const daysLeftInMonth = (daysInMonth - 1) - currentDayNum; 619 | if (remaining <= daysLeftInMonth) { 620 | const newDayNum = currentDayNum + remaining; 621 | const newDay = KhmerDate.fromDayNumber(newDayNum); 622 | let newBeYear = result.beYear; 623 | if (result.monthIndex === MonthIndex.Pisakh) { // ពិសាខ 624 | if (result.moonPhase === MoonPhase.Waxing && newDay.moonPhase === MoonPhase.Waning) { 625 | newBeYear++; 626 | } 627 | } 628 | result = new KhmerDate(newDay.day, newDay.moonPhase, result.monthIndex, newBeYear); 629 | remaining = 0; 630 | } 631 | else { 632 | remaining -= (daysLeftInMonth + 1); 633 | const nextMonth = nextMonthOf(result.monthIndex, result.beYear); 634 | const newBeYear = (result.monthIndex === MonthIndex.Cheit) ? result.beYear + 1 : result.beYear; 635 | result = new KhmerDate(1, MoonPhase.Waxing, nextMonth, newBeYear); // Start at 1កើត 636 | } 637 | } 638 | return result; 639 | } 640 | subtractDays(count) { 641 | if (count === 0) 642 | return this; 643 | let result = new KhmerDate(this.day, this.moonPhase, this.monthIndex, this.beYear); 644 | let remaining = count; 645 | while (remaining > 0) { 646 | const currentDayNum = result.getDayNumber(); 647 | if (remaining <= currentDayNum) { 648 | const newDayNum = currentDayNum - remaining; 649 | const newDay = KhmerDate.fromDayNumber(newDayNum); 650 | let newBeYear = result.beYear; 651 | if (result.monthIndex === MonthIndex.Pisakh) { // ពិសាខ 652 | if (result.moonPhase === MoonPhase.Waning && newDay.moonPhase === MoonPhase.Waxing) { 653 | newBeYear--; 654 | } 655 | } 656 | result = new KhmerDate(newDay.day, newDay.moonPhase, result.monthIndex, newBeYear); 657 | remaining = 0; 658 | } 659 | else { 660 | remaining -= (currentDayNum + 1); 661 | const prevMonth = previousMonthOf(result.monthIndex, result.beYear); 662 | const newBeYear = (result.monthIndex === MonthIndex.Pisakh) ? result.beYear - 1 : result.beYear; 663 | const daysInPrevMonth = getNumberOfDaysInKhmerMonth(prevMonth, newBeYear); 664 | const newDay = KhmerDate.fromDayNumber(daysInPrevMonth - 1); 665 | result = new KhmerDate(newDay.day, newDay.moonPhase, prevMonth, newBeYear); 666 | } 667 | } 668 | return result; 669 | } 670 | toString() { 671 | return `${this.day}${MoonPhaseNames[this.moonPhase]} ខែ${LunarMonthNames[this.monthIndex]} ព.ស.${this.beYear}`; 672 | } 673 | } 674 | // ============================================================================ 675 | // Main Conversion Functions 676 | // ============================================================================ 677 | // Helper function to get approximate BE year (like original getMaybeBEYear) 678 | function getMaybeBEYear(year, month) { 679 | // SolarMonth['មេសា'] = 3 (0-based), so month <= 4 (1-based) 680 | if (month <= 4) { 681 | return year + 543; 682 | } 683 | else { 684 | return year + 544; 685 | } 686 | } 687 | // Cache for Pisakha Bochea dates by year 688 | const visakhaBocheaCache = {}; 689 | // Cache for New Year Full Info 690 | const newYearInfoCache = {}; 691 | /** 692 | * Find BE Year transition datetime for a given Gregorian year 693 | * BE year increases on ១រោច ខែពិសាខ (1st waning day of Pisakh = dayNumber 15 of month 5) 694 | * Returns timestamp in milliseconds at midnight of that day 695 | */ 696 | function getPisakhaBochea(year, isSearching = false) { 697 | if (visakhaBocheaCache[year]) { 698 | return visakhaBocheaCache[year]; 699 | } 700 | // Search for 1រោច Pisakh (when BE year changes) - start from April since it typically occurs then 701 | for (let searchMonth = 4; searchMonth <= 6; searchMonth++) { 702 | const daysInMonth = new Date(year, searchMonth, 0).getDate(); 703 | for (let searchDay = 1; searchDay <= daysInMonth; searchDay++) { 704 | // Avoid infinite recursion by using simplified BE year during search 705 | const result = gregorianToKhmerInternal(year, searchMonth, searchDay, 12, 0, 0, true); 706 | if (result.khmer.monthIndex === MonthIndex.Pisakh && result._khmerDateObj.getDayNumber() === 15) { 707 | // Found 1រោច Pisakh - return timestamp at midnight (start of BE year change day) 708 | // BE year changes at 00:00 on this day 709 | const timestamp = new Date(year, searchMonth - 1, searchDay, 0, 0, 0, 0).getTime(); 710 | visakhaBocheaCache[year] = timestamp; 711 | return timestamp; 712 | } 713 | } 714 | } 715 | // Fallback if not found 716 | const fallback = new Date(year, 3, 15, 0, 0, 0, 0).getTime(); 717 | visakhaBocheaCache[year] = fallback; 718 | return fallback; 719 | } 720 | function gregorianToKhmerInternal(year, month, day, hour = 0, minute = 0, second = 0, isSearching = false) { 721 | /** 722 | * This follows the original momentkh algorithm exactly using JDN for tracking 723 | */ 724 | // Epoch: January 1, 1900 = dayNumber 0 (១កើត), month index 1 (បុស្ស) 725 | let epochJdn = gregorianToJulianDay(1900, 1, 1); 726 | const targetJdn = gregorianToJulianDay(year, month, day); 727 | let khmerMonth = 1; // បុស្ស 728 | let khmerDayNumber = 0; // 0-29 format 729 | let diffDays = targetJdn - epochJdn; 730 | // Move epoch by full Khmer years 731 | if (diffDays > 0) { 732 | while (true) { 733 | // Get Gregorian date of current epoch to calculate BE year 734 | const epochGreg = julianDayToGregorian(epochJdn); 735 | // Match original: use epochMoment.clone().add(1, 'year') 736 | const nextYearBE = getMaybeBEYear(epochGreg.year + 1, epochGreg.month); 737 | const daysInNextYear = getNumberOfDaysInKhmerYear(nextYearBE); 738 | if (diffDays > daysInNextYear) { 739 | diffDays -= daysInNextYear; 740 | epochJdn += daysInNextYear; 741 | } 742 | else { 743 | break; 744 | } 745 | } 746 | } 747 | else if (diffDays < 0) { 748 | while (diffDays < 0) { 749 | const epochGreg = julianDayToGregorian(epochJdn); 750 | const currentYearBE = getMaybeBEYear(epochGreg.year, epochGreg.month); 751 | const daysInCurrentYear = getNumberOfDaysInKhmerYear(currentYearBE); 752 | diffDays += daysInCurrentYear; 753 | epochJdn -= daysInCurrentYear; 754 | } 755 | } 756 | // Move epoch by full Khmer months 757 | while (diffDays > 0) { 758 | const epochGreg = julianDayToGregorian(epochJdn); 759 | const currentBE = getMaybeBEYear(epochGreg.year, epochGreg.month); 760 | const daysInMonth = getNumberOfDaysInKhmerMonth(khmerMonth, currentBE); 761 | if (diffDays > daysInMonth) { 762 | diffDays -= daysInMonth; 763 | epochJdn += daysInMonth; 764 | khmerMonth = nextMonthOf(khmerMonth, currentBE); 765 | } 766 | else { 767 | break; 768 | } 769 | } 770 | // Add remaining days 771 | khmerDayNumber = diffDays; 772 | // Fix overflow (e.g., if month has only 29 days but we calculated 30) 773 | const finalBE = getMaybeBEYear(year, month); 774 | const totalDaysInMonth = getNumberOfDaysInKhmerMonth(khmerMonth, finalBE); 775 | if (khmerDayNumber >= totalDaysInMonth) { 776 | khmerDayNumber = khmerDayNumber % totalDaysInMonth; 777 | khmerMonth = nextMonthOf(khmerMonth, finalBE); 778 | } 779 | // Convert dayNumber to day/moonPhase format 780 | const khmerDayInfo = KhmerDate.fromDayNumber(khmerDayNumber); 781 | // Calculate actual BE year 782 | // The BE year changes on ១រោច ខែពិសាខ (1st waning day of Pisakh = dayNumber 15) 783 | // Compare datetime (including hour/minute) against BE year transition datetime 784 | let beYear; 785 | if (isSearching) { 786 | // During search, use simple approximation to avoid recursion 787 | beYear = month <= 4 ? year + 543 : year + 544; 788 | } 789 | else { 790 | // Normal mode: compare against exact BE year transition datetime (1រោច Pisakh at 00:00) 791 | const inputTimestamp = new Date(year, month - 1, day, hour, minute, second).getTime(); 792 | const beYearTransitionTimestamp = getPisakhaBochea(year); 793 | if (inputTimestamp >= beYearTransitionTimestamp) { 794 | // On or after 1រោច Pisakh (new BE year) 795 | beYear = year + 544; 796 | } 797 | else { 798 | // Before 1រោច Pisakh (old BE year) 799 | beYear = year + 543; 800 | } 801 | } 802 | // Calculate additional info 803 | let jsYear = beToJs(beYear); 804 | let animalYearIndex = ((beYear + 4) % 12 + 12) % 12; 805 | // Adjust Era and Animal Year based on Khmer New Year logic 806 | // They should change at New Year, not wait for Pisakha Bochea (which changes BE) 807 | if (!isSearching) { 808 | const newYearInfo = getNewYearFullInfo(year); 809 | const inputTimestamp = new Date(year, month - 1, day, hour, minute, second).getTime(); 810 | const visakhaBocheaTimestamp = getPisakhaBochea(year); 811 | // Animal Year changes at Moha Songkran (exact New Year time) 812 | // Only apply manual increment if we are in the gap between New Year and Pisakha Bochea 813 | // (After Pisakha Bochea, the BE year increments, so the formula based on BE automatically gives the new Animal Year) 814 | if (inputTimestamp >= newYearInfo.newYearMoment.getTime() && inputTimestamp <= visakhaBocheaTimestamp) { 815 | animalYearIndex = (animalYearIndex + 1) % 12; 816 | } 817 | // Era changes at Midnight of Date Lerng Sak (3rd or 4th day of NY) 818 | if (inputTimestamp >= newYearInfo.lerngSakMoment.getTime() && inputTimestamp <= visakhaBocheaTimestamp) { 819 | jsYear++; 820 | } 821 | } 822 | const sakIndex = ((jsYear % 10) + 10) % 10; 823 | const dayOfWeek = getDayOfWeek(year, month, day); 824 | const khmerDate = new KhmerDate(khmerDayInfo.day, khmerDayInfo.moonPhase, khmerMonth, beYear); 825 | return { 826 | gregorian: { year, month, day, hour, minute, second, dayOfWeek }, 827 | khmer: { 828 | day: khmerDayInfo.day, 829 | moonPhase: khmerDayInfo.moonPhase, 830 | moonPhaseName: MoonPhaseNames[khmerDayInfo.moonPhase], 831 | monthIndex: khmerMonth, 832 | monthName: LunarMonthNames[khmerMonth], 833 | beYear: beYear, 834 | jsYear: jsYear, 835 | animalYear: animalYearIndex, 836 | animalYearName: AnimalYearNames[animalYearIndex], 837 | sak: sakIndex, 838 | sakName: SakNames[sakIndex], 839 | dayOfWeek: dayOfWeek, 840 | dayOfWeekName: WeekdayNames[dayOfWeek] 841 | }, 842 | _khmerDateObj: khmerDate 843 | }; 844 | } 845 | function khmerToGregorian(day, moonPhase, monthIndex, beYear) { 846 | // Validate input parameters 847 | validateKhmerDate(day, moonPhase, monthIndex, beYear); 848 | // Convert enums to numbers if needed 849 | const moonPhaseNum = typeof moonPhase === 'number' ? moonPhase : moonPhase; 850 | const monthIndexNum = typeof monthIndex === 'number' ? monthIndex : monthIndex; 851 | // Convert BE year to approximate Gregorian year 852 | const approxYear = beYear - 544; 853 | // Search within a range around the approximate year 854 | // Start from 2 years before to 2 years after to account for calendar differences 855 | const startYear = approxYear - 2; 856 | const endYear = approxYear + 2; 857 | let candidates = []; 858 | // Iterate through Gregorian dates to find all matches 859 | for (let year = startYear; year <= endYear; year++) { 860 | for (let month = 1; month <= 12; month++) { 861 | const daysInMonth = getDaysInGregorianMonth(year, month); 862 | for (let gDay = 1; gDay <= daysInMonth; gDay++) { 863 | // For BE year transition day (1រោច Pisakh) and the day before (15កើត Pisakh), 864 | // check multiple times during the day because BE year can change during this period 865 | const isAroundBEYearChange = monthIndexNum === MonthIndex.Pisakh && 866 | ((day === 15 && moonPhaseNum === MoonPhase.Waxing) || (day === 1 && moonPhaseNum === MoonPhase.Waning)); 867 | const timesToCheck = isAroundBEYearChange 868 | ? [0, 6, 12, 18, 23] // Check at different hours 869 | : [0]; // Normal case: just check at midnight 870 | for (const hour of timesToCheck) { 871 | const khmerResult = gregorianToKhmerInternal(year, month, gDay, hour, 0, 0, false); 872 | // Check if it matches our target 873 | if (khmerResult.khmer.beYear === beYear && 874 | khmerResult.khmer.monthIndex === monthIndexNum && 875 | khmerResult.khmer.day === day && 876 | khmerResult.khmer.moonPhase === moonPhaseNum) { 877 | candidates.push({ year, month, day: gDay }); 878 | break; // Found a match for this date, no need to check other times 879 | } 880 | } 881 | } 882 | } 883 | } 884 | if (candidates.length === 0) { 885 | throw new Error(`Could not find Gregorian date for Khmer date: ${day} ${moonPhaseNum === MoonPhase.Waxing ? 'កើត' : 'រោច'} month ${monthIndexNum} BE ${beYear}`); 886 | } 887 | // If multiple candidates found, prefer closest to approximate year 888 | if (candidates.length > 1) { 889 | // First, try to filter by year distance 890 | const minDistance = Math.min(...candidates.map(c => Math.abs(c.year - approxYear))); 891 | const closestCandidates = candidates.filter(c => Math.abs(c.year - approxYear) === minDistance); 892 | // If we have a unique closest candidate, return it 893 | if (closestCandidates.length === 1) { 894 | return closestCandidates[0]; 895 | } 896 | // If there are ties, prefer the one that matches at noon 897 | const noonMatches = closestCandidates.filter(c => { 898 | const noonCheck = gregorianToKhmerInternal(c.year, c.month, c.day, 12, 0, 0, false); 899 | return noonCheck.khmer.beYear === beYear && 900 | noonCheck.khmer.monthIndex === monthIndexNum && 901 | noonCheck.khmer.day === day && 902 | noonCheck.khmer.moonPhase === moonPhaseNum; 903 | }); 904 | if (noonMatches.length > 0) { 905 | return noonMatches[0]; 906 | } 907 | // Fall back to first closest candidate 908 | return closestCandidates[0]; 909 | } 910 | return candidates[0]; 911 | } 912 | function getNewYearFullInfo(ceYear) { 913 | if (newYearInfoCache[ceYear]) { 914 | return newYearInfoCache[ceYear]; 915 | } 916 | // Calculate using the standard algorithm first to get necessary info (like angsar for numberNewYearDay) 917 | const jsYear = adToJs(ceYear); 918 | let newYearInfo = getNewYearInfo(jsYear); 919 | // Get Lerng Sak info 920 | let bodithey = getBoditheyJs(jsYear); 921 | const isAthikameasPrev = isAdhikameas(jsYear - 1); 922 | const isChantrathimeasPrev = isChantrathimeas(jsYear - 1); 923 | if (isAthikameasPrev && isChantrathimeasPrev) { 924 | bodithey = (bodithey + 1) % 30; 925 | } 926 | // lunar DateLerngSak 927 | const lunarDateLerngSak = { 928 | day: bodithey >= 6 ? bodithey - 1 : bodithey, 929 | month: bodithey >= 6 ? 4 : 5 // ចេត្រ or ពិសាខ 930 | }; 931 | // Number of new year days 932 | const numberNewYearDay = newYearInfo.newYearsDaySotins[0].angsar === 0 ? 4 : 3; 933 | // Use April 17 as epoch and work backwards 934 | const epochLerngSakGreg = { year: ceYear, month: 4, day: 17 }; 935 | // IMPORTANT: prevent recursion by passing isSearching=true (or any flag that skips Era check) 936 | // gregorianToKhmerInternal(..., isSearching=true) uses simplified BE calc and skips Era check 937 | const khEpoch = gregorianToKhmerInternal(ceYear, 4, 17, 12, 0, 0, true)._khmerDateObj; 938 | // Calculate difference 939 | const diffFromEpoch = ((khEpoch.monthIndex - 4) * 29 + khEpoch.getDayNumber()) - 940 | ((lunarDateLerngSak.month - 4) * 29 + lunarDateLerngSak.day); 941 | // Calculate days to subtract 942 | const daysToSubtract = diffFromEpoch + numberNewYearDay - 1; 943 | // Calculate new year date (Moha Songkran) 944 | const epochJdn = gregorianToJulianDay(epochLerngSakGreg.year, epochLerngSakGreg.month, epochLerngSakGreg.day); 945 | let newYearJdn = epochJdn - daysToSubtract; 946 | // Override with cache if available 947 | if (khNewYearMoments[ceYear]) { 948 | const [datePart, timePart] = khNewYearMoments[ceYear].split(' '); 949 | const [d, m, y] = datePart.split('-').map(Number); 950 | const [hr, min] = timePart.split(':').map(Number); 951 | // Update newYearInfo time 952 | newYearInfo.timeOfNewYear = { hour: hr, minute: min }; 953 | // Update JDN based on cached date 954 | newYearJdn = gregorianToJulianDay(y, m, d); 955 | } 956 | const newYearDate = julianDayToGregorian(newYearJdn); 957 | const newYearMoment = new Date(newYearDate.year, newYearDate.month - 1, newYearDate.day, newYearInfo.timeOfNewYear.hour, newYearInfo.timeOfNewYear.minute); 958 | // Calculate Lerng Sak Date (Midnight) 959 | // Lerng Sak is the last day of NY celebration. 960 | // Jdn = newYearJdn + (numberNewYearDay - 1) 961 | const lerngSakJdn = newYearJdn + numberNewYearDay - 1; 962 | const lerngSakDate = julianDayToGregorian(lerngSakJdn); 963 | const lerngSakMoment = new Date(lerngSakDate.year, lerngSakDate.month - 1, lerngSakDate.day, 0, 0, 0); // Midnight 964 | const result = { 965 | newYearMoment, 966 | lerngSakMoment, 967 | newYearInfo: { 968 | year: newYearDate.year, 969 | month: newYearDate.month, 970 | day: newYearDate.day, 971 | hour: newYearInfo.timeOfNewYear.hour, 972 | minute: newYearInfo.timeOfNewYear.minute 973 | } 974 | }; 975 | newYearInfoCache[ceYear] = result; 976 | return result; 977 | } 978 | function getKhmerNewYear(ceYear) { 979 | const info = getNewYearFullInfo(ceYear); 980 | return info.newYearInfo; 981 | } 982 | // ============================================================================ 983 | // Formatting Functions 984 | // ============================================================================ 985 | function formatKhmer(khmerData, formatString) { 986 | if (!formatString) { 987 | // Default format 988 | const { khmer } = khmerData; 989 | const moonDay = `${khmer.day}${khmer.moonPhaseName}`; 990 | return toKhmerNumeral(`ថ្ងៃ${khmer.dayOfWeekName} ${moonDay} ខែ${khmer.monthName} ឆ្នាំ${khmer.animalYearName} ${khmer.sakName} ពុទ្ធសករាជ ${khmer.beYear}`); 991 | } 992 | // Custom format 993 | const formatRules = { 994 | 'W': () => khmerData.khmer.dayOfWeekName, 995 | 'w': () => WeekdayNamesShort[khmerData.gregorian.dayOfWeek], 996 | 'd': () => toKhmerNumeral(khmerData.khmer.day), 997 | 'D': () => toKhmerNumeral((khmerData.khmer.day < 10 ? '0' : '') + khmerData.khmer.day), 998 | 'dr': () => khmerData.khmer.day, 999 | 'Dr': () => (khmerData.khmer.day < 10 ? '0' : '') + khmerData.khmer.day, 1000 | 'n': () => MoonPhaseShort[khmerData.khmer.moonPhase], 1001 | 'N': () => khmerData.khmer.moonPhaseName, 1002 | 'o': () => MoonDaySymbols[khmerData._khmerDateObj.getDayNumber()], 1003 | 'm': () => khmerData.khmer.monthName, 1004 | 'M': () => SolarMonthNames[khmerData.gregorian.month - 1], 1005 | 'a': () => khmerData.khmer.animalYearName, 1006 | 'as': () => AnimalYearEmojis[khmerData.khmer.animalYear], 1007 | 'e': () => khmerData.khmer.sakName, 1008 | 'b': () => toKhmerNumeral(khmerData.khmer.beYear), 1009 | 'br': () => khmerData.khmer.beYear, 1010 | 'c': () => toKhmerNumeral(khmerData.gregorian.year), 1011 | 'cr': () => khmerData.gregorian.year, 1012 | 'j': () => toKhmerNumeral(khmerData.khmer.jsYear), 1013 | 'jr': () => khmerData.khmer.jsYear, 1014 | 'Ms': () => SolarMonthAbbreviationNames[khmerData.gregorian.month - 1], 1015 | 'ms': () => LunarMonthAbbreviationNames[khmerData.khmer.monthIndex] 1016 | }; 1017 | // Sort keys by length descending to ensure longer tokens (like 'Ms', 'ms') are matched before shorter ones (like 'M', 'm') 1018 | const sortedKeys = Object.keys(formatRules).sort((a, b) => b.length - a.length); 1019 | const regex = new RegExp(`\\[([^\\]]+)\\]|(${sortedKeys.join('|')})`, 'g'); 1020 | const result = formatString.replace(regex, (match, escaped, token) => { 1021 | if (escaped) { 1022 | return escaped; 1023 | } 1024 | const value = formatRules[token](); 1025 | return String(value); 1026 | }); 1027 | return result; 1028 | } 1029 | // ============================================================================ 1030 | // Wrapper function for public API 1031 | function gregorianToKhmer(year, month, day, hour = 0, minute = 0, second = 0) { 1032 | // Validate input parameters 1033 | validateGregorianDate(year, month, day, hour, minute, second); 1034 | return gregorianToKhmerInternal(year, month, day, hour, minute, second, false); 1035 | } 1036 | // ============================================================================ 1037 | // Public API 1038 | // ============================================================================ 1039 | // Conversion functions 1040 | function fromGregorian(year, month, day, hour = 0, minute = 0, second = 0) { 1041 | return gregorianToKhmer(year, month, day, hour, minute, second); 1042 | } 1043 | function fromKhmer(day, moonPhase, monthIndex, beYear) { 1044 | return khmerToGregorian(day, moonPhase, monthIndex, beYear); 1045 | } 1046 | // New Year function 1047 | function getNewYear(ceYear) { 1048 | return getKhmerNewYear(ceYear); 1049 | } 1050 | // Format function 1051 | function format(khmerData, formatString) { 1052 | return formatKhmer(khmerData, formatString); 1053 | } 1054 | // Utility for creating date from Date object 1055 | function fromDate(date) { 1056 | // Validate Date object 1057 | validateDateObject(date); 1058 | return gregorianToKhmer(date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()); 1059 | } 1060 | // Convert Khmer to Date object 1061 | function toDate(day, moonPhase, monthIndex, beYear) { 1062 | const greg = khmerToGregorian(day, moonPhase, monthIndex, beYear); 1063 | return new Date(greg.year, greg.month - 1, greg.day); 1064 | } 1065 | // Constants export 1066 | exports.constants = { 1067 | LunarMonths, 1068 | LunarMonthNames, 1069 | SolarMonthNames, 1070 | SolarMonthAbbreviationNames, 1071 | LunarMonthAbbreviationNames, 1072 | AnimalYearNames, 1073 | AnimalYearEmojis, 1074 | SakNames, 1075 | WeekdayNames, 1076 | MoonPhaseNames 1077 | }; 1078 | // Default export - aggregate all exports for convenience 1079 | exports.default = { 1080 | fromGregorian, 1081 | fromKhmer, 1082 | getNewYear, 1083 | format, 1084 | fromDate, 1085 | toDate, 1086 | constants: exports.constants, 1087 | MoonPhase, 1088 | MonthIndex, 1089 | AnimalYear, 1090 | Sak, 1091 | DayOfWeek 1092 | }; 1093 | --------------------------------------------------------------------------------