Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions demo/nodejs/lichun_calculator.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
const Astronomy = require('./astronomy');

const SOLAR_TERMS = [
{ name: 'Lichun (立春)', nameEn: 'Beginning of Spring', longitude: 315, season: 'Spring' },
{ name: 'Yushui (雨水)', nameEn: 'Rain Water', longitude: 330, season: 'Spring' },
{ name: 'Jingzhe (惊蛰)', nameEn: 'Awakening of Insects', longitude: 345, season: 'Spring' },
{ name: 'Chunfen (春分)', nameEn: 'Spring Equinox', longitude: 0, season: 'Spring' },
{ name: 'Qingming (清明)', nameEn: 'Clear and Bright', longitude: 15, season: 'Spring' },
{ name: 'Guyu (谷雨)', nameEn: 'Grain Rain', longitude: 30, season: 'Spring' },
{ name: 'Lixia (立夏)', nameEn: 'Beginning of Summer', longitude: 45, season: 'Summer' },
{ name: 'Xiaoman (小满)', nameEn: 'Grain Buds', longitude: 60, season: 'Summer' },
{ name: 'Mangzhong (芒种)', nameEn: 'Grain in Ear', longitude: 75, season: 'Summer' },
{ name: 'Xiazhi (夏至)', nameEn: 'Summer Solstice', longitude: 90, season: 'Summer' },
{ name: 'Xiaoshu (小暑)', nameEn: 'Slight Heat', longitude: 105, season: 'Summer' },
{ name: 'Dashu (大暑)', nameEn: 'Great Heat', longitude: 120, season: 'Summer' },
{ name: 'Liqiu (立秋)', nameEn: 'Beginning of Autumn', longitude: 135, season: 'Autumn' },
{ name: 'Chushu (处暑)', nameEn: 'Stopping the Heat', longitude: 150, season: 'Autumn' },
{ name: 'Bailu (白露)', nameEn: 'White Dew', longitude: 165, season: 'Autumn' },
{ name: 'Qiufen (秋分)', nameEn: 'Autumn Equinox', longitude: 180, season: 'Autumn' },
{ name: 'Hanlu (寒露)', nameEn: 'Cold Dew', longitude: 195, season: 'Autumn' },
{ name: 'Shuangjiang (霜降)', nameEn: 'Frost\'s Descent', longitude: 210, season: 'Autumn' },
{ name: 'Lidong (立冬)', nameEn: 'Beginning of Winter', longitude: 225, season: 'Winter' },
{ name: 'Xiaoxue (小雪)', nameEn: 'Slight Snow', longitude: 240, season: 'Winter' },
{ name: 'Daxue (大雪)', nameEn: 'Great Snow', longitude: 255, season: 'Winter' },
{ name: 'Dongzhi (冬至)', nameEn: 'Winter Solstice', longitude: 270, season: 'Winter' },
{ name: 'Xiaohan (小寒)', nameEn: 'Slight Cold', longitude: 285, season: 'Winter' },
{ name: 'Dahan (大寒)', nameEn: 'Great Cold', longitude: 300, season: 'Winter' }
];

function calculateSolarTerm(longitude, year) {
// Create search start date (January 1st of the given year)
const searchStart = new Date(year, 0, 1); // Month is 0-indexed
const searchDays = 366; // Search for 366 days

// First attempt: search within the target year
try {
const astroTime = Astronomy.SearchSunLongitude(longitude, searchStart, searchDays);
// Only return if the result is in the target year
if (astroTime && astroTime.date.getFullYear() === year) {
return astroTime;
}
return astroTime;
} catch (error) {
console.error('Error searching for solar term:', error);
}

return null; // No solar term found in the target year
}

/**
* Get the Lichun (Beginning of Spring) date for a given year
* @param {number} year - The year to find Lichun for
* @returns {AstroTime|null} - The AstroTime object for Lichun, or null if not found
*/
function getLichunDate(year) {
return calculateSolarTerm(315, year); // 315 degrees is Lichun
}

/**
* Calculate the search period between two Lichun dates
* @param {number} year - The starting year
* @returns {Object} - Object containing searchStart (AstroTime) and searchDays (number)
*/
function calculateSearchPeriod(year) {
// Get Lichun of the specified year
const lichunStart = getLichunDate(year);
if (!lichunStart) {
throw new Error(`Could not find Lichun date for year ${year}`);
}

// Get Lichun of the next year
const lichunEnd = getLichunDate(year + 1);
if (!lichunEnd) {
throw new Error(`Could not find Lichun date for year ${year + 1}`);
}

// Calculate the difference in days between the two Lichun dates
const searchDays = (lichunEnd.date.getTime() - lichunStart.date.getTime()) / (1000 * 60 * 60 * 24);

// Subtract 1 day from the start date to ensure we capture the first Lichun
// SearchSunLongitude requires dateStart to be earlier than the desired longitude event
const adjustedStartDate = new Date(lichunStart.date.getTime() - 24 * 60 * 60 * 1000);

return {
searchStart: adjustedStartDate,
searchDays: Math.ceil(searchDays) + 1 // Add 1 day to compensate for the earlier start
};
}

