সরাসরি পর্বে যান
পর্ব ৩৮ · ৫৯-এর মধ্যে · মডিউল ৬

তারিখ ও সময় — java.time

Date and Time in Java — java.time: LocalDate, Instant, ZonedDateTime, Period vs Duration, Formatting, Time Zones and Testable Clocks

Taught in Bangla, this lesson explains java.time: dates, wall-clock times, instants and durations as separate types, why legacy Date and Calendar were replaced, creating, adding and comparing dates with month-end rules, Period versus Duration, formatting and parsing with DateTimeFormatter, time zones like Asia/Dhaka and DST, and testable code with Clock.

এই পর্বে যা শিখবেন

  • তারিখ, ঘড়ির সময়, মুহূর্ত আর ব্যবধান — চারটি আলাদা ধারণা, আর কোনটির জন্য কোন java.time টাইপ
  • পুরোনো Date/Calendar-এর সমস্যা, আর java.time-এ তৈরি, হিসাব, তুলনা ও মাস-শেষের নিয়ম
  • Period বনাম Duration, ChronoUnit.DAYS.between, আর DateTimeFormatter দিয়ে লেখা ও পড়া (বাংলা মাসের নামসহ)
  • টাইম জোন: ঢাকা ↔ নিউ ইয়র্ক মিটিং, DST-এর ফাঁক, আর “UTC-তে রাখুন, দেখানোর সময় রূপান্তর করুন” নীতি
  • Clock দিয়ে টেস্টযোগ্য কোড, TemporalAdjusters, সাধারণ ভুল আর AI-এর কোড যাচাই

১ · “সময়” এক শব্দ, চার অর্থ

চারটি বাক্য পড়ুন। “আমার জন্মদিন ১৫ আগস্ট।” “অফিস খোলে সকাল ৯টায়।” “আপনার bKash পেমেন্টটি হয়েছে ঠিক এই মুহূর্তে।” “পরীক্ষা চলবে ৩ ঘণ্টা।” চারটিতেই আমরা “সময়” নিয়ে কথা বলছি, কিন্তু আসলে চারটি একেবারে আলাদা জিনিস।

  • ক্যালেন্ডারের তারিখ। জন্মদিন একটি দিন — তার সাথে কোনো ঘণ্টা-মিনিট নেই। আপনি লন্ডনে গেলেও আপনার জন্মদিন ১৫ আগস্টই থাকে।
  • দেয়ালঘড়ির সময়। “সকাল ৯টা” প্রতিদিন আসে, আর ঢাকার অফিসের ৯টা আর লন্ডনের অফিসের ৯টা আলাদা মুহূর্ত — কিন্তু দুটিই “৯টা”।
  • সময়রেখার একটি মুহূর্ত। পেমেন্টটি পৃথিবীর সব জায়গার জন্য একটি মাত্র মুহূর্তে ঘটেছে। ঢাকার ঘড়িতে সেটা সকাল ১০:৩০, নিউ ইয়র্কের ঘড়িতে রাত ১২:৩০ — কিন্তু মুহূর্ত একটাই।
  • ব্যবধান। “৩ ঘণ্টা” কোনো নির্দিষ্ট সময়ে বাঁধা নয়; এটা একটি পরিমাণ। আর “৬ মাসের ঋণ”-ও ব্যবধান, কিন্তু ক্যালেন্ডারের — কারণ সব মাস সমান লম্বা নয়।

ঢাকার কমলাপুর স্টেশনের বোর্ডে লেখা: “সোনার বাংলা এক্সপ্রেস ছাড়বে সকাল ৭:০০-এ।” এটা দেয়ালঘড়ির সময় — স্টেশনের ঘড়ির হিসাবে। আপনার টিকিটে লেখা “তারিখ: ২১ ফেব্রুয়ারি” — এটা ক্যালেন্ডারের তারিখ। ট্রেন ঠিক কখন ছাড়ল, সেটা রেকর্ড হয় একটি মুহূর্ত হিসেবে, যা লন্ডনে বসে আপনার আত্মীয়ও তাঁর ঘড়িতে মিলিয়ে দেখতে পারেন। আর “যাত্রা প্রায় ৫ ঘণ্টা” — ব্যবধান। চারটি তথ্য একই টিকিটে থাকে, কিন্তু কেউ এগুলো গুলিয়ে ফেলে না। প্রোগ্রামে গুলিয়ে ফেলাই সবচেয়ে সাধারণ সময়-সংক্রান্ত bug.

পুরোনো Java-তে এই চারটির জন্য প্রায় একটিই class ছিল: java.util.Date. নাম “Date”, অথচ ভেতরে রাখে একটি মুহূর্ত; ছাপলে দেখায় কম্পিউটারের নিজস্ব টাইম জোনে। ফলে জন্মদিন রাখতে গেলে তার সাথে অপ্রয়োজনীয় ঘণ্টা-মিনিট আর জোন জুড়ে যেত, আর সার্ভার অন্য দেশে সরালে জন্মদিন এক দিন সরে যাওয়ার মতো অদ্ভুত bug হতো।

Java 8-এ (২০১৪) এল নতুন package java.time, যা JSR 310 নামের একটি স্পেসিফিকেশনের ফল। এর মূল চিন্তা সহজ: প্রতিটি অর্থের জন্য আলাদা টাইপ, যাতে ভুল করে একটির জায়গায় আরেকটি বসালে compiler-ই আপত্তি করে। আর প্রতিটি টাইপ immutable — একবার তৈরি হলে বদলায় না।

চার অর্থ, চার পরিবার

Calendar date, wall-clock time, instant on the timeline, and amount of time

🎂 তারিখ

জন্মদিন, পরীক্ষার দিন, কিস্তির দিন। ঘণ্টা নেই, জোন নেই।

LocalDate

🕘 দেয়ালঘড়ির সময়

অফিস খোলার সময়, অ্যালার্ম। প্রতিদিন আসে, জোন নেই।

LocalTime

🌍 মুহূর্ত

পেমেন্ট, লগইন, অর্ডার। পৃথিবীতে একটাই, জোনভেদে ঘড়ি আলাদা।

Instant · ZonedDateTime

⏱️ ব্যবধান

৩ ঘণ্টার পরীক্ষা, ৬ মাসের ঋণ, ২২ বছর বয়স।

Duration · Period
ছবি ৩৭.১ — java.time-এর প্রথম শিক্ষা: আগে ঠিক করুন আপনার “সময়” আসলে কোন অর্থে, তারপর টাইপ বাছুন।
মূল ধারণা

সময়-সংক্রান্ত প্রায় সব bug-এর শিকড় একটাই: একটি অর্থের জিনিস আরেক অর্থের টাইপে রাখা। java.time সেই ভুল কঠিন করে দেয়। যেমন LocalDate-এর সাথে ঘণ্টা যোগ করার method-ই নেই, আর LocalDateTime থেকে Instant পেতে হলে আপনাকে জোন বলতেই হয়।

২ · পুরোনো Date আর Calendar — কেন বদলাতে হলো

Java 1.0 (১৯৯৬) এসেছিল java.util.Date নিয়ে। পরের সংস্করণে এর বেশিরভাগ method deprecated হয়ে যায় আর আসে java.util.Calendar. দুটি মিলে যে পুরোনো date API, তা নিয়ে Java ডেভেলপাররা প্রায় দুই দশক ভুগেছেন। মূল সমস্যা তিনটি।

সমস্যা ১: মাস গোনা শুরু ০ থেকে

নিচের কোডের লেখক ২১ ফেব্রুয়ারি — ভাষা শহীদ দিবস — বোঝাতে চেয়েছিলেন। ফেব্রুয়ারি বছরের দ্বিতীয় মাস, তাই লিখেছেন 2:

Calendar cal = Calendar.getInstance();
cal.clear();
cal.set(2026, 2, 21);          // ২১ ফেব্রুয়ারি বোঝাতে চেয়েছিলেন
SimpleDateFormat f = new SimpleDateFormat("dd MMMM yyyy", Locale.ENGLISH);
System.out.println(f.format(cal.getTime()));
21 March 2026

ছাপা হলো মার্চ! কারণ Calendar-এ জানুয়ারি = ০, ফেব্রুয়ারি = ১, … ডিসেম্বর = ১১ (Calendar.DECEMBER-এর মান সত্যিই 11). অথচ দিন গোনা শুরু ১ থেকে। আরও খারাপ: cal.set(2026, 12, 16) লিখলে কোনো error হয় না — Calendar চুপচাপ পরের বছরে চলে যায়, আর ফল দাঁড়ায় ১৬ জানুয়ারি ২০২৭ (Java 21-এ চালিয়ে যাচাই করা)। ভুল মান দিলে থামানোর বদলে “বুঝে নেওয়া” — এই নরম আচরণ (lenient) bug লুকিয়ে রাখে।

সমস্যা ২: mutable — যে কেউ বদলে দিতে পারে

Date আর Calendar দুটিই mutable: date.setTime(...) বা cal.add(...) ডাকলে সেই object-ই বদলে যায়। ধরুন একটি Loan object তার শুরুর তারিখ হিসেবে একটি Date রাখে, আর getter সেই Date-টিই ফেরত দেয়। বাইরের কোনো কোড সেটার ওপর setTime ডাকলে ঋণের শুরুর তারিখই বদলে গেল — Loan টেরও পেল না। পর্ব ১৭-এর defensive copy ছাড়া এর প্রতিকার নেই।

সমস্যা ৩: SimpleDateFormat thread-safe নয়

তারিখকে লেখায় রূপ দিতে ব্যবহার হতো SimpleDateFormat. এটি বানাতে একটু খরচ হয়, তাই অনেকে একটি static formatter বানিয়ে পুরো অ্যাপে ভাগ করতেন। কিন্তু এর ভেতরে একটি Calendar field আছে, যা প্রতিটি format বা parse-এর সময় বদলায়। ফলে দুটি thread একসাথে ব্যবহার করলে একজনের তারিখ আরেকজনের ফলে মিশে যেতে পারে — ভুল তারিখ, বা অদ্ভুত exception, তাও শুধু চাপের সময়। একে thread-safe নয় বলে; পর্ব ৪০-এ thread নিয়ে বিস্তারিত দেখবেন। java.time-এর DateTimeFormatter immutable, তাই নিশ্চিন্তে static final রাখা যায়।

পুরোনো API-র কষ্ট থেকে জন্ম নেয় Joda-Time — Stephen Colebourne-এর তৈরি একটি ওপেন-সোর্স লাইব্রেরি, যা ২০০০-এর দশকে Java জগতে কার্যত মানদণ্ড হয়ে ওঠে। পরে Colebourne নিজেই সহ-নেতৃত্ব দেন JSR 310-এর, যেখানে Joda-Time-এর অভিজ্ঞতা থেকে নতুন করে নকশা করা হয় java.time. এটি যুক্ত হয় Java 8-এ (মার্চ ২০১৪)। আজ Joda-Time-এর নিজের ওয়েবসাইটও নতুন প্রজেক্টে java.time ব্যবহারের পরামর্শ দেয় (ভাবনার প্রশ্ন ১)।

পুরোনো কোডের সাথে কাজ করতে হলে

