পঞ্চাঙ্গের অধিকাংশ নাম একটি continuous angular quantity-কে সমান segment-এ ভাগ করে পাওয়া যায়। Angle সীমানার সামান্য আগে না পরে আছে—এই সিদ্ধান্তেই বর্তমান রাশি, নক্ষত্র, তিথি, করণ বা যোগ নির্ধারিত হয়। অতএব boundary handling শুধু formatting-এর বিষয় নয়; এটি calendar logic-এর অংশ।
Boundary কেন এত গুরুত্বপূর্ণ?
একটি longitude ২৯.৯৯৯৯৯৯° হলে তা এখনও প্রথম রাশির মধ্যে। ৩০.০০০০০০° হলে পরবর্তী রাশি শুরু। যদি প্রথম value-কে display-এর জন্য ৩০.০০° round করে তারপর classification চালানো হয়, transition বাস্তবের আগেই ঘটে গেছে বলে ধরে নেওয়া হবে।
একই সমস্যা আরও কয়েকভাবে দেখা যায়:
- Moon ১৩°২০′ অতিক্রম করলে নক্ষত্র বদলায়;
- Moon–Sun elongation ১২° অতিক্রম করলে তিথি বদলায়;
- Elongation-এর প্রতি ৬°-এ করণ segment বদলায়;
- Nirāyaṇa Sun+Moon sum-এর প্রতি ১৩°২০′-এ যোগ বদলায়;
- Sun ৩৬০° থেকে ০°-এ গেলে Meṣa boundary crossing হয়;
- Event time সূর্যোদয় বা মধ্যরাত্রির কয়েক second এদিক-ওদিক হলে প্রয়োগের দিন বদলাতে পারে।
Panchanga segment widths
| উপাদান | মূল angle | Segment সংখ্যা | প্রতি segment |
|---|---|---|---|
| রাশি | Nirāyaṇa longitude | ১২ | ৩০° |
| নক্ষত্র | Nirāyaṇa Moon | ২৭ | ১৩°২০′ |
| তিথি | Normalize(Moon − Sun) | ৩০ | ১২° |
| করণ half-segment | Normalize(Moon − Sun) | ৬০ | ৬° |
| যোগ | Normalize(Nirāyaṇa Sun + Moon) | ২৭ | ১৩°২০′ |
| নবাংশ—যদি ব্যবহৃত হয় | Nirāyaṇa longitude | ১০৮ | ৩°২০′ |
Segment width দশমিক literal হিসেবে লিখলে ১৩.৩৩৩৩ বা ৩.৩৩৩৩ দিয়ে truncate করবেন না। 360.0 / 27.0 বা scaled-index formula ব্যবহার করুন, যাতে source definition অক্ষুণ্ণ থাকে।
০°/৩৬০° normalization
C# remainder operator negative value-কে positive circle-এ নিজে থেকে ফিরিয়ে দেয় না। তাই একটি robust normalization function দরকার:
static double Normalize360(double value)
{
if (Double.IsNaN(value) || Double.IsInfinity(value))
throw new ArgumentOutOfRangeException("value");
double normalized = value % 360.0;
if (normalized < 0.0) normalized += 360.0;
// Protect the half-open range if upstream produces 360 by rounding.
if (normalized >= 360.0) normalized = 0.0;
return normalized;
}
| Input | Normalized |
|---|---|
| −১° | ৩৫৯° |
| ০° | ০° |
| ৩৬০° | ০° |
| ৩৬১° | ১° |
| ৭২০° | ০° |
Shortest signed angular difference
static double SignedAngle(double value, double target)
{
double difference = Normalize360(value - target);
if (difference >= 180.0) difference -= 360.0;
return difference;
}
৩৫৯.৯° থেকে ০.১°-এর forward separation ০.২°; ordinary subtraction −৩৫৯.৮°। Root search, comparison ও transit direction-এ wrapped angular difference ব্যবহার না করলে ০° boundary-তে false jump তৈরি হয়।
Half-open interval policy: [start, end)
প্রতিটি segment-কে inclusive start এবং exclusive end হিসেবে সংজ্ঞায়িত করুন:
Segment 0 = [0°, width)
Segment 1 = [width, 2 × width)
…
Exact boundary নতুন segment-এর অন্তর্গত
এই policy-তে ৩০° ঠিক হলে বৃষ শুরু, ১৩°২০′ ঠিক হলে দ্বিতীয় নক্ষত্র শুরু এবং elongation ১২° ঠিক হলে দ্বিতীয়া শুরু। Half-open interval ব্যবহার করলে কোনো exact angle দুটি segment-এ পড়ে না এবং কোনো gap থাকে না।
Calendar nuance: Exact astronomical boundary-তে নতুন angular segment শুরু হলেও কোন পঞ্জিকা-দিনে সেই নতুন নাম প্রয়োগ হবে তা sunrise, midnight বা festival rule আলাদাভাবে নির্ধারণ করে।
Floating-point বাস্তবতা
.NET double একটি binary floating-point type, সাধারণত প্রায় ১৫–১৭ decimal digit precision দেয়। ০.১, ১৩⅓ বা বহু astronomical fraction binary-তে exact নয়। ফলে mathematically exact boundary code-এ এমন হতে পারে:
29.999999999999996
30.000000000000004
এটি library failure নয়; floating representation-এর স্বাভাবিক বৈশিষ্ট্য। সমাধান হলো arbitrary round করে value বদলে দেওয়া নয়, বরং calculation contract স্পষ্ট করা।
Double.Epsilon comparison tolerance নয়
Double.Epsilon হলো zero-এর ওপরে ক্ষুদ্রতম positive representable double-এর কাছাকাছি একটি value; astronomical longitude-এর practical error tolerance নয়। Ephemeris uncertainty, solver tolerance ও quantity scale অনুযায়ী tolerance নির্ধারণ করুন।
একটি universal epsilon-ও নিরাপদ নয়
সব angle-এ +1e-8 যোগ করে Floor করলে boundary আগেভাগে পার হয়ে যাবে। আবার epsilon খুব ছোট হলে কোনো উপকার হবে না। তাই:
- Ordinary instant classification raw normalized angle দিয়ে করুন;
- Exact transition দরকার হলে root solve করুন;
- Near-boundary diagnostics-এ explicit uncertainty state দিন;
- Display rounding কখনও classifier input হবে না।
C# segment classification
static int SegmentIndex(double angle, int segmentCount)
{
if (segmentCount <= 0)
throw new ArgumentOutOfRangeException("segmentCount");
double normalized = Normalize360(angle);
int index = (int)Math.Floor(
normalized * segmentCount / 360.0);
// Defensive only; Normalize360 promises [0, 360).
if (index < 0) index = 0;
if (index >= segmentCount) index = segmentCount - 1;
return index;
}
static int RashiIndex(double nirayanaLongitude)
{
return SegmentIndex(nirayanaLongitude, 12);
}
static int NakshatraIndex(double nirayanaMoon)
{
return SegmentIndex(nirayanaMoon, 27);
}
static int TithiNumber(double moonLongitude, double sunLongitude)
{
double elongation = Normalize360(moonLongitude - sunLongitude);
return SegmentIndex(elongation, 30) + 1;
}
static int KaranaHalfIndex(double moonLongitude, double sunLongitude)
{
double elongation = Normalize360(moonLongitude - sunLongitude);
return SegmentIndex(elongation, 60);
}
static int YogaIndex(double nirayanaSun, double nirayanaMoon)
{
return SegmentIndex(nirayanaSun + nirayanaMoon, 27);
}
Karana half-index থেকে নাম নির্ধারণের সময় repeating seven karaṇa এবং four fixed karaṇa-র traditional sequence আলাদা rule হিসেবে প্রয়োগ করতে হবে। Index formula ঠিক হলেই karana name mapping স্বয়ংক্রিয়ভাবে ঠিক হয় না।
Near-boundary diagnostic
public sealed class BoundaryDiagnostic
{
public int SegmentIndex { get; set; }
public double NormalizedAngle { get; set; }
public double PreviousBoundary { get; set; }
public double NextBoundary { get; set; }
public double DistanceToNearestBoundary { get; set; }
public bool IsWithinDiagnosticTolerance { get; set; }
}
static BoundaryDiagnostic InspectBoundary(
double angle,
int segmentCount,
double diagnosticToleranceDegrees)
{
double value = Normalize360(angle);
int index = SegmentIndex(value, segmentCount);
double width = 360.0 / segmentCount;
double previous = index * width;
double next = (index + 1) * width;
double fromPrevious = value - previous;
double toNext = next - value;
double nearest = Math.Min(fromPrevious, toNext);
BoundaryDiagnostic result = new BoundaryDiagnostic();
result.SegmentIndex = index;
result.NormalizedAngle = value;
result.PreviousBoundary = Normalize360(previous);
result.NextBoundary = Normalize360(next);
result.DistanceToNearestBoundary = nearest;
result.IsWithinDiagnosticTolerance =
nearest <= diagnosticToleranceDegrees;
return result;
}
এই diagnostic classifier-এর সিদ্ধান্ত বদলায় না; শুধু report-এ জানায় যে result boundary-এর খুব কাছে এবং displayed rounded value দেখে segment বিচার করা উচিত নয়।
Transition instant: equality নয়, root-finding
“প্রতি মিনিটে longitude বের করে প্রথম ৩০.০০ পাওয়া” reliable algorithm নয়। Event function তৈরি করে এমন দুটি JD খুঁজুন যেখানে function-এর sign বদলায়; তারপর bisection বা Brent-type solver দিয়ে root refine করুন।
Boundary event function
static double BoundaryFunction(
Func<double, double> longitudeAtJulianDay,
double targetLongitude,
double julianDay)
{
double longitude = longitudeAtJulianDay(julianDay);
return SignedAngle(longitude, targetLongitude);
}
Bracket ছোট রাখুন, যাতে target-এর বিপরীত বিন্দু—target ±180°—এর wrap-কে false sign change মনে না হয়। সূর্য, চন্দ্র ও retrograde planet-এর জন্য search step এক হবে না। Retrograde body একই boundary বহুবার পার হতে পারে; direction metadata রাখুন।
C# 5-compatible bisection
static double BisectRoot(
Func<double, double> function,
double left,
double right,
double timeToleranceDays,
int maxIterations)
{
double fLeft = function(left);
double fRight = function(right);
if (Double.IsNaN(fLeft) || Double.IsNaN(fRight))
throw new InvalidOperationException("Boundary function returned NaN.");
if (fLeft == 0.0) return left;
if (fRight == 0.0) return right;
if (Math.Sign(fLeft) == Math.Sign(fRight))
throw new ArgumentException("Root is not bracketed.");
for (int i = 0; i < maxIterations; i++)
{
double middle = left + (right - left) / 2.0;
double fMiddle = function(middle);
if (Math.Abs(right - left) <= timeToleranceDays)
return middle;
if (fMiddle == 0.0) return middle;
if (Math.Sign(fLeft) != Math.Sign(fMiddle))
{
right = middle;
fRight = fMiddle;
}
else
{
left = middle;
fLeft = fMiddle;
}
}
throw new InvalidOperationException(
"Boundary root did not converge within the iteration limit.");
}
এক second time tolerance চাইলে:
timeToleranceDays = 1 ÷ 86400
Angular tolerance ও time tolerance এক নয়। সূর্য প্রায় ১°/day চললে ১ arcsecond longitude প্রায় ২৪ seconds time-এর সমান; Moon-এর দ্রুত গতিতে একই ১ arcsecond অনেক কম সময়। তাই body rate না দেখে angle tolerance-কে clock tolerance বলা যাবে না।
Display rounding ও DMS carry
Calculation-এর raw longitude রেখে আলাদা formatter ব্যবহার করুন। Second round হয়ে 60 হলে minute-এ carry, minute 60 হলে degree-তে carry এবং 30° হলে rāśi-তে carry লাগতে পারে।
public sealed class DmsValue
{
public int Degree { get; set; }
public int Minute { get; set; }
public int Second { get; set; }
}
static DmsValue ToDmsInsideSegment(
double angleInsideSegment,
double segmentWidth)
{
double safe = angleInsideSegment;
if (safe < 0.0) safe = 0.0;
long totalSeconds = (long)Math.Round(
safe * 3600.0,
MidpointRounding.AwayFromZero);
long maximum = (long)Math.Round(
segmentWidth * 3600.0,
MidpointRounding.AwayFromZero);
// Formatting must not silently turn the previous segment into the next.
if (totalSeconds >= maximum)
totalSeconds = maximum - 1;
DmsValue result = new DmsValue();
result.Degree = (int)(totalSeconds / 3600L);
result.Minute = (int)((totalSeconds % 3600L) / 60L);
result.Second = (int)(totalSeconds % 60L);
return result;
}
উপরের clamp শুধু “current segment-এর ভিতরের display” বানাতে উপযোগী। Astronomical longitude-এর সাধারণ rounded display-এ carry করে পরবর্তী rāśi label দেখানোও বৈধ হতে পারে—তবে classification raw value থেকেই থাকবে। UI-তে চাইলে ২৯°৫৯′৫৯″+ বা “boundary-এর নিকট” note দেখান।
Rounding mode প্রকাশ করুন
.NET-এর Math.Round overload ও MidpointRounding mode অনুযায়ী midpoint আচরণ বদলায়। Report snapshot স্থিতিশীল রাখতে formatter-এ mode explicit করুন; environment default-এর ওপর নির্ভর করবেন না।
Calendar rule-এ formatted time নয়, raw instant
সংক্রান্তি ১১:৫৯:৫৯.৬ PM হলে display “১২:০০:০০ AM” হতে পারে। যদি month-start rule formatted clock string parse করে, ঘটনা ভুল দিনে পড়বে। Rule engine-এ ব্যবহার করুন:
- Raw Julian Day অথবা high-resolution UTC instant;
- Unrounded local
DateTime; - Actual sunrise, midnight ও next-sunrise boundaries;
- Explicit comparison—
<,<=,>; - Applied boundary policy trace।
bool beforeMidnight = sankrantiLocal < localMidnight;
bool atMidnight = sankrantiLocal == localMidnight;
// Format only after the calendar decision is complete.
string display = sankrantiLocal.ToString(
"dd MMM yyyy, hh:mm:ss tt",
CultureInfo.InvariantCulture);
DateTime-এর tick precision থাকলেও upstream ephemeris uncertainty তার চেয়ে বড় হতে পারে। অতএব exact equality বিরল এবং model-dependent; equality policy documented রাখুন। JD-based solver result থেকে DateTime conversion করার সময় early historical range ও calendar conversion আলাদা রাখুন।
Boundary regression tests
প্রতিটি boundary-র তিনটি test
Boundary B এবং small test delta δ নিয়ে পরীক্ষা করুন:
B − δ → previous segment
B → new segment
B + δ → new segment
const double Delta = 1e-10;
static void AssertBoundary(
double boundary,
int segmentCount,
int previousIndex,
int newIndex)
{
Assert.AreEqual(previousIndex,
SegmentIndex(boundary - Delta, segmentCount));
Assert.AreEqual(newIndex,
SegmentIndex(boundary, segmentCount));
Assert.AreEqual(newIndex,
SegmentIndex(boundary + Delta, segmentCount));
}
Test delta production tolerance নয়; এটি deterministic unit-test probe। Boundary value code-এ একই formula থেকে তৈরি করুন, decimal text copy করে নয়।
প্রস্তাবিত validation checklist
Normalize360(-1)= 359;Normalize360(0)= 0;Normalize360(360)= 0;Normalize360(720)= 0;- NaN rejected;
- Infinity rejected;
- Segment interval is [start, end);
- Exact 30° enters next rāśi;
- Every ১২ rāśi boundary tested;
- Last rāśi wraps to Meṣa;
- Exact 13°20′ enters next nakṣatra;
- Every ২৭ nakṣatra boundary tested;
- Last nakṣatra wraps to first;
- Exact 12° enters next tithi;
- Every ৩০ tithi boundary tested;
- Amāvasyā end wraps to Śukla Pratipad;
- Exact 6° enters next karana half;
- Every ৬০ karana half-boundary tested;
- Karana name-cycle separately tested;
- Exact yoga boundary enters next yoga;
- Every ২৭ yoga boundary tested;
- Nirāyaṇa profile same across classifier and solver;
- Raw longitude retained;
- Display longitude stored separately;
- No pre-classification rounding;
- No universal epsilon added;
Double.Epsilonnot used as domain tolerance;- Signed angle at 359.9°/0.1° tested;
- Antipodal ±180° behavior tested;
- Root bracket sign change required;
- Bracket span excludes false antipodal wrap;
- Bisection iteration cap enforced;
- NaN function result rejected;
- Time tolerance expressed in days;
- Root returned inside bracket;
- Forward crossing direction retained;
- Retrograde crossing direction retained;
- Multiple crossings not silently collapsed;
- DMS second carry tested;
- DMS minute carry tested;
- Rāśi carry/wrap tested;
- Rounding mode explicit;
- Calendar rule uses raw instant;
- Sunrise equality policy tested;
- Midnight equality policy tested;
- Next-sunrise equality policy tested;
- Historical JD retained through solve;
- HTML/XML/API raw values agree;
- Invariant-culture serialization tested;
- Regression snapshots include engine/profile version।
সাধারণ ভুল ও সমাধান
| ভুল | ফল | সমাধান |
|---|---|---|
Round করে তারপর Floor |
Boundary আগেভাগে পার | Raw angle দিয়ে classification |
| Negative remainder normalize না করা | Negative segment index | Normalize360 |
| ৩৬০°-কে segment ১২/২৭ ধরা | Array index error | ৩৬০° → ০° |
সব calculation-এ +epsilon |
Systematic early transition | Root solver ও diagnostic tolerance |
Double.Epsilon domain tolerance |
প্রায় কোনো practical effect নেই | Model/solver-based tolerance |
| Minute-by-minute equality search | Event miss বা coarse time | Bracket + root refinement |
| Formatted midnight দিয়ে rule | দিন/মাস ভুল | Raw instant comparison |
| DMS-তে 60 second print | অবৈধ display | Carry normalization |
উপসংহার
রাশি, নক্ষত্র, তিথি, করণ ও যোগ—সবগুলোই angular boundary classification। সঠিক result-এর জন্য angle প্রথমে [0°, 360°) range-এ normalize করতে হবে, segment-কে [start, end) policy-তে ভাগ করতে হবে এবং unrounded value দিয়ে index নির্ধারণ করতে হবে।
Transition time equality search করে নয়, bracketed root solver দিয়ে বের করুন। Angular tolerance, clock tolerance ও display precision আলাদা parameter। Display-এর DMS বা clock round হবে calculation শেষ হওয়ার পরে; calendar rule সর্বদা raw event instant ব্যবহার করবে।
এই separation বজায় রাখলে ২৯°৫৯′৫৯.৯৯″, ০°/৩৬০° wrap, মধ্যরাত্রির কয়েক fraction-second এবং historical JD—সব boundary case একই consistent policy-তে পরিচালিত হবে।
তথ্যসূত্র ও আরও পাঠ
- Microsoft Learn—C# floating-point numeric types;
doublerange ও approximate precision. - Microsoft Learn—Math.Round; .NET Framework overload ও rounding behavior.
- Microsoft Learn—MidpointRounding; explicit midpoint strategy.
- Microsoft Learn—Math.Floor; segment-index calculation.
- Swiss Ephemeris Programmer’s Documentation; calculation interface, transit/crossing tools ও numerical considerations.
- Swiss Ephemeris General Documentation; ephemeris precision, coordinate models ও comparison guidance.
মন্তব্য, আলোচনা ও প্রশ্ন