/**
* Get all 24 solar terms between two Lichun dates
* @param {number} year - The starting year (between Lichun of this year and next year)
* @returns {Array} - Array of objects containing solar term information and dates
*/
function getAllSolarTerms(year) {
try {
const { searchStart, searchDays } = calculateSearchPeriod(year);
const solarTerms = [];

// Search for each of the 24 solar terms
for (const term of SOLAR_TERMS) {
try {
const astroTime = Astronomy.SearchSunLongitude(term.longitude, searchStart, searchDays);
if (astroTime) {
solarTerms.push({
...term,
date: astroTime.date,
astroTime: astroTime
});
}
} catch (error) {
console.error(`Error searching for ${term.name}:`, error);
// Continue with other terms even if one fails
}
}

// Sort by date to ensure chronological order
solarTerms.sort((a, b) => a.date.getTime() - b.date.getTime());

return solarTerms;
} catch (error) {
console.error('Error calculating solar terms:', error);
return [];
}
}

/**
* Get solar terms for a specific year with more detailed information
* @param {number} year - The year to get solar terms for
* @returns {Object} - Object containing year info, search period, and all solar terms
*/
function getSolarTermsForYear(year) {
try {
const searchPeriod = calculateSearchPeriod(year);
const solarTerms = getAllSolarTerms(year);

return {
year: year,
searchPeriod: {
startDate: searchPeriod.searchStart,
endDate: new Date(searchPeriod.searchStart.getTime() + searchPeriod.searchDays * 24 * 60 * 60 * 1000),
searchDays: searchPeriod.searchDays
},
solarTerms: solarTerms,
totalTerms: solarTerms.length
};
} catch (error) {
console.error(`Error getting solar terms for year ${year}:`, error);
return {
year: year,
error: error.message,
solarTerms: [],
totalTerms: 0
};
}
}

module.exports = {
SOLAR_TERMS,
calculateSolarTerm,
getLichunDate,
calculateSearchPeriod,
getAllSolarTerms,
getSolarTermsForYear
};
17 changes: 17 additions & 0 deletions demo/nodejs/quick_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Quick test - you can run this with: node -e "const {calculateSolarTerm} = require('./lichun_calculator'); console.log(calculateSolarTerm(315, 2024));"

const { calculateSolarTerm } = require('./lichun_calculator');

// Get command line arguments
const longitude = process.argv[2] || 315; // Default to Lichun (315°)
const year = process.argv[3] || 2024; // Default to 2024

console.log(`Calculating solar term for longitude ${longitude}° in year ${year}:`);
const result = calculateSolarTerm(parseInt(longitude), parseInt(year));

if (result) {
console.log(`Result: ${result.date.toISOString()}`);
console.log(`Date: ${result.date.toDateString()}`);
} else {
console.log('No solar term found for the given parameters.');
}
132 changes: 132 additions & 0 deletions demo/nodejs/seoul_solar_time.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env node

/**
* seoul_solar_time.js - True Solar Time for Seoul, South Korea
*
* This program calculates the true solar time for Seoul, South Korea.
* Seoul coordinates: 37.5665° N, 126.9780° E
*
* True solar time is based on the actual position of the Sun in the sky,
* where 12:00 represents when the Sun is at its highest point (solar noon).
*/

const Astronomy = require('./astronomy.js');

// Seoul, South Korea coordinates
const SEOUL_LATITUDE = 37.5665; // North
const SEOUL_LONGITUDE = 126.9780; // East
const SEOUL_ELEVATION = 38; // meters above sea level

function formatTime(hours) {
// Convert decimal hours to HH:MM:SS.mmm format
const totalMilliseconds = Math.round(hours * 3.6e6);
const totalSeconds = Math.floor(totalMilliseconds / 1000);
const milliseconds = totalMilliseconds % 1000;
const totalMinutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const totalHours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
const displayHours = totalHours % 24;

return `${displayHours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}.${milliseconds.toString().padStart(3, '0')}`;
}

function calculateTrueSolarTime(date) {
// Create observer for Seoul
const observer = new Astronomy.Observer(SEOUL_LATITUDE, SEOUL_LONGITUDE, SEOUL_ELEVATION);

// Calculate hour angle of the Sun
const hourAngle = Astronomy.HourAngle(Astronomy.Body.Sun, date, observer);

// Convert hour angle to true solar time
// True solar time = (hour angle + 12) % 24
// This gives us the time since solar midnight
const solarTimeHours = (hourAngle + 12.0) % 24.0;

return {
hourAngle: hourAngle,
solarTimeHours: solarTimeHours,
formattedTime: formatTime(solarTimeHours)
};
}