অনেক লাইব্রেরি আর পুরোনো প্রজেক্ট এখনো Date দেয় বা চায়। Java 8 থেকে সেতু আছে: date.toInstant() দিয়ে Instant-এ যান, আর উল্টো দিকে Date.from(instant). নিয়ম: সীমানায় একবার রূপান্তর করুন, ভেতরের সব হিসাব java.time-এ।

৩ · java.time-এর মানচিত্র — কোন কাজে কোন টাইপ

java.time-এ অনেক class, কিন্তু প্রতিদিনের কাজে লাগে মোটামুটি নিচের কয়েকটি। ছাপলে কেমন দেখায়, সেটাও দেওয়া আছে — সবগুলো আন্তর্জাতিক মান ISO-8601 অনুযায়ী (বছর-মাস-দিন, তারপর T, তারপর সময়):

টাইপকী বোঝায়বাস্তব উদাহরণছাপলে
LocalDateশুধু তারিখজন্মদিন, পরীক্ষার দিন2026-02-21
LocalTimeশুধু ঘড়ির সময়অফিস খোলে ৯টায়09:00
LocalDateTimeতারিখ + সময়, জোন ছাড়া“২৬ মার্চ সকাল ১০:৩০” — কোন শহরের, বলা নেই2026-03-26T10:30
Instantসময়রেখার একটি মুহূর্ত (UTC)পেমেন্ট, লগের সময়2026-03-26T04:30:00Z
ZonedDateTimeতারিখ + সময় + জোনের নিয়মঢাকার একটি মিটিং2026-03-26T10:30+06:00[Asia/Dhaka]
OffsetDateTimeতারিখ + সময় + শুধু UTC থেকে পার্থক্যAPI বা ডেটাবেসে আদান-প্রদান2026-03-26T10:30+06:00
ZoneIdএকটি টাইম জোনের নাম ও নিয়মAsia/Dhaka, Europe/LondonAsia/Dhaka
Durationসেকেন্ড-ন্যানোসেকেন্ডে মাপা ব্যবধানপরীক্ষা ২ ঘণ্টা ১৫ মিনিটPT2H15M
Periodবছর-মাস-দিনে মাপা ব্যবধানবয়স, ঋণের মেয়াদP22Y2M3D
Year, YearMonth, MonthDayতারিখের অংশবেতনের মাস, প্রতি বছরের জন্মদিন2026-02, --02-21
DayOfWeek, Monthenum — সপ্তাহের দিন, মাসশুক্রবার ছুটিFRIDAY, FEBRUARY

নামের “Local” শব্দটি নতুনদের বিভ্রান্ত করে। এর মানে “এই এলাকার” নয়, বরং “কোনো জোনের তথ্য নেই” — যে দেখছেন, তাঁর স্থানীয় ঘড়ির সাপেক্ষে পড়তে হবে। তাই LocalDateTime একা কোনো মুহূর্ত নির্দেশ করে না: “২৬ মার্চ সকাল ১০:৩০” ঢাকায় এক মুহূর্ত, নিউ ইয়র্কে আরেক। আর Instant-এর শেষের Z মানে “Zulu” বা UTC — শূন্য অফসেট। Month enum থাকায় LocalDate.of(2026, Month.FEBRUARY, 21)-ও লেখা যায়, আর java.time-এ মাসের সংখ্যা স্বাভাবিক: ফেব্রুয়ারি = ২।

এবার চারটি বাস্তব পরিস্থিতিতে টাইপ বাছাইয়ের চিন্তাটা ধাপে ধাপে দেখুন:

  1. জন্মদিনLocalDateশুধু দিন
  2. অফিস ৯টায়LocalTimeপ্রতিদিন
  3. পেমেন্টInstantএকটি মুহূর্ত
  4. বিদেশি মিটিংZonedDateTimeশহরের ঘড়িতে
  5. কতক্ষণDuration · Periodব্যবধান
ধাপ ১প্রশ্ন: ঘণ্টা-মিনিট লাগবে?
LocalDate birth = LocalDate.of(2004, 8, 15);
→ 2004-08-15
ঘণ্টা লাগে না, জোনও না — LocalDate
ধাপ ২প্রশ্ন: নির্দিষ্ট দিন আছে?
LocalTime open = LocalTime.of(9, 0);
→ 09:00
প্রতিদিনের নিয়ম, কোনো দিনে বাঁধা নয়
ধাপ ৩প্রশ্ন: সারা পৃথিবীর জন্য একটাই মুহূর্ত?
Instant paidAt = Instant.parse("2026-03-26T04:30:00Z");
→ 2026-03-26T04:30:00Z
ডেটাবেসে এভাবেই রাখুন; দেখানোর সময় জোন
ধাপ ৪প্রশ্ন: কোন শহরের ঘড়িতে ঠিক করা?
ZonedDateTime.of(2026, 11, 2, 20, 0, 0, 0,
                 ZoneId.of("Asia/Dhaka"))
→ 2026-11-02T20:00+06:00[Asia/Dhaka]
ধাপ ৫প্রশ্ন: ঘড়ির ব্যবধান, নাকি ক্যালেন্ডারের?
Duration.ofMinutes(135)  → PT2H15M
Period.ofMonths(6)       → P6M
ঘণ্টা-মিনিট: Duration; বছর-মাস-দিন: Period
  1. জন্মদিন — LocalDate. নিজেকে জিজ্ঞেস করুন, “ঘণ্টা-মিনিট কি অর্থবহ?” জন্মদিনে নয়। Date বা LocalDateTime-এ রাখলে অপ্রয়োজনীয় “00:00” জুড়ে যায়, আর জোন বদলালে তারিখ সরে যাওয়ার ঝুঁকি তৈরি হয়।
  2. অফিস খোলে ৯টায় — LocalTime. এটা প্রতিদিনের নিয়ম; কোনো তারিখ নেই। কোনো নির্দিষ্ট দিনের সাথে মেলাতে চাইলে পরে date.atTime(open) দিয়ে LocalDateTime বানাবেন।
  3. পেমেন্টের মুহূর্ত — Instant. গ্রাহক ঢাকায়, সার্ভার সিঙ্গাপুরে, অডিটর লন্ডনে — সবার কাছে মুহূর্তটি এক হতে হবে। তাই জোন-নিরপেক্ষ UTC-তে রাখুন। LocalDateTime এখানে সবচেয়ে বিপজ্জনক ভুল, কারণ কোন জোনের সময় তা হারিয়ে যায়।
  4. বিদেশি সহকর্মীর সাথে মিটিং — ZonedDateTime. মিটিং ঠিক হয়েছে “ঢাকার সময় রাত ৮টা”। এখানে মানুষের কাছে অর্থবহ হলো শহরের ঘড়ি, তাই জোনসহ রাখুন। জোনের নিয়ম (DST) জানে বলে অন্য শহরে রূপান্তর ঠিক হয় — অংশ ৭-এ।
  5. “কতক্ষণ” — Duration বা Period. পরীক্ষা ২ ঘণ্টা ১৫ মিনিট: ঘড়ির ব্যবধান, Duration. ঋণ ৬ মাস বা বয়স ২২ বছর: ক্যালেন্ডারের ব্যবধান, Period — কারণ “এক মাস” কখনো ২৮ দিন, কখনো ৩১। অংশ ৫-এ পার্থক্যটা গভীরভাবে দেখবেন।

৪ · তৈরি, হিসাব আর তুলনা

java.time-এ কোনো public constructor নেই; object বানানো হয় static factory method দিয়ে। দুটি সবচেয়ে বেশি লাগে:

LocalDate d1 = LocalDate.of(2026, 2, 21);              // সংখ্যা দিয়ে
LocalDate d2 = LocalDate.parse("2026-02-21");           // ISO লেখা থেকে
LocalTime t  = LocalTime.parse("09:15");
LocalDateTime dt = LocalDateTime.parse("2026-02-21T09:15");
Duration exam = Duration.parse("PT1H30M");              // ১ ঘণ্টা ৩০ মিনিট
Period loan   = Period.parse("P1Y2M");                  // ১ বছর ২ মাস

ভুল মান দিলে java.time পুরোনো Calendar-এর মতো “বুঝে নেয়” না, সাথে সাথে থামে। LocalDate.of(2026, 2, 30) দেয় java.time.DateTimeException: Invalid date 'FEBRUARY 30', আর LocalDate.of(2026, 13, 1) দেয় Invalid value for MonthOfYear (valid values 1 - 12): 13 (দুটিই Java 21-এ যাচাই করা)। ভুল যেখানে, ধরা পড়ল সেখানেই।

হিসাব — প্রতিবার নতুন object

plusDays, minusMonths, plusYears, withDayOfMonth — সব method নতুন object ফেরত দেয়; মূলটি বদলায় না। এটা immutable হওয়ার অর্থ, আর এখানেই নতুনদের প্রথম ভুল:

LocalDate x = LocalDate.of(2026, 2, 21);
x.plusDays(1);                 // ফল কোথাও রাখা হয়নি — হারিয়ে গেল
System.out.println(x);         // 2026-02-21, বদলায়নি
x = x.plusDays(1);             // ঠিক: নতুন object ভ্যারিয়েবলে রাখুন

পর্ব ৮-এ String-এর toUpperCase() নিয়ে ঠিক এই কথাই দেখেছিলেন। লাভটাও একই: একটি LocalDate যত জায়গায় ইচ্ছা ভাগ করুন, কেউ সেটা বদলে দিতে পারবে না — পুরোনো Date-এর সমস্যা ২ এখানে নেই।

মাস-শেষের নিয়ম — ৩১ জানুয়ারি + ১ মাস

ধরুন কেউ ৩১ জানুয়ারি ঋণ নিলেন, প্রতি মাসে একই তারিখে কিস্তি। ফেব্রুয়ারিতে তো ৩১ তারিখ নেই! java.time-এর নিয়ম: মাস যোগ করার পর দিনটি সেই মাসে না থাকলে, সেই মাসের শেষ বৈধ দিনে নামিয়ে আনা হয়। ২০২৬ সাল leap year নয়, তাই ফল ২৮ ফেব্রুয়ারি; ২০২৮ সালে একই হিসাব দেয় ২৯ ফেব্রুয়ারি। কিন্তু এর পরে একটি সূক্ষ্ম ফাঁদ আছে, ধাপে ধাপে দেখুন:

1LocalDate start = LocalDate.of(2026, 1, 31);2LocalDate feb  = start.plusMonths(1);3LocalDate mar1 = feb.plusMonths(1);    // চেইন4LocalDate mar2 = start.plusMonths(2);  // মূল থেকে5// কিস্তি i: start.plusMonths(i)
ভ্যারিয়েবল
start
feb
mar1
mar2
  1. শুরু ৩১ জানুয়ারি। মাসের শেষ দিন — এখানেই ঝামেলার শুরু, কারণ সব মাসে ৩১ তারিখ নেই।
  2. + ১ মাস → ২৮ ফেব্রুয়ারি। “ফেব্রুয়ারির ৩১” অসম্ভব, তাই java.time দিনটিকে মাসের শেষ বৈধ দিনে নামাল। exception নয়, পরের মাসে গড়িয়ে যাওয়াও নয় (পুরোনো lenient Calendar যা করতে পারত)। ব্যাংকের হিসাবেও সাধারণত এটাই প্রত্যাশিত।
  3. তার ওপর + ১ মাস → ২৮ মার্চ! feb শুধু একটি তারিখ — 2026-02-28. সে মনে রাখে না যে আসলে ৩১ থেকে এসেছিল। তাই ২৮ + ১ মাস = ২৮ মার্চ। মার্চে ৩১ তারিখ থাকা সত্ত্বেও কিস্তি তিন দিন আগে চলে এল — আর প্রতি মাসে এই ভুল জমতে থাকবে।
  4. মূল তারিখ থেকে + ২ মাস → ৩১ মার্চ। start.plusMonths(2) সরাসরি জানুয়ারি থেকে গোনে, তাই মার্চের ৩১ পাওয়া গেল। একই “দুই মাস”, দুই রকম ফল — পার্থক্য শুধু কোথা থেকে গুনছেন।
  5. নিয়ম: প্রতিবার মূল তারিখ থেকে। কিস্তি i-এর তারিখ = start.plusMonths(i), আগের কিস্তি থেকে নয়। একই কারণে plusMonths(1).plusMonths(1) আর plusMonths(2) সবসময় সমান নয় — মাসের হিসাবে যোগের সাধারণ নিয়ম খাটে না।

তুলনা

দুটি তারিখ বা সময় তুলনা করতে isBefore, isAfter, isEqual, আর মান সমান কি না দেখতে equals. java.time-এর class-গুলো value-based class, তাই == দিয়ে তুলনা করবেন না — একই তারিখের দুটি object একই object হবে, এমন নিশ্চয়তা নেই। এগুলো Comparable-ও (পর্ব ৩৩), তাই তারিখের তালিকা সরাসরি sort করা যায়।

এবার এই অংশের তিনটি বাস্তব হিসাব একসাথে চালান — জন্মতারিখ থেকে বয়স, মাসিক কিস্তির তারিখ, আর লাইব্রেরির জরিমানা। লক্ষ করুন, “আজ” হিসেবে now() নয়, একটি নির্দিষ্ট তারিখ দেওয়া — তাই যতবার, যেদিনই চালান, ফল একই:

import java.time.*;
import java.time.temporal.ChronoUnit;

class Main {
    public static void main(String[] args) {
        LocalDate birth = LocalDate.of(2004, 8, 15);
        LocalDate today = LocalDate.of(2026, 10, 18);     // now() নয়, নির্দিষ্ট দিন
        Period age = Period.between(birth, today);
        System.out.println("বয়স: " + age.getYears() + " বছর " + age.getMonths()
                + " মাস " + age.getDays() + " দিন (" + age + ")");
        System.out.println("মোট দিন: " + ChronoUnit.DAYS.between(birth, today));

        LocalDate loan = LocalDate.of(2026, 1, 31);       // ঋণ নেওয়ার দিন
        for (int i = 1; i <= 4; i++)
            System.out.println("কিস্তি " + i + ": " + loan.plusMonths(i));

        LocalDate due = LocalDate.of(2026, 10, 5);         // বই ফেরতের শেষ দিন
        LocalDate returned = LocalDate.of(2026, 10, 18);
        long late = Math.max(0, ChronoUnit.DAYS.between(due, returned));
        System.out.println("দেরি " + late + " দিন, জরিমানা " + late * 5 + " টাকা");
        System.out.println(returned.isAfter(due) + ", ফেরত " + returned.getDayOfWeek());
    }
}

আউটপুট আট লাইন: বয়স: 22 বছর 2 মাস 3 দিন (P22Y2M3D), মোট দিন: 8099, তারপর কিস্তি 1: 2026-02-28, কিস্তি 2: 2026-03-31, কিস্তি 3: 2026-04-30, কিস্তি 4: 2026-05-31, তারপর দেরি 13 দিন, জরিমানা 65 টাকা আর true, ফেরত SUNDAY. Period.between আগে পুরো বছর, তারপর পুরো মাস, শেষে বাকি দিন গোনে — ১৫ আগস্ট ২০২৬-এ ২২ বছর, ১৫ অক্টোবরে ২ মাস, তারপর ৩ দিন। আর ChronoUnit.DAYS.between দেয় মোট দিনের সংখ্যা, একটি long. কিস্তির লুপ প্রতিবার মূল loan থেকে গোনে, তাই মার্চ আর মে-তে ৩১ তারিখ ঠিকঠাক এসেছে। বই সময়মতো ফেরত দিলে between ঋণাত্মক হতো; Math.max(0, ...) সেটা ০ করে দেয়। (অনলাইন runner-এর জন্য class public নয়; নিজের প্রজেক্টে public class Main থাকে Main.java-তে।)

৫ · Period বনাম Duration — ক্যালেন্ডারের ব্যবধান আর ঘড়ির ব্যবধান

দুটিই “কত সময়”, কিন্তু মাপার একক আলাদা, আর সেজন্য অর্থও আলাদা। Duration মাপে সেকেন্ড আর ন্যানোসেকেন্ডে — যেন একটি স্টপওয়াচ। Period মাপে বছর, মাস আর দিনে — যেন দেয়ালের ক্যালেন্ডার। “১ দিন” কথাটিও দুই জগতে আলাদা: Duration.ofDays(1) মানে ঠিক ২৪ ঘণ্টা (ছাপলে PT24H), আর Period.ofDays(1) মানে “ক্যালেন্ডারে পরের দিন, একই ঘড়ির সময়” — যা DST-এর দিনে ২৩ বা ২৫ ঘণ্টাও হতে পারে (অংশ ৭)।

Period age = Period.between(LocalDate.of(2004, 8, 15),
                            LocalDate.of(2026, 10, 18));
// P22Y2M3D — ২২ বছর ২ মাস ৩ দিন
LocalDate end = LocalDate.of(2026, 1, 31).plus(Period.ofMonths(6));

শুধু LocalDate-এর মধ্যে কাজ করে। মানুষের ভাষার ব্যবধান: বয়স, ঋণ বা চাকরির মেয়াদ, সাবস্ক্রিপশন। P22Y2M3D পড়ুন “Period: 22 Years 2 Months 3 Days”.

Instant start = Instant.parse("2026-03-26T04:30:00Z");
Instant end   = Instant.parse("2026-03-26T06:45:30Z");
Duration d = Duration.between(start, end);   // PT2H15M30S
long minutes = d.toMinutes();                // 135

কাজ করে সময় আছে এমন টাইপের মধ্যে — Instant, LocalTime, LocalDateTime, ZonedDateTime. পরীক্ষার সময়, API-র timeout, ডেলিভারি কত ঘণ্টা দেরিতে। PT মানে “Period of Time”, তারপর ঘণ্টা (H), মিনিট (M), সেকেন্ড (S).

Duration.between(LocalDate.of(2026, 1, 1), LocalDate.of(2026, 2, 1));
java.time.temporal.UnsupportedTemporalTypeException: Unsupported unit: Seconds

LocalDate-এ সেকেন্ড বলে কিছু নেই, তাই Duration মাপতে পারে না। দুই তারিখের মোট দিন চাইলে ChronoUnit.DAYS.between(a, b), আর বছর-মাস-দিন চাইলে Period.between(a, b).

ChronoUnit হলো সময়ের এককগুলোর একটি enum — DAYS, WEEKS, MONTHS, HOURS, MINUTES ইত্যাদি। এর between method মোট পরিমাণ দেয় একটি সংখ্যায়, যা প্রায়ই Period-এর চেয়ে কাজের। খেয়াল করুন, Period.getDays() শুধু “বাকি দিন” দেয় (ওপরের উদাহরণে ৩), মোট দিন নয় — লাইব্রেরির জরিমানায় Period.between(...).getDays() লিখলে দুই মাস দেরিতেও জরিমানা আসবে মাত্র কয়েক দিনের। মোট দিনের জন্য সবসময় ChronoUnit.DAYS.between.

Period.between(a, b)-এর ফলাফল সবসময় স্বজ্ঞাত নয়, কারণ মাসের দৈর্ঘ্য আলাদা। যেমন ৩১ জানুয়ারি থেকে ১ মার্চ ২০২৬ দেয় P1M1D — ৩১ জানুয়ারি + ১ মাস = ২৮ ফেব্রুয়ারি, তারপর আরও ১ দিন। আর ২৯ ফেব্রুয়ারি ২০০০-এ জন্মানো কারও ২৮ ফেব্রুয়ারি ২০২৬-এ বয়স আসে P25Y11M30D — ২৬ বছর নয় (দুটিই Java 21-এ যাচাই করা)। leap day-তে জন্মদিনের নিয়ম আইন ও প্রতিষ্ঠানভেদে আলাদা হতে পারে, তাই বয়সের সীমা যেখানে আইনি বিষয়, সেখানে নিয়মটি লিখিতভাবে জেনে নিন।

৬ · লেখা থেকে তারিখ, তারিখ থেকে লেখা — DateTimeFormatter

ISO রূপ 2026-02-21 কম্পিউটারের জন্য চমৎকার — বর্ণক্রমে সাজালেই সময়ক্রমে সাজানো হয়ে যায়। কিন্তু বাংলাদেশে মানুষ লেখেন 21/02/2026. এই রূপান্তরের যন্ত্র DateTimeFormatter:

static final DateTimeFormatter BD = DateTimeFormatter.ofPattern("dd/MM/uuuu");

String s = LocalDate.of(2026, 2, 21).format(BD);   // "21/02/2026"
LocalDate d = LocalDate.parse("21/02/2026", BD);   // 2026-02-21

immutable আর thread-safe বলে static final রাখা নিরাপদ — SimpleDateFormat-এর ঠিক উল্টো। প্যাটার্নের অক্ষরগুলো বড়-ছোট হাতের ওপর নির্ভর করে, আর এখানেই অনেক ভুল হয়:

অক্ষরঅর্থউদাহরণসাধারণ ভুল
ddমাসের দিন21DD = বছরের দিন (২১ ফেব্রুয়ারি = ৫২তম দিন)
MM, MMMMমাস: সংখ্যা, পুরো নাম02, Februarymm = মিনিট!
uuuu / yyyyবছর2026YYYY = সপ্তাহভিত্তিক বছর (অংশ ৯)
HH / hh + a২৪-ঘণ্টা / ১২-ঘণ্টা + AM/PM22:05 / 10:30 AMhh দিয়ে a না দিলে সকাল-রাত বোঝা যায় না
EEEEসপ্তাহের দিনের নামSaturdayLocale না দিলে কম্পিউটারের ভাষায় আসে

mm ভুলটি বেশ নিরীহ দেখায়, কিন্তু LocalDate-এর ওপর "dd/mm/uuuu" দিয়ে format করলে প্রোগ্রাম থামে UnsupportedTemporalTypeException: Unsupported field: MinuteOfHour দিয়ে — তারিখে মিনিট নেই। আর uuuu বনাম yyyy? yyyy মানে “যুগের বছর” (খ্রিস্টাব্দ/খ্রিস্টপূর্ব যুগের ভেতরে), uuuu মানে সরল বছর-সংখ্যা। সাধারণ ব্যবহারে দুটির ফল এক। পার্থক্য ধরা পড়ে কঠোর যাচাইয়ে: ResolverStyle.STRICT দিলে yyyy যুগ (era) না জেনে তারিখ বানাতে পারে না, তাই "21/02/2026" parse ব্যর্থ হয়। তাই অনেক অভিজ্ঞ ডেভেলপার java.time-এ uuuu লেখেন।