function main() {
console.log("=== True Solar Time for Seoul, South Korea ===\n");
console.log(`Location: Seoul, South Korea`);
console.log(`Coordinates: ${SEOUL_LATITUDE}° N, ${SEOUL_LONGITUDE}° E`);
console.log(`Elevation: ${SEOUL_ELEVATION} meters\n`);

// Get current time
const now = new Date();
console.log(`Current UTC time: ${now.toISOString()}`);
console.log(`Current local time: ${now.toString()}\n`);

// Calculate true solar time for current moment
const solarTime = calculateTrueSolarTime(now);

// Calculate current local time in Seoul (in hours)
// Seoul is UTC+9
const localTime = new Date(now.getTime() + 9 * 60 * 60 * 1000);
const localTimeHours = localTime.getHours() + localTime.getMinutes() / 60 + localTime.getSeconds() / 3600 + localTime.getMilliseconds() / 3600000;

// Calculate offset (true solar time - local clock time)
let offsetMinutes = (solarTime.solarTimeHours - localTimeHours) * 60;
// Normalize offset to [-720, +720) minutes for clarity
if (offsetMinutes < -720) offsetMinutes += 1440;
if (offsetMinutes >= 720) offsetMinutes -= 1440;

// Calculate adjusted local time (what the clock would read if it matched the Sun's position)
let adjustedLocalTimeHours = (localTimeHours + offsetMinutes / 60) % 24;
if (adjustedLocalTimeHours < 0) adjustedLocalTimeHours += 24;
const adjustedLocalTimeStr = formatTime(adjustedLocalTimeHours);

console.log("=== Current True Solar Time ===");
console.log(`Hour angle of Sun: ${solarTime.hourAngle.toFixed(4)} hours`);
console.log(`True solar time: ${solarTime.solarTimeHours.toFixed(4)} hours`);
console.log(`Formatted: ${solarTime.formattedTime}\n`);
console.log(`Current local time in Seoul: ${formatTime(localTimeHours)} (hours: ${localTimeHours.toFixed(4)})`);
console.log(`Offset (true solar time - local time): ${offsetMinutes.toFixed(1)} minutes`);
console.log(`Adjusted local time (if clock matched Sun): ${adjustedLocalTimeStr}\n`);

// Calculate for different times of day
console.log("=== True Solar Time Throughout the Day ===");

const times = [
{ name: "Sunrise (approximate)", hour: 6 },
{ name: "Morning", hour: 9 },
{ name: "Solar Noon (should be ~12:00)", hour: 12 },
{ name: "Afternoon", hour: 15 },
{ name: "Sunset (approximate)", hour: 18 },
{ name: "Midnight", hour: 0 }
];

times.forEach(({ name, hour }) => {
const testDate = new Date(now);
testDate.setUTCHours(hour, 0, 0, 0);

const timeSolarTime = calculateTrueSolarTime(testDate);

console.log(`${name.padEnd(25)} | ${testDate.toISOString().substring(11, 19)} UTC | ${timeSolarTime.formattedTime} solar time`);
});

console.log("\n=== Explanation ===");
console.log("True solar time is based on the actual position of the Sun in the sky.");
console.log("• 12:00 solar time = Sun at highest point (solar noon)");
console.log("• 00:00 solar time = Sun at lowest point (solar midnight)");
console.log("• The difference from standard time is due to:");
console.log(" - Earth's elliptical orbit (equation of time)");
console.log(" - Observer's longitude relative to time zone meridian");
console.log(" - Daylight saving time adjustments");
}

if (require.main === module) {
main();
}

module.exports = {
calculateTrueSolarTime,
formatTime,
SEOUL_LATITUDE,
SEOUL_LONGITUDE,
SEOUL_ELEVATION
};
46 changes: 46 additions & 0 deletions demo/nodejs/test_lichun.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const { SOLAR_TERMS, calculateSolarTerm } = require('./lichun_calculator');

// Test the function with different solar terms
console.log('=== Solar Terms Calculator Test ===\n');

// Test for Lichun (立春) - Beginning of Spring (longitude 315)
console.log('Testing Lichun (立春) for year 2024:');
const lichun2024 = calculateSolarTerm(315, 2024);
if (lichun2024) {
console.log(`Lichun 2024: ${lichun2024.date.toISOString()}`);
console.log(`Date: ${lichun2024.date.toDateString()}`);
} else {
console.log('Lichun 2024 not found');
}

console.log('\n---');

// Test for Chunfen (春分) - Spring Equinox (longitude 0)
console.log('Testing Chunfen (春分) for year 2024:');
const chunfen2024 = calculateSolarTerm(0, 2024);
if (chunfen2024) {
console.log(`Chunfen 2024: ${chunfen2024.date.toISOString()}`);
console.log(`Date: ${chunfen2024.date.toDateString()}`);
} else {
console.log('Chunfen 2024 not found');
}

console.log('\n---');

// Test for Dongzhi (冬至) - Winter Solstice (longitude 270)
console.log('Testing Dongzhi (冬至) for year 2024:');
const dongzhi2024 = calculateSolarTerm(270, 2024);
if (dongzhi2024) {
console.log(`Dongzhi 2024: ${dongzhi2024.date.toISOString()}`);
console.log(`Date: ${dongzhi2024.date.toDateString()}`);
} else {
console.log('Dongzhi 2024 not found');
}

console.log('\n---');

// Display all solar terms
console.log('All Solar Terms:');
SOLAR_TERMS.forEach((term, index) => {
console.log(`${index + 1}. ${term.name} (${term.nameEn}) - Longitude: ${term.longitude}° - Season: ${term.season}`);
});
Loading