ভুল ইনপুট — DateTimeParseException

ব্যবহারকারীর লেখা তারিখ ভুল হতেই পারে। তখন আসে DateTimeParseException — একটি unchecked exception, যার বার্তা বেশ পরিষ্কার (সব Java 21-এ যাচাই করা):

LocalDate.parse("21-02-2026", BD)
→ Text '21-02-2026' could not be parsed at index 2

LocalDate.parse("21/02/2026")            // formatter ছাড়া = ISO ধরে নেয়
→ Text '21/02/2026' could not be parsed at index 0

LocalDate.parse("2026-02-30")
→ Text '2026-02-30' could not be parsed: Invalid date 'FEBRUARY 30'

“index 2” মানে লেখার তৃতীয় অক্ষরে (গোনা ০ থেকে) সমস্যা — সেখানে / আশা করা হয়েছিল, পাওয়া গেল -. ফর্মে তারিখ নিলে এই exception ধরে ব্যবহারকারীকে বলুন “তারিখ দিন/মাস/বছর রূপে লিখুন”.

একটি অপ্রত্যাশিত আচরণ: ofPattern("dd/MM/uuuu") দিয়ে "31/02/2026" parse করলে কোনো exception হয় না — ফল আসে 2026-02-28! কারণ ডিফল্ট ResolverStyle.SMART ১–৩১-এর মধ্যের দিনকে মাসের শেষ বৈধ দিনে নামিয়ে আনে (৩২ দিলে অবশ্য exception). জন্মতারিখ বা চুক্তির তারিখের মতো জায়গায় এটা চান না। .withResolverStyle(ResolverStyle.STRICT) যোগ করলে একই ইনপুটে আসে Invalid date 'FEBRUARY 31' — তবে তখন প্যাটার্নে yyyy নয়, uuuu লাগবে।

বাংলায় মাস আর দিনের নাম — Locale

মাস বা দিনের নাম কোন ভাষায় আসবে, তা ঠিক করে Locale. Locale না দিলে আসে কম্পিউটারের ডিফল্ট ভাষায় — আপনার ল্যাপটপে এক রকম, সার্ভারে আরেক রকম। তাই সবসময় স্পষ্ট করে দিন:

var bn = DateTimeFormatter.ofPattern("d MMMM uuuu, EEEE", Locale.forLanguageTag("bn"));
var en = DateTimeFormatter.ofPattern("d MMMM uuuu, EEEE", Locale.ENGLISH);
LocalDate d = LocalDate.of(2026, 2, 21);
System.out.println(d.format(bn));   // 21 ফেব্রুয়ারী 2026, শনিবার
System.out.println(d.format(en));   // 21 February 2026, Saturday

Java 21-এ যাচাই করে দেখা গেছে: bn Locale-এ মাসের নাম আসে জানুয়ারী, ফেব্রুয়ারী, মার্চ … ডিসেম্বর, আর দিনের নাম সোমবার … রবিবার। দুটি জিনিস খেয়াল করুন। এক, বানান JDK-র ভেতরের Unicode CLDR ডেটা থেকে আসে (“ফেব্রুয়ারী”, দীর্ঘ ঈ-কার) — বাংলা একাডেমির প্রমিত বানান “ফেব্রুয়ারি” চাইলে নিজের তালিকা বানাতে হবে। দুই, অঙ্ক থেকে যায় ইংরেজি (21, 2026); বাংলা অঙ্ক চাইলে আলাদা ব্যবস্থা লাগে। আর এগুলো খ্রিস্টীয় ক্যালেন্ডারের মাসের বাংলা নাম — বৈশাখ-জ্যৈষ্ঠের বঙ্গাব্দ নয়; বঙ্গাব্দের জন্য JDK-তে কোনো তৈরি ক্যালেন্ডার নেই।

৭ · টাইম জোন — ঢাকা, নিউ ইয়র্ক আর লন্ডন

একটি টাইম জোন শুধু “UTC থেকে কত ঘণ্টা এগিয়ে বা পিছিয়ে” নয়; এটা একটি অঞ্চলের ঘড়ির পুরো ইতিহাস ও নিয়ম — কবে কত অফসেট ছিল, কবে বদলাবে। এই নিয়মগুলো রাখা হয় IANA time zone database-এ, আর JDK নিজের ভেতরে এর একটি কপি নিয়ে আসে। জোনের নাম লেখা হয় “মহাদেশ/শহর” রূপে: Asia/Dhaka, America/New_York, Europe/London. ভুল নাম দিলে: ZoneRulesException: Unknown time-zone ID: Asia/Dhak.

ঢাকা এখন UTC+6, আর বাংলাদেশে বর্তমানে কোনো DST (daylight saving time) নেই — সারা বছর একই অফসেট। (২০০৯ সালে অল্প সময়ের জন্য ছিল; ভাবনার প্রশ্ন ২-এ সেই ইতিহাস।) কিন্তু যুক্তরাষ্ট্র আর যুক্তরাজ্যে আছে: বসন্তে ঘড়ি এক ঘণ্টা এগোয়, শরতে পেছায়। ২০২৬ সালে নিউ ইয়র্কে ঘড়ি এগোবে ৮ মার্চ, পেছাবে ১ নভেম্বর; লন্ডনে এগোবে ২৯ মার্চ (JDK-র জোন-নিয়ম থেকে যাচাই করা)। ফলে ঢাকা আর নিউ ইয়র্কের পার্থক্য বছরের কিছু সময় ১০ ঘণ্টা, কিছু সময় ১১ ঘণ্টা। “নিউ ইয়র্ক = ঢাকা − ১১ ঘণ্টা” হার্ডকোড করা কোড বছরের অর্ধেক সময় ভুল।

একটি মুহূর্ত ভিন্ন শহরের ঘড়িতে কেমন দেখায়, ধাপে ধাপে দেখুন:

1Instant pay = Instant.parse("2026-03-26T04:30:00Z");2var dhaka  = pay.atZone(ZoneId.of("Asia/Dhaka"));3var ny     = pay.atZone(ZoneId.of("America/New_York"));4var london = pay.atZone(ZoneId.of("Europe/London"));5dhaka.isEqual(ny);  dhaka.equals(ny);
দেয়ালঘড়িতে কত বাজে
pay (UTC)
ঢাকা
নিউ ইয়র্ক
লন্ডন
  1. মুহূর্তটি — UTC-তে। একজন গ্রাহক পেমেন্ট করলেন; সার্ভার রেকর্ড করল একটি Instant. এর কোনো “স্থানীয় ঘড়ি” নেই — Z মানে শূন্য অফসেট। ডেটাবেসে এভাবেই রাখা সবচেয়ে নিরাপদ।
  2. ঢাকার ঘড়ি — সকাল ১০:৩০। atZone একই মুহূর্তকে ঢাকার নিয়মে দেখায়: UTC + ৬ ঘণ্টা। মুহূর্ত বদলায়নি, শুধু কোন ঘড়িতে পড়ছি তা বদলেছে।
  3. নিউ ইয়র্কের ঘড়ি — রাত ১২:৩০। ৮ মার্চ থেকে সেখানে DST, তাই অফসেট −৫ নয়, −৪। ঢাকার সাথে পার্থক্য এখন ১০ ঘণ্টা। জোনের নিয়ম জানা না থাকলে এখানেই এক ঘণ্টার ভুল হতো।
  4. লন্ডনের ঘড়ি — ভোর ৪:৩০। লন্ডনের DST শুরু ২৯ মার্চ, তাই ২৬ মার্চ লন্ডন এখনো UTC-র সমান। তিন দিন পরে একই হিসাবে লন্ডনে দেখাত ৫:৩০। মজার ব্যাপার, যুক্তরাষ্ট্র আর ইউরোপ একই দিনে ঘড়ি বদলায় না।
  5. তিনটি একই মুহূর্ত? isEqual সময়রেখার মুহূর্ত তুলনা করে — true. কিন্তু equals তারিখ, সময়, অফসেট আর জোন সব মেলায় — জোন আলাদা, তাই false. “একই সময়ে ঘটেছে কি না” জানতে isEqual বা toInstant() তুলনা করুন।
সংরক্ষণ করুন Instant (UTC)-এ।
দেখানোর সময় ব্যবহারকারীর জোনে রূপান্তর করুন।

এই নীতির পেছনের যুক্তি: মুহূর্ত কখনো দ্ব্যর্থক হয় না, কিন্তু স্থানীয় সময় হতে পারে। তবে ব্যতিক্রম আছে। ভবিষ্যতের মানুষের ঘটনা — “আগামী বছর ১৫ মার্চ সকাল ১০টায় ঢাকায় মিটিং” — অনেক সময় স্থানীয় সময় আর জোন হিসেবে রাখাই ভালো, কারণ ততদিনে সরকার জোনের নিয়ম বদলে দিলে মানুষ আসবেন “সকাল ১০টায়”, পুরোনো হিসাবের মুহূর্তে নয়। OffsetDateTime শুধু একটি নির্দিষ্ট অফসেট (+06:00) রাখে, জোনের নিয়ম নয় — API-তে সময় আদান-প্রদানে এটা বেশ চলে, কারণ লেখাটি দ্ব্যর্থহীন।

প্রতিটি দিন ২৪ ঘণ্টার নয় — DST-এর ফাঁক

নিউ ইয়র্কে ৮ মার্চ ২০২৬ রাত ২টায় ঘড়ি সরাসরি ৩টায় লাফ দেবে। অর্থাৎ সেদিন ২:০০ থেকে ২:৫৯ পর্যন্ত কোনো সময় সেখানে অস্তিত্বই রাখে না। আর সেদিন মাত্র ২৩ ঘণ্টার। ZonedDateTime দিয়ে “নিউ ইয়র্কে ৮ মার্চ ২:৩০” বানাতে চাইলে Java ফাঁকের দৈর্ঘ্য পরিমাণ এগিয়ে দেয়: ফল 03:30-04:00. নভেম্বরের উল্টো দিনে ১:০০–১:৫৯ দুইবার আসে, দিনটি ২৫ ঘণ্টার। তাই “দিন = ২৪ ঘণ্টা” ধরে নেওয়া কোড — যেমন plusHours(24) দিয়ে “কাল একই সময়” বের করা — DST অঞ্চলে বছরে দুইবার ভুল করে। “কাল একই ঘড়ির সময়” চাইলে plusDays(1), “ঠিক ২৪ ঘণ্টা পরে” চাইলে plusHours(24) — দুটি আলাদা প্রশ্ন।

৮ · Clock, TemporalAdjusters আর বাস্তব হিসাব

now() সবখানে নয় — Clock দিয়ে টেস্টযোগ্য কোড

একটি ডেলিভারি অ্যাপের কথা ভাবুন: “৩ কর্মদিবসের মধ্যে পৌঁছাবে।” কোডটি যদি ভেতরে LocalDate.now() ডাকে, তাহলে এর টেস্ট আজ পাস করবে, কাল হয়তো ফেল করবে — কারণ ফল নির্ভর করে কোন দিন চালাচ্ছেন তার ওপর। বৃহস্পতিবারে অর্ডার হলে শুক্র-শনি বাদ যাওয়ার নিয়ম ঠিক আছে কি না, সেটা টেস্ট করতে কি বৃহস্পতিবার পর্যন্ত অপেক্ষা করবেন?

সমাধান Clock. java.time-এর সব now() method-এর একটি রূপ আছে যা একটি Clock নেয়: LocalDate.now(clock), Instant.now(clock). আসল অ্যাপে দিন Clock.systemDefaultZone() বা Clock.system(ZoneId.of("Asia/Dhaka")); টেস্টে দিন Clock.fixed(...) — যে ঘড়ি সবসময় একই মুহূর্ত দেখায়। ঘড়িকে বাইরে থেকে পাঠানো — এটা পর্ব ২১-এর dependency injection, আর fixed clock আসলে একটি সহজ mock. পর্ব ৪৭-এ JUnit দিয়ে এভাবেই টেস্ট লিখবেন।

TemporalAdjusters — “পরের শুক্রবার”, “মাসের শেষ দিন”

অনেক ব্যবসার নিয়ম ক্যালেন্ডার-ভাষায় লেখা: “বেতন মাসের শেষ দিনে”, “মিটিং পরের রবিবার”. এগুলোর জন্য TemporalAdjusters-এ তৈরি নিয়ম আছে, যা with(...)-এ দিতে হয়। ১৮ অক্টোবর ২০২৬ (রবিবার) থেকে যাচাই করা ফল:

LocalDate t = LocalDate.of(2026, 10, 18);                         // রবিবার
t.with(TemporalAdjusters.next(DayOfWeek.FRIDAY));                // 2026-10-23
t.with(TemporalAdjusters.lastDayOfMonth());                      // 2026-10-31
t.with(TemporalAdjusters.lastInMonth(DayOfWeek.THURSDAY));       // 2026-10-29
t.with(TemporalAdjusters.next(DayOfWeek.SUNDAY));                // 2026-10-25
t.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));          // 2026-10-18

শেষ দুই লাইনের পার্থক্য লক্ষ করুন: next আজ রবিবার হলেও পরের রবিবারে যায়, nextOrSame আজকেই থাকে। একটি পরীক্ষার রুটিন বানাতে — রবিবার আর বুধবারে পরীক্ষা, ১৮ অক্টোবরের পর থেকে — একটি EnumSet<DayOfWeek> (পর্ব ২৪) আর একটি লুপই যথেষ্ট:

Set<DayOfWeek> examDays = EnumSet.of(DayOfWeek.SUNDAY, DayOfWeek.WEDNESDAY);
var f = DateTimeFormatter.ofPattern("dd/MM/uuuu EEEE", Locale.forLanguageTag("bn"));
LocalDate e = LocalDate.of(2026, 10, 18);
for (String subject : List.of("বাংলা", "ইংরেজি", "গণিত", "ICT")) {
    do { e = e.plusDays(1); } while (!examDays.contains(e.getDayOfWeek()));
    System.out.println(subject + " → " + e.format(f));
}

ছাপা হয় (Java 21-এ যাচাই করা): বাংলা → 21/10/2026 বুধবার, ইংরেজি → 25/10/2026 রবিবার, গণিত → 28/10/2026 বুধবার, ICT → 01/11/2026 রবিবার. সরকারি ছুটির দিনও বাদ দিতে চাইলে একটি Set<LocalDate> রেখে লুপের শর্তে যোগ করুন — LocalDate-এর equals আর hashCode মান-ভিত্তিক, তাই Set-এ ঠিকঠাক কাজ করে।

এবার সব একসাথে: fixed clock দিয়ে ডেলিভারির কর্মদিবস (শুক্র-শনি বাদ), ঢাকা থেকে নিউ ইয়র্কে মিটিংয়ের সময় DST-এর আগে ও পরে, আর DST-এর দিনের দৈর্ঘ্য। বাংলাদেশে সরকারি অফিসে শুক্র-শনি সাপ্তাহিক ছুটি, তবে অনেক প্রতিষ্ঠানে নিয়ম আলাদা — তাই আসল কোডে ছুটির দিনগুলো কনফিগারেশন থেকে আসা উচিত।

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

class Main {
    static final ZoneId DHAKA = ZoneId.of("Asia/Dhaka"), NY = ZoneId.of("America/New_York");
    static LocalDate deliveryDate(Clock clock, int workingDays) {
        LocalDate d = LocalDate.now(clock);                // ঘড়ি বাইরে থেকে আসে
        while (workingDays > 0) {
            d = d.plusDays(1);
            if (d.getDayOfWeek() != DayOfWeek.FRIDAY && d.getDayOfWeek() != DayOfWeek.SATURDAY) workingDays--;
        }
        return d;
    }
    public static void main(String[] args) {
        Clock clock = Clock.fixed(Instant.parse("2026-10-15T04:00:00Z"), DHAKA);
        System.out.println("অর্ডার " + LocalDateTime.now(clock) + ", ডেলিভারি " + deliveryDate(clock, 3));
        DateTimeFormatter bn = DateTimeFormatter.ofPattern("d MMMM, EEEE", Locale.forLanguageTag("bn"));
        for (LocalDate day : new LocalDate[] { LocalDate.of(2026, 10, 26), LocalDate.of(2026, 11, 2) }) {
            ZonedDateTime meet = ZonedDateTime.of(day, LocalTime.of(20, 0), DHAKA);
            System.out.println(meet.format(bn) + " | ঢাকা " + meet.toLocalTime() + " = নিউ ইয়র্ক "
                    + meet.withZoneSameInstant(NY).toLocalTime() + " | UTC " + meet.toInstant());
        }
        ZonedDateTime m = ZonedDateTime.of(LocalDate.of(2026, 3, 8), LocalTime.MIDNIGHT, NY);
        System.out.println("৮ মার্চ নিউ ইয়র্কের দিন: " + Duration.between(m, m.plusDays(1)));
    }
}

আউটপুট চার লাইন: অর্ডার 2026-10-15T10:00, ডেলিভারি 2026-10-20; 26 অক্টোবর, সোমবার | ঢাকা 20:00 = নিউ ইয়র্ক 10:00 | UTC 2026-10-26T14:00:00Z; 2 নভেম্বর, সোমবার | ঢাকা 20:00 = নিউ ইয়র্ক 09:00 | UTC 2026-11-02T14:00:00Z; ৮ মার্চ নিউ ইয়র্কের দিন: PT23H. fixed clock বলছে “এখন” ঢাকায় বৃহস্পতিবার ১৫ অক্টোবর সকাল ১০টা — তাই শুক্র-শনি বাদ দিয়ে রবি, সোম, মঙ্গল গুনে ডেলিভারি ২০ অক্টোবর; যেদিনই চালান, একই ফল। দুই সোমবারের মিটিং ঢাকার ঘড়িতে একই রাত ৮টা, UTC-তেও একই ১৪:০০ — কিন্তু নিউ ইয়র্কে প্রথমটি সকাল ১০টা, দ্বিতীয়টি ৯টা, কারণ মাঝে ১ নভেম্বর সেখানে DST শেষ হয়েছে। শেষ লাইনটি DST-এর ফাঁক: নিউ ইয়র্কে সেদিন মাত্র ২৩ ঘণ্টা। অংশ ৭-এর মতো LocalTime.of(2, 30) দিয়ে সেদিনের একটি ZonedDateTime বানিয়ে ছাপুন — দেখবেন অস্তিত্বহীন ২:৩০ এগিয়ে 03:30-04:00 হয়ে গেছে।

সাধারণ ভুলের তালিকা

ভুলকী হয়ঠিক পথ
বিশ্বব্যাপী ঘটনায় LocalDateTimeকোন জোনের সময়, হারিয়ে যায়; সার্ভার সরালে ঘণ্টা ভুলInstant রাখুন, দেখাতে ZonedDateTime
নতুন কোডে Date/Calendarmutable, ০-ভিত্তিক মাস, জোন-বিভ্রান্তিjava.time; পুরোনো API-র সাথে শুধু সীমানায় রূপান্তর
“দিন = ২৪ ঘণ্টা”DST-এর দিনে এক ঘণ্টা ভুলক্যালেন্ডারের দিনে plusDays/Period
অফসেট হার্ডকোড (“নিউ ইয়র্ক −১১”)বছরের কিছু অংশে ভুলZoneId.of("America/New_York")
ব্যবসার কোডে সরাসরি now()টেস্ট দিনের ওপর নির্ভরশীলClock বাইরে থেকে দিন
x.plusDays(1); ফল না রাখাকিছুই বদলায় নাx = x.plusDays(1);
মোট দিনে Period.getDays()শুধু বাকি দিন আসেChronoUnit.DAYS.between
C/C++/Python জানলে

C-তে সময় মানে time_t (epoch থেকে সেকেন্ড) আর struct tm, যেখানে tm_mon-ও ০ থেকে ১১ — পুরোনো Java Calendar-এর মতোই (দেখুন C-এর standard library). C++-এর <chrono>-এ duration আর time_point আছে, আর C++20 থেকে ক্যালেন্ডার ও টাইম জোনও (C++ standard library ভ্রমণ)। Python-এর datetime-এ date, time, datetime, timedelta — কিন্তু একই datetime class জোনসহ (aware) আর জোন ছাড়া (naive) দুই রূপেই থাকে, যেখানে Java আলাদা টাইপ দেয়; বিস্তারিত Python standard library ভ্রমণ পর্বে।

পরীক্ষা ও ইন্টারভিউতে
  • Date আর LocalDate-এর পার্থক্য? — Date mutable, আসলে একটি মুহূর্ত রাখে; LocalDate immutable, শুধু তারিখ — সময় বা জোন নেই।
  • LocalDateTime, ZonedDateTime আর Instant? — প্রথমটি জোন ছাড়া তারিখ-সময়; দ্বিতীয়টি জোনের নিয়মসহ; তৃতীয়টি UTC-তে সময়রেখার একটি মুহূর্ত।
  • Period আর Duration? — Period বছর-মাস-দিনে (ক্যালেন্ডার), Duration সেকেন্ড-ন্যানোসেকেন্ডে (ঘড়ি)।
  • java.time কি thread-safe? — হ্যাঁ, এর class-গুলো immutable; DateTimeFormatter-ও thread-safe, যা SimpleDateFormat নয়।
  • yyyy আর YYYY? — প্রথমটি ক্যালেন্ডারের বছর; দ্বিতীয়টি সপ্তাহভিত্তিক বছর, যা বছরের শেষ বা শুরুর কয়েক দিনে আলাদা হতে পারে।

৯ · AI-এর যুগে — তারিখ-সময়ের কোড যাচাই

তারিখ-সময়ের bug সবচেয়ে বিপজ্জনক এই কারণে যে এগুলো বছরের বেশিরভাগ সময় লুকিয়ে থাকে। AI-এর লেখা কোড demo-তে নিখুঁত চলে, কারণ demo চলে আজ, আপনার ল্যাপটপে, ঢাকার জোনে। সমস্যা দেখা দেয় ৩১ ডিসেম্বরে, DST-এর রাতে, বা সার্ভার বিদেশে সরানোর পর। AI শিখেছে ইন্টারনেটের কোটি কোটি লাইন থেকে, যার বড় অংশ java.time আসার আগের — তাই নিচের ভুলগুলো খুব সাধারণ।

সবচেয়ে সূক্ষ্মটি week-based year: প্যাটার্নে yyyy-এর জায়গায় YYYY. বছরের বেশিরভাগ দিনে দুটির ফল এক, তাই টেস্টে ধরা পড়ে না। কিন্তু Locale.US-এ ৩০ ডিসেম্বর ২০২৬ format করলে YYYY-MM-dd দেয় 2027-12-30 — পুরো এক বছর সামনে! কারণ যুক্তরাষ্ট্রের নিয়মে সপ্তাহ শুরু রবিবারে, আর ২৭ ডিসেম্বর (রবিবার) থেকে শুরু হওয়া সপ্তাহে ১ জানুয়ারি ২০২৭ আছে, তাই পুরো সপ্তাহটি “২০২৭-এর প্রথম সপ্তাহ”। Locale.UK-এ (ISO নিয়ম) উল্টো ঘটনা: ১ জানুয়ারি ২০২৭ দেখায় 2026-01-01 (দুটিই Java 21-এ যাচাই করা)। অর্থাৎ ভুলটি Locale-ভেদে বছরের শেষে বা শুরুতে আসে।

AI যা লেখেকেন সমস্যাআপনি যা চাইবেন
ব্যবসার method-এর ভেতরে LocalDateTime.now()জোন অস্পষ্ট, সার্ভারের জোনের ওপর নির্ভর; টেস্ট করা যায় নাClock parameter বা field; মুহূর্তে Instant.now(clock)
new Date(), SimpleDateFormat, Calendarmutable, thread-safe নয়, ০-ভিত্তিক মাসjava.time আর static final DateTimeFormatter
পেমেন্ট/অর্ডারের সময় LocalDateTime-এ সেভকোন জোনের সময়, জানা যায় নাInstant (ডেটাবেসে UTC timestamp)
"YYYY-MM-dd"বছরের শেষ/শুরুতে ভুল বছর"uuuu-MM-dd" বা "yyyy-MM-dd"
plusHours(24) দিয়ে “কাল”DST-এর দিনে ভুলplusDays(1)
লুপে d = d.plusMonths(1) দিয়ে কিস্তি৩১ তারিখ একবার ২৮ হলে সারাজীবন ২৮start.plusMonths(i)

AI-কে তারিখ-সময়ের কোড লেখাতে বা রিভিউ করাতে এই কাঠামো ব্যবহার করুন:

আমি Java 21-এ একটি ব্যবসার অ্যাপ লিখছি; java.time জানি। কাজ: [যেমন — ঋণের মাসিক কিস্তির তারিখ আর দেরির জরিমানা হিসাব] শর্ত: ১. শুধু java.time; Date, Calendar, SimpleDateFormat একদম নয়। ২. ব্যবসার কোডে সরাসরি now() ডাকবে না; একটি java.time.Clock বাইরে থেকে নেবে, যাতে টেস্টে Clock.fixed দেওয়া যায়। ৩. প্রতিটি ভ্যারিয়েবলের টাইপ বেছে নেওয়ার কারণ এক লাইনে লেখো: LocalDate, Instant, নাকি ZonedDateTime. ৪. সংরক্ষণে Instant (UTC); দেখানোর সময় ZoneId.of("Asia/Dhaka") — কোনো অফসেট হার্ডকোড নয়। ৫. formatter-এ yyyy বা uuuu, কখনো YYYY নয়; Locale স্পষ্ট করে দাও। ৬. মাসিক তারিখ মূল তারিখ থেকে plusMonths(i) দিয়ে; মোট দিনের জন্য ChronoUnit.DAYS.between. ৭. শেষে কয়েকটি টেস্ট-কেস দাও: ৩১ জানুয়ারি, leap year-এর ২৯ ফেব্রুয়ারি, ৩০-৩১ ডিসেম্বর, আর DST আছে এমন একটি জোনে ঘড়ি বদলের দিন।

AI-এর কোড হাতে পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Joda-Time এত জনপ্রিয় ছিল, তবু Java 8-এ সেটিকে হুবহু নেওয়া হলো না — JSR 310 নতুন করে নকশা করল। কেন?

শুরুর কষ্ট। Java 1.0-এর Date class ছিল তাড়াহুড়োর নকশা: নাম তারিখ, ভেতরে মুহূর্ত; বছর লিখতে হতো ১৯০০ বাদ দিয়ে; মাস ০ থেকে। পরের সংস্করণে Calendar আসে, যা আরও শক্তিশালী কিন্তু আরও জটিল আর একই রকম mutable. দুই দশক ধরে Java ডেভেলপাররা এর চারপাশে সহায়ক class লিখেছেন, আর thread-safety-র bug খুঁজেছেন।

Joda-Time. এই শূন্যস্থানে Stephen Colebourne তৈরি করেন Joda-Time — immutable টাইপ, পরিষ্কার নাম, সঠিক টাইম জোন হিসাব। ২০০০-এর দশকে এটি এত ছড়িয়ে পড়ে যে অনেক প্রজেক্টে “তারিখ মানেই Joda”. স্বাভাবিক প্রশ্ন ছিল: এটাকেই JDK-তে ঢুকিয়ে দিলে হয় না?

কেন হুবহু নয়। Colebourne নিজেই এর বিপক্ষে যুক্তি দিয়েছিলেন। তাঁর মতে Joda-Time-এর কিছু মৌলিক নকশাগত দুর্বলতা ছিল। যেমন, মানুষের সময় (ক্যালেন্ডার-ঘড়ি) আর যন্ত্রের সময় (সময়রেখার মুহূর্ত) যথেষ্ট আলাদা করা হয়নি; অনেক method null পেলে চুপচাপ “এখন” ধরে নিত, যা bug লুকায়; ভিন্ন ক্যালেন্ডার-ব্যবস্থা প্লাগ করার নমনীয়তা সাধারণ কোডকে জটিল করত। JDK-তে একবার কিছু ঢুকলে সেটা কার্যত চিরস্থায়ী — পরে ঠিক করা প্রায় অসম্ভব। তাই JSR 310-এ, যার সহ-নেতৃত্বে ছিলেন Colebourne আর Michael Nascimento Santos, আর পরে Oracle-এর প্রকৌশলীরাও যুক্ত হন, অভিজ্ঞতা রেখে নকশা নতুন করা হলো: Instant যন্ত্রের জন্য, Local... মানুষের জন্য, null গ্রহণ নয়, ISO ক্যালেন্ডার প্রধান।

দীর্ঘ পথ। কাজটি Java 7-এ ঢোকেনি; স্পেসিফিকেশন, রেফারেন্স ইমপ্লিমেন্টেশন আর টেস্ট শেষ করে java.time আসে Java 8-এ, মার্চ ২০১৪-তে। পুরোনো Java-র জন্য ThreeTen-Backport নামে একটি লাইব্রেরিও বানানো হয়। Android-এ নতুন সংস্করণগুলোতে java.time সরাসরি পাওয়া যায়। আর Joda-Time-এর ওয়েবসাইট এখন নিজেই বলে, Java 8 বা পরের সংস্করণে java.time ব্যবহার করুন।

মূল উপলব্ধি: জনপ্রিয় আর সঠিক এক জিনিস নয়। স্থায়ী মানদণ্ডে কিছু ঢোকানোর আগে অভিজ্ঞতা থেকে শেখা ভুলগুলো ঠিক করে নেওয়া — এমনকি নিজের তৈরি জনপ্রিয় লাইব্রেরির ভুলও — ভালো প্রকৌশলের লক্ষণ।

প্র ০২টাইম জোনের নিয়ম কোথা থেকে আসে, আর বাংলাদেশের ২০০৯ সালের DST-এর অভিজ্ঞতা IANA database-এ কীভাবে ধরা আছে?

জোন মানে রাজনীতি। পৃথিবী ঘোরে নিয়মমতো, কিন্তু কোন দেশের ঘড়ি কত অফসেটে চলবে, তা ঠিক করে সেই দেশের সরকার — আর সরকার সেটা বদলায়, কখনো মাত্র কয়েক সপ্তাহের নোটিশে। একটি সফটওয়্যারকে “২০০৯ সালের ১ জুলাই ঢাকায় সন্ধ্যা ৭টা” সঠিকভাবে UTC-তে রূপান্তর করতে হলে জানতে হবে সেদিন ঢাকার নিয়ম কী ছিল। এজন্যই জোন শুধু একটি অফসেট নয়, একটি পূর্ণ ইতিহাস।

IANA tz database. এই ইতিহাস সংগ্রহ করা হয় একটি স্বেচ্ছাসেবী-চালিত ডেটাবেসে, যা শুরু করেছিলেন Arthur David Olson — তাই একে “Olson database”-ও বলে; দীর্ঘদিন এর সম্পাদনায় যুক্ত Paul Eggert. ২০১১ সাল থেকে এটি IANA (যে সংস্থা ইন্টারনেটের ডোমেইন ও নম্বর-ব্যবস্থার সমন্বয় করে) দেখাশোনা করে। বছরে কয়েকবার নতুন সংস্করণ বের হয়, নাম যেমন 2023c. Linux, macOS, Android, ব্রাউজার আর JDK — সবাই এই ডেটা ব্যবহার করে। JDK নিজের ভেতরে একটি কপি রাখে; যেমন এই কোর্সের অনলাইন runner-এর Java 21-এ আছে সংস্করণ 2023c (চালিয়ে যাচাই করা)। পুরোনো JDK-তে নতুন নিয়ম পেতে JDK আপডেট করতে হয়, অথবা Oracle-এর TZUpdater-এর মতো টুল লাগে।

বাংলাদেশের অভিজ্ঞতা। বিদ্যুৎ-ঘাটতি আর লোডশেডিং কমানোর লক্ষ্যে ২০০৯ সালে বাংলাদেশ সরকার DST চালু করে: ১৯ জুন রাত ১১টায় ঘড়ি এক ঘণ্টা এগিয়ে দেওয়া হয়, ঢাকা হয় UTC+7. বছরের শেষে, ৩১ ডিসেম্বর মধ্যরাতে, ঘড়ি আবার UTC+6-এ ফেরে, আর তারপর DST আর চালু হয়নি। JDK-র Asia/Dhaka নিয়মে এই দুটি পরিবর্তন হুবহু আছে — getTransitions() ডাকলে দেখা যায় ২০০৯-০৬-১৯ তারিখে +06:00 থেকে +07:00-এ “Gap”, আর বছর শেষে +07:00 থেকে +06:00-এ “Overlap”. এর আগের পরিবর্তনগুলোও আছে, যেমন ১৯৫১ সালে +06:30 থেকে +06:00-এ আসা। তাই ২০০৯ সালের জুলাইয়ের কোনো লগের স্থানীয় সময় Java সঠিকভাবে UTC-তে নিতে পারে — যদি আপনি ZoneId.of("Asia/Dhaka") ব্যবহার করেন, হার্ডকোড করা “+6” নয়।

মূল উপলব্ধি: টাইম জোন হলো মানুষের সিদ্ধান্তের ইতিহাস, পদার্থবিদ্যা নয়। তাই অফসেট কখনো হার্ডকোড করবেন না; জোনের নাম ব্যবহার করুন আর JDK হালনাগাদ রাখুন — জোন-নিয়মও নিরাপত্তা আপডেটের মতো রক্ষণাবেক্ষণের বিষয়।

প্র ০৩“২০৩৮ সালের সমস্যা” কী, Java-র Instant কি এ থেকে মুক্ত, আর তবু Java প্রোগ্রাম কীভাবে এতে পড়তে পারে?

epoch আর ৩২ বিট। Unix আর C-এর জগতে সময় রাখা হয় epoch — ১ জানুয়ারি ১৯৭০, ০০:০০ UTC — থেকে পার হওয়া সেকেন্ড হিসেবে, time_t টাইপে। বহু পুরোনো সিস্টেমে time_t একটি signed ৩২-বিট পূর্ণসংখ্যা, যার সর্বোচ্চ মান 2,147,483,647. Java-তে Instant.ofEpochSecond(Integer.MAX_VALUE) ছাপলে আসে 2038-01-19T03:14:07Z — ঢাকার ঘড়িতে সেদিন সকাল ৯:১৪:০৭। এর এক সেকেন্ড পরে ৩২-বিট কাউন্টার overflow করে ঋণাত্মক হয়ে যায় (পর্ব ৭-এর integer overflow মনে করুন), আর সময় পিছিয়ে যায় ১৯০১ সালের ডিসেম্বরে। একেই বলে ২০৩৮ সালের সমস্যা — Y2K-র মতো, কিন্তু আরও গভীরে, কারণ এটা অনেক সময় হার্ডওয়্যার আর ফাইল-ফরম্যাটের ভেতরে বসানো।

Java কোথায় নিরাপদ। Java শুরু থেকেই System.currentTimeMillis() দিয়েছে long (৬৪ বিট) মিলিসেকেন্ডে, যা কোটি কোটি বছর চলবে। Instant রাখে একটি long সেকেন্ড আর একটি int ন্যানোসেকেন্ড; এর সীমা যাচাই করে দেখা যায় Instant.MIN = -1000000000-01-01T00:00:00Z আর Instant.MAX = +1000000000-12-31T23:59:59.999999999Z — প্রায় একশো কোটি বছর দুই দিকে। অর্থাৎ Java-র ভেতরের হিসাবে ২০৩৮ কোনো সমস্যা নয়।

তবু কোথায় বিপদ। Java প্রোগ্রাম একা চলে না। ডেটাবেসের কোনো কলাম যদি ৩২-বিট timestamp হয় — যেমন MySQL-এর TIMESTAMP টাইপের সর্বোচ্চ সীমা ২০৩৮ সালের ১৯ জানুয়ারি — তাহলে Java-র ঠিক Instant-ও সেভ করার সময় আটকে যাবে। পুরোনো বাইনারি ফাইল-ফরম্যাট, নেটওয়ার্ক প্রোটোকল, বা কোনো এমবেডেড যন্ত্রের (যেমন পুরোনো রাউটার বা শিল্প-নিয়ন্ত্রক) সাথে ৪ বাইটে সময় আদান-প্রদান হলেও একই কথা। আর আজই সমস্যা হতে পারে: ৩০ বছরের গৃহঋণ বা দীর্ঘমেয়াদি বন্ডের শেষ তারিখ ২০৩৮ পেরিয়ে যায়, তাই ব্যাংকিং সফটওয়্যার এই সীমা অনেক আগেই ছুঁয়েছে।

মূল উপলব্ধি: আপনার ভাষার টাইপ নিরাপদ হলেও সিস্টেমের দুর্বলতম অংশ — ডেটাবেস কলাম, ফাইল-ফরম্যাট, পুরোনো যন্ত্র — সীমা নির্ধারণ করে। সময় সংরক্ষণের প্রতিটি সীমানায় জিজ্ঞেস করুন: কত বিট, কোন একক, কত সাল পর্যন্ত?

প্র ০৪UTC-তে মাঝে মাঝে একটি leap second যোগ হয়, অথচ Java-র Instant ২৩:৫৯:৬০ দেখাতে পারে না। Java তাহলে সময় মাপে কীভাবে?

দুই রকম সময়। পারমাণবিক ঘড়ির সেকেন্ড একেবারে সুষম। কিন্তু পৃথিবীর ঘূর্ণন সামান্য অনিয়মিত আর দীর্ঘমেয়াদে ধীর হচ্ছে। দুটিকে কাছাকাছি রাখতে ১৯৭২ সাল থেকে UTC-তে মাঝে মাঝে একটি বাড়তি সেকেন্ড — leap second — যোগ করা হয়, সাধারণত ৩০ জুন বা ৩১ ডিসেম্বরের শেষে; তখন ঘড়ি দেখায় ২৩:৫৯:৬০। এখন পর্যন্ত ২৭টি যোগ হয়েছে, শেষটি ২০১৬ সালের ৩১ ডিসেম্বর। সমস্যা হলো, এগুলো আগে থেকে নিয়মিত হিসাব করা যায় না; ঘোষণা আসে কয়েক মাস আগে।

সফটওয়্যারের কষ্ট। বেশিরভাগ কম্পিউটার ধরে নেয় প্রতিটি দিন ঠিক ৮৬,৪০০ সেকেন্ড। leap second এলে কোনো সিস্টেম একই সেকেন্ড দুইবার দেখায়, কোনোটি ঘড়ি পেছায় — আর সময় পেছালে “আগের ঘটনা পরে ঘটেছে” মনে হয়। ২০১২ সালের leap second-এর সময় Linux kernel-এর একটি ত্রুটিতে বহু সার্ভারে Java সহ বিভিন্ন প্রোগ্রামের CPU ব্যবহার হঠাৎ আকাশ ছুঁয়েছিল, আর বেশ কয়েকটি বড় ওয়েবসাইট সমস্যায় পড়েছিল।

Java-র সমাধান। Instant-এর Javadoc বলে, Java নিজের একটি সময়-মাপকাঠি ব্যবহার করে — “Java Time-Scale” — যেখানে প্রতিটি দিন ঠিক ৮৬,৪০০ ভাগে বিভক্ত। leap second-এর দিনে বাড়তি সেকেন্ডটি আলাদা করে দেখানো হয় না; বর্তমান যুগের জন্য এটি UTC-SLS (smoothed leap seconds) নামের একটি প্রস্তাবের সাথে সামঞ্জস্যপূর্ণ, যেখানে বাড়তি সেকেন্ডটি দিনের শেষ ১০০০ সেকেন্ডে অল্প অল্প করে ছড়িয়ে দেওয়া হয় — প্রতিটি সেকেন্ড সামান্য লম্বা। ফল: সময় কখনো পেছায় না, কখনো ২৩:৫৯:৬০ আসে না, প্রতিটি দিনের হিসাব সরল থাকে। বিনিময়ে leap second-এর আশপাশে Java-র সেকেন্ড পারমাণবিক সেকেন্ডের সাথে নিখুঁতভাবে মেলে না — ব্যবসার কাজে যা নগণ্য, তবে বৈজ্ঞানিক বা উপগ্রহ-সংক্রান্ত সফটওয়্যারে জরুরি। Google-এর মতো প্রতিষ্ঠান সার্ভারে একই চিন্তা প্রয়োগ করেছে, যাকে বলে “leap smear”.

ভবিষ্যৎ। সমস্যা এত বেশি যে ২০২২ সালে ওজন ও পরিমাপের আন্তর্জাতিক সম্মেলন (CGPM) সিদ্ধান্ত নিয়েছে, ২০৩৫ সালের মধ্যে leap second যোগ করা বন্ধ হবে — তার বদলে UTC আর পৃথিবীর ঘূর্ণনের পার্থক্য অনেক বড় হতে দেওয়া হবে।

মূল উপলব্ধি: নিখুঁত বাস্তবতা আর সরল প্রোগ্রামিং মডেলের মধ্যে Java সচেতনভাবে সরলতা বেছে নিয়েছে। প্রতিটি অ্যাবস্ট্রাকশন কোথাও একটু নির্ভুলতা ছাড়ে — কোথায় ছাড়ে, তা জানা থাকলে কখন সেটা গুরুত্বপূর্ণ, তাও বোঝা যায়।

প্র ০৫YYYY লেখার bug বছরের ৩৬০ দিন লুকিয়ে থাকে। বাস্তব অ্যাপে এটি কীভাবে বিপর্যয় ঘটিয়েছে, আর কীভাবে আগেভাগে ধরা যায়?

সপ্তাহভিত্তিক বছর কেন আছে। কারখানা, হিসাবরক্ষণ আর সরবরাহ-ব্যবস্থায় অনেক সময় সপ্তাহের নম্বর দিয়ে পরিকল্পনা হয় — “২০২৭ সালের ৫ নম্বর সপ্তাহে চালান”. ISO 8601 নিয়মে সপ্তাহ শুরু সোমবারে, আর বছরের প্রথম সপ্তাহ হলো যেটিতে সেই বছরের প্রথম বৃহস্পতিবার আছে। ফলে ডিসেম্বরের শেষ কয়েক দিন পরের বছরের প্রথম সপ্তাহে পড়তে পারে, বা জানুয়ারির প্রথম কয়েক দিন আগের বছরের শেষ সপ্তাহে। এই “সপ্তাহের বছর”-ই প্যাটার্নের Y. যুক্তরাষ্ট্রের মতো কিছু Locale-এ নিয়ম আলাদা (সপ্তাহ শুরু রবিবারে), তাই কোন দিনে বছর বদলায়, তাও Locale-ভেদে বদলায় — এই পর্বেই দেখেছেন, US-এ ২৭–৩১ ডিসেম্বর ২০২৬ দেখায় ২০২৭, আর UK-তে ১–৩ জানুয়ারি ২০২৭ দেখায় ২০২৬।

কেন লুকিয়ে থাকে। বছরের প্রায় সব দিনে YYYY আর yyyy হুবহু এক ফল দেয়। ডেভেলপার পরীক্ষা করেন, কোড রিভিউয়ার চোখ বুলান, টেস্ট পাস করে — সবকিছু ঠিক। তারপর বছরের শেষ সপ্তাহে হঠাৎ তারিখ এক বছর লাফিয়ে যায়।

বাস্তব ঘটনা। এই ধরনের ঘটনা বহুবার ঘটেছে। ২০১৪ সালের ডিসেম্বরের শেষ দিকে একটি জনপ্রিয় সামাজিক যোগাযোগ অ্যাপের মোবাইল সংস্করণে বহু ব্যবহারকারী হঠাৎ লগ-আউট হয়ে যান, আবার ঢুকতে পারছিলেন না; পরে জানা যায়, তারিখ format করার কোডে ছিল সপ্তাহের বছর, ফলে সার্ভারের কাছে অনুরোধগুলো যেন এক বছর ভবিষ্যৎ থেকে আসছিল। অন্য নানা সিস্টেমে টিকিটের মেয়াদ, সার্টিফিকেটের তারিখ বা রিপোর্টের ফাইল-নাম এক বছর এগিয়ে যাওয়ার ঘটনাও রিপোর্ট হয়েছে। প্রতিটির মূল একই — একটি বড় হাতের অক্ষর।

কীভাবে ধরবেন। প্রথমত, টেস্টে নির্দিষ্ট তারিখ: Clock.fixed দিয়ে ২৮–৩১ ডিসেম্বর আর ১–৩ জানুয়ারি — এবং একাধিক Locale-এ — চালানো। দ্বিতীয়ত, static analysis: IntelliJ IDEA এ ধরনের প্যাটার্নে সতর্ক করে, আর Error Prone-এর মতো টুলে এর জন্য আলাদা নিয়ম আছে। তৃতীয়ত, অভ্যাস: java.time-এ uuuu লিখলে Y-এর সাথে গুলিয়ে ফেলার সুযোগই কমে। আর মেশিন-থেকে-মেশিন আদান-প্রদানে নিজের প্যাটার্ন না বানিয়ে DateTimeFormatter.ISO_LOCAL_DATE-এর মতো তৈরি formatter ব্যবহার করা।

মূল উপলব্ধি: যে bug বছরে মাত্র কয়েক দিন দেখা দেয়, তা সাধারণ পরীক্ষায় ধরা পড়ে না — ধরতে হয় সীমানার তারিখগুলো ইচ্ছে করে পরীক্ষা করে। নিয়ন্ত্রণযোগ্য ঘড়ি (Clock) ছাড়া সেই পরীক্ষা সম্ভবই নয়।

অনুশীলন

বেশিরভাগ কাজে নতুন কোড লিখতে হবে না — ওপরের কোড বদলে দেখা আর ভাবাই আসল কাজ।

  1. আউটপুট অনুমান করুন: অংশ ৪-এর প্রোগ্রামে জন্মতারিখ বদলে LocalDate.of(2004, 10, 19) দিন — অর্থাৎ জন্মদিন “আজ”-এর এক দিন পরে। Period কী হবে? বয়স কি ২২?

    P21Y11M29D — ২১ বছর ১১ মাস ২৯ দিন (Java 21-এ যাচাই করা)। জন্মদিন আসতে এক দিন বাকি, তাই ২২ নয়। হিসাবটা: ১৯ অক্টোবর ২০২৫-এ ২১ বছর, ১৯ সেপ্টেম্বর ২০২৬-এ ১১ মাস, তারপর সেপ্টেম্বরের বাকি ১১ দিন আর অক্টোবরের ১৮ দিন = ২৯ দিন। বয়স বের করতে “বছরের পার্থক্য” (২০২৬ − ২০০৪ = ২২) নিলে এখানেই ভুল হতো — এজন্য Period.between(...).getYears().

  2. চেইনের ফাঁদ নিজে দেখুন: একই প্রোগ্রামের কিস্তির লুপ বদলে লিখুন — একটি LocalDate d = loan; নিয়ে লুপে d = d.plusMonths(1); তারপর d ছাপুন। চারটি তারিখ কী আসবে, আর কোনটি “ঠিক”?

    2026-02-28, 2026-03-28, 2026-04-28, 2026-05-28 (যাচাই করা)। ফেব্রুয়ারিতে একবার ২৮ হয়ে যাওয়ার পর প্রতিটি মাসে ২৮ থেকেই গোনা হচ্ছে। গ্রাহকের চুক্তিতে “প্রতি মাসের শেষ দিন” বা “৩১ তারিখ (না থাকলে শেষ দিন)” লেখা থাকলে মূল লুপের loan.plusMonths(i)-ই ঠিক। যদি নিয়ম সত্যিই “মাসের শেষ দিন” হয়, তাহলে আরও স্পষ্ট লেখা loan.plusMonths(i).with(TemporalAdjusters.lastDayOfMonth()).

  3. রিভিউ করে আবার লিখুন: একটি অর্ডার-সার্ভিসের নিচের কোডে অন্তত চারটি সমস্যা খুঁজুন:
    static SimpleDateFormat FMT = new SimpleDateFormat("dd/MM/YYYY HH:mm");
    
    void placeOrder(Order o) {
        o.setCreatedAt(LocalDateTime.now());
        LocalDateTime deadline = o.getCreatedAt().plusHours(72);
        sms.send(o.phone(), "শেষ সময়: " + FMT.format(java.sql.Timestamp.valueOf(deadline)));
    }

    (১) static SimpleDateFormat — thread-safe নয়, একসাথে অনেক অর্ডারে তারিখ মিশে যেতে পারে; (২) YYYY — বছরের শেষে ভুল বছর; (৩) অর্ডারের মুহূর্ত LocalDateTime-এ — কোন জোনের, জানা নেই; সার্ভার বিদেশে গেলে সময় ভুল; (৪) সরাসরি now() — টেস্টে নিয়ন্ত্রণ করা যায় না; (৫) পুরোনো আর নতুন API-র অপ্রয়োজনীয় মিশ্রণ। একটি ভালো রূপ:

    static final ZoneId DHAKA = ZoneId.of("Asia/Dhaka");
    static final DateTimeFormatter FMT = DateTimeFormatter.ofPattern("dd/MM/uuuu HH:mm");
    private final Clock clock;                       // constructor-এ আসে
    
    void placeOrder(Order o) {
        Instant created = Instant.now(clock);
        o.setCreatedAt(created);                     // ডেটাবেসে UTC
        Instant deadline = created.plus(Duration.ofHours(72));
        sms.send(o.phone(), "শেষ সময়: " + deadline.atZone(DHAKA).format(FMT));
    }

    “৭২ ঘণ্টা” এখানে সত্যিই ঘড়ির ব্যবধান, তাই Duration. নিয়ম যদি হতো “৩ দিন পর একই সময়”, তাহলে ঢাকার জোনে plusDays(3) — বাংলাদেশে DST নেই বলে ফল একই, কিন্তু DST আছে এমন দেশে আলাদা।

  4. নকশার সিদ্ধান্ত: ঢাকা থেকে লন্ডনের একটি ফ্লাইট-বুকিং সিস্টেমে প্রতিটি তথ্যের জন্য টাইপ বাছুন: (ক) যাত্রীর জন্মতারিখ; (খ) ফ্লাইট ছাড়ার সময়, যা টিকিটে ঢাকার স্থানীয় সময়ে লেখা থাকবে; (গ) ফ্লাইটের দৈর্ঘ্য; (ঘ) বুকিং কখন করা হলো, অডিটের জন্য; (ঙ) প্রতিদিন রাত ১১:৫৫-তে চেক-ইন বন্ধের নিয়ম।

    (ক) LocalDate — দিন যথেষ্ট, জোন অপ্রাসঙ্গিক। (খ) ZonedDateTime জোন Asia/Dhaka সহ — মানুষের কাছে অর্থবহ ঢাকার ঘড়ি; পৌঁছানোর সময় দেখাতে একই মুহূর্তকে Europe/London-এ রূপান্তর। ভবিষ্যতের ফ্লাইট বলে স্থানীয় সময় আর জোন রাখাই ভালো, যাতে জোনের নিয়ম বদলালেও টিকিটের সময় ঠিক থাকে। (গ) Duration — ঘড়ির ব্যবধান; দুই জোনের স্থানীয় সময় বিয়োগ করে নয়, দুটি মুহূর্তের মধ্যে Duration.between. (ঘ) Instant — অডিটে দ্ব্যর্থহীন মুহূর্ত দরকার। (ঙ) LocalTime — প্রতিদিনের নিয়ম, পরে প্রতিটি দিনের সাথে মিলিয়ে ZonedDateTime.

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমJava-তে একটি পেমেন্টের সময় সংরক্ষণ করে পরে ব্যবহারকারীকে তাঁর শহরের ঘড়িতে দেখানোর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ⏱️ Instant.now(clock) দিয়ে মুহূর্তটি নেওয়া
  2. 💾 ডেটাবেসে UTC timestamp হিসেবে সংরক্ষণ
  3. 📥 পরে ডেটাবেস থেকে Instant পড়া
  4. 🌐 atZone(ব্যবহারকারীর ZoneId) দিয়ে ZonedDateTime বানানো
  5. 🖨️ DateTimeFormatter আর Locale দিয়ে লেখায় রূপ দিয়ে দেখানো

Java-তে LocalDate.of(2026, 1, 31).plusMonths(1) কী ফেরত দেয়?

  • DateTimeException, কারণ ৩১ ফেব্রুয়ারি নেই
  • 2026-02-28
  • 2026-03-03
  • 2026-03-02

Java-র পুরোনো Calendar-এ cal.set(2026, 2, 21) লিখলে তারিখটি কোন মাসের হয়?

  • ফেব্রুয়ারি
  • জানুয়ারি
  • মার্চ
  • exception হয়

একটি আন্তর্জাতিক পেমেন্ট সিস্টেমে লেনদেন ঠিক কোন মুহূর্তে হলো, তা ডেটাবেসে রাখতে Java-র কোন টাইপ সবচেয়ে উপযুক্ত?

  • Instant
  • LocalDateTime
  • LocalDate
  • LocalTime

Java-তে Duration.between(LocalDate.of(2026, 1, 1), LocalDate.of(2026, 2, 1)) চালালে কী হয়?

  • PT744H
  • P1M
  • compile error
  • UnsupportedTemporalTypeException: Unsupported unit: Seconds

Java-তে LocalDate.of(2026, 12, 30)-কে DateTimeFormatter.ofPattern("YYYY-MM-dd", Locale.US) দিয়ে format করলে কী ছাপা হয়?

  • 2026-12-30
  • 2027-12-30
  • IllegalArgumentException
  • 2025-12-30

America/New_York জোনে ৮ মার্চ ২০২৬ মধ্যরাত থেকে পরের দিনের মধ্যরাত পর্যন্ত Duration.between কী দেয়, যেদিন সেখানে DST শুরু হয়?

  • PT24H
  • PT25H
  • PT23H
  • DateTimeException

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. আগে অর্থ, পরে টাইপ: তারিখ → LocalDate, দেয়ালঘড়ি → LocalTime, বিশ্বব্যাপী মুহূর্ত → Instant, শহরের ঘড়িতে ঘটনা → ZonedDateTime, ব্যবধান → Duration (ঘড়ি) বা Period (ক্যালেন্ডার)।
  2. নতুন কোডে java.time: সব টাইপ immutable আর thread-safe; plus...-এর ফল ভ্যারিয়েবলে রাখুন। পুরোনো Date/Calendar শুধু সীমানায়।
  3. মাসের হিসাব: ৩১ জানুয়ারি + ১ মাস = ফেব্রুয়ারির শেষ দিন; কিস্তি সবসময় মূল তারিখ থেকে plusMonths(i). মোট দিন ChronoUnit.DAYS.between.
  4. জোন: Asia/Dhaka এখন UTC+6, DST নেই; বিদেশি জোনে DST আছে, দিন ২৩-২৫ ঘণ্টারও হয়। সংরক্ষণে Instant, দেখানোয় জোন; অফসেট হার্ডকোড নয়।
  5. টেস্ট আর format: now()-এর বদলে Clock; প্যাটার্নে uuuu/yyyy, কখনো YYYY নয়; Locale স্পষ্ট করে দিন।

পরের পর্বে: object-কে লেখায় আর লেখাকে object-এ — JSON আর serialization. একটি অর্ডার object, যার ভেতরে এই পর্বের Instant-ও আছে, কীভাবে ফাইলে বা নেটওয়ার্কে পাঠানোর মতো রূপ পায়, আর Java-র পুরোনো built-in serialization কেন সাবধানে ব্যবহার করতে হয়।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান