এই পর্বে যা শিখবেন
- চাহিদা থেকে নকশা — ডেটা মডেল, ফাংশনের তালিকা আর ডেটার প্রবাহ আগে ঠিক করা
- যাচাই,
addExpenseআর undo — নতুন state বানিয়ে পুরোনোটি অক্ষত রাখা reduceআরMapদিয়ে খাতওয়ারি ও মাসওয়ারি মোট,Intlদিয়ে বাংলা টাকা ও মাসের নাম- সামান্য DOM আর event দিয়ে পাতলা UI — একটি submit-এর পুরো যাত্রা
- edge case-এর টেবিল দিয়ে হাতে টেস্ট, নিজের কম্পিউটারে চালানো আর AI-কে দিয়ে কোড রিভিউ
১ · কেন একটি প্রজেক্ট — “হিসাব খাতা”-র চাহিদা
বিশটি পর্বে আপনি অনেক টুকরো শিখেছেন — মান আর টাইপ, ভ্যারিয়েবল, শর্ত, লুপ, function, object, array, map/filter/reduce, destructuring, Map/Set, JSON আর তারিখ। প্রতিটি আলাদাভাবে বোঝা এক জিনিস, আর সবগুলো জোড়া দিয়ে একটি কাজের জিনিস বানানো আরেক জিনিস। রান্নার বইয়ে প্রতিটি মশলার গুণ পড়া আর পুরো এক বেলার রান্না করা যেমন আলাদা। এই পর্ব সেই পুরো রান্না — Module ৩-এর শেষ পর্ব, আর কোর্সের মাঝপথের প্রজেক্ট।
গল্পটি এরকম। রাফি ঢাকায় একটি অফিসে চাকরি করেন। মাসের শেষে হিসাব মেলে না — রিকশা, চা-শিঙাড়া, মোবাইল রিচার্জ, ইন্টারনেট বিল; টাকা কোথায় গেল কেউ জানে না। তিনি চান একটি ছোট পাতা, যেখানে প্রতিটি খরচ লিখে রাখা যায় আর মাস শেষে দেখা যায় কোন খাতে কত গেল। আমরা ঠিক সেটাই বানাব: হিসাব খাতা, যা চলে ব্রাউজারে, একটি HTML পাতা আর কিছু JavaScript দিয়ে।
চাহিদা — অ্যাপটি কী কী পারবে
| # | চাহিদা | উদাহরণ | কোন পর্বের টুল |
|---|---|---|---|
| ১ | খরচ যোগ — টাকা, খাত, নোট, তারিখ | ৮৫.৫০ টাকা · খাবার · চা-শিঙাড়া · ৫ অক্টোবর | object (পর্ব ১৪), spread (পর্ব ১৭) |
| ২ | যাচাই — টাকা শূন্যের বেশি, খাত জানা, তারিখ বৈধ | −৪০ টাকা → “টাকার পরিমাণ শূন্যের বেশি হতে হবে” | শর্ত (পর্ব ১১), number (পর্ব ১০) |
| ৩ | তারিখ অনুযায়ী সাজানো তালিকা | ২৮ সেপ্টেম্বর আগে, ৫ অক্টোবর পরে | toSorted (পর্ব ১৬) |
| ৪ | মোট আর খাতওয়ারি মোট | খাবার ৩৩৫.৫০৳, বিল ১,২০০.৫০৳ | reduce (পর্ব ১৬), Map (পর্ব ১৮) |
| ৫ | খাত দিয়ে ফিল্টার | শুধু “যাতায়াত” দেখাও | filter (পর্ব ১৬) |
| ৬ | মাসওয়ারি সারাংশ, বাংলা মাসের নামে | অক্টোবর ২০২৬: ১,৪৬৬.০০৳ | Intl (পর্ব ২০) |
| ৭ | শেষ কাজটি ফেরানো (undo) | ভুল খরচ যোগ হলে এক চাপে বাতিল | array (পর্ব ১৫) |
| ৮ | JSON হিসেবে export | সুন্দর indentation-সহ লেখা | JSON.stringify (পর্ব ১৯) |
কী বানাব না — পরিধির সীমা
একটি ভালো প্রজেক্টের অর্ধেক কাজ হলো “না” বলা। প্রথম সংস্করণে যা যা থাকবে না: লগইন, সার্ভার, একাধিক ব্যবহারকারী, আর স্থায়ীভাবে সেভ করা। পাতা reload করলে খাতা খালি হয়ে যাবে — ডেটা থাকবে শুধু memory-তে। এর দুটি কারণ। এক, এই পাতার কোড চালানোর জায়গাটি একটি নিরাপদ sandbox, যেখানে localStorage ব্যবহার করা যায় না। দুই, স্থায়ী সংরক্ষণ নিজেই একটি বড় বিষয় — পর্ব ৪০-এ localStorage আর IndexedDB দিয়ে এই খাতাকেই আমরা মনে রাখতে শেখাব। এই ছোট, কিন্তু সম্পূর্ণ কাজ করে এমন প্রথম সংস্করণকেই MVP (minimum viable product) বলা হয়।
এই প্রজেক্টের আসল শিক্ষা কোনো নতুন syntax নয় — নকশা। হিসাবের সব নিয়ম থাকবে এমন ফাংশনে, যারা DOM চেনে না, শুধু ডেটা নেয় আর নতুন ডেটা ফেরত দেয়। পাতার সাথে কথা বলবে আলাদা একটি পাতলা স্তর। এই ভাগটুকু ঠিক থাকলে টেস্ট, undo, পরে storage বা framework যোগ — সবই সহজ হয়ে যায়।
২ · নকশা — ডেটা মডেল, ফাংশনের তালিকা আর ডেটার প্রবাহ
কোড লেখার আগে তিনটি প্রশ্নের উত্তর কাগজে লিখুন: ডেটা দেখতে কেমন? কোন কোন কাজ লাগবে? ডেটা কোন পথে চলবে? এই তিনটি পরিষ্কার থাকলে কোড প্রায় নিজে থেকেই লেখা হয়ে যায়।
ডেটা মডেল — একটি খরচ আর পুরো state
একটি খরচ হলো একটি সাধারণ object. পুরো অ্যাপের অবস্থা — অর্থাৎ state — আরেকটি object, যার ভেতরে খরচের একটি array আর পরের id-র নম্বর:
// একটি খরচ
{ id: 3, date: '2026-10-01', category: 'bills', amount: 120050, note: 'ইন্টারনেট' }
// পুরো অ্যাপের state — একটিই জায়গায়
{ items: [ /* খরচের object, যোগ করার ক্রমে */ ], nextId: 6 }
// খাতের তালিকা — key কোডে, লেখা বাংলায়
const CATS = { food: 'খাবার', transport: 'যাতায়াত', bills: 'বিল', other: 'অন্যান্য' };প্রতিটি সিদ্ধান্তের পেছনে একটি কারণ আছে:
amountপূর্ণসংখ্যা পয়সায়: ১২০০.৫০ টাকা মানে120050. পর্ব ১০-এ দেখেছেন দশমিক টাকা যোগ করলে245.85000000000002-এর মতো ভুল জমে; পয়সার যোগ হুবহু নির্ভুল। টাকায় রূপান্তর হবে শুধু দেখানোর সময়।dateএকটি'YYYY-MM-DD'string,Dateobject নয়: এমন string JSON-এ হুবহু যায়-আসে, অক্ষর ধরে সাজালেই তারিখের ক্রমে সাজানো হয়, আর time zone-এর কারণে এক দিন সরে যাওয়ার ফাঁদ (পর্ব ২০) থাকে না। খরচের জন্য আমাদের দিন দরকার, মিলিসেকেন্ড নয়।categoryএকটি ছোট ইংরেজি key: কোডে তুলনা সহজ; বাংলা নাম আসেCATSথেকে। পরে খাতের নাম বদলালে পুরোনো ডেটা নষ্ট হয় না।- প্রতিটি খরচের
id: একই দিনে একই টাকার দুটি চা হতে পারে; id ছাড়া আলাদা করা যায় না। পরে মুছে ফেলা বা edit করার জন্যও id লাগবে।
ফাংশনের তালিকা — দুই দলে ভাগ
| ফাংশন | নেয় → দেয় | দল |
|---|---|---|
toPaisa(text) | '85.50' → 8550 (ভুল হলে NaN) | pure core |
validate(e) | খরচ → null অথবা ভুলের বার্তা | pure core |
addExpense(state, e) | পুরোনো state + খরচ → নতুন state | pure core |
total(items) | খরচের array → মোট পয়সা | pure core |
byCategory(items), byMonth(items) | খরচের array → Map (খাত/মাস → পয়সা) | pure core |
sortedByDate(items) | array → তারিখে সাজানো নতুন array | pure core |
money(p), monthName(ym) | 171600 → '১,৭১৬.০০৳'; '2026-10' → 'অক্টোবর ২০২৬' | pure core (ফরম্যাট) |
render(), commit(next), tryAdd(e), event listener | DOM পড়ে/লেখে, state বদলায় | thin UI shell |
খেয়াল করুন, হিসাবের ফাংশনগুলো পুরো state নয়, items নেয়। কারণ: ফিল্টার করা তালিকার মোটও একই total দিয়ে বের করা যায়। ছোট, এক-কাজের ফাংশন যত কম জিনিস চায়, তত বেশি জায়গায় কাজে লাগে।
এখন সবচেয়ে গুরুত্বপূর্ণ ছবি — দুই স্তর কীভাবে কথা বলে। ধাপে ধাপে দেখুন:
- পাতা (DOM)
form, ul, pব্যবহারকারী যা দেখেন - Thin UI shell
tryAdd, renderDOM ছোঁয়ার একমাত্র জায়গা - State
{ items, nextId }সত্যের একমাত্র উৎস - Pure core
addExpense, totalডেটা ঢোকে, ডেটা বেরোয়
addExpense(state, e) → নতুন state total(items) → 171600 byCategory(items) → Map { 'food' => 33550, … } // একই ইনপুটে সবসময় একই আউটপুট — console-এ টেস্ট করা যায়
let state = { items: [ … ], nextId: 6 };
const past = [ S0, S1, S2, S3, S4 ]; // undo-র জন্য
পাতায় যা দেখা যায়, সব এখান থেকেই আসে
submit → tryAdd({ … ঘর থেকে পড়া মান … })
→ commit(addExpense(state, e))
→ render() // state থেকে পুরো তালিকা নতুন করে
এখানে হিসাবের কোনো নিয়ম নেই
DOM বদলালেন (table বানালেন) → শুধু render বদলায়
storage যোগ (পর্ব ৪০) → শুধু shell বদলায়
React-এ নিলেন (পর্ব ৫৪) → core হুবহু থাকে
VAT/বাজেটের নিয়ম বদলালেন → শুধু core বদলায়, টেস্ট চালান
- ভেতরের স্তর: pure core. এখানে আছে হিসাবের সব নিয়ম — যাচাই, যোগ, মোট, সাজানো। এই ফাংশনগুলো DOM চেনে না, বাইরের কোনো ভ্যারিয়েবল বদলায় না, আর একই ইনপুটে সবসময় একই ফল দেয়। তাই পাতা ছাড়াই, শুধু console-এ এদের টেস্ট করা যায়।
- মাঝখানে: state. পুরো অ্যাপের অবস্থা একটিমাত্র object-এ থাকে। তালিকা, মোট, JSON — পাতার প্রতিটি জিনিস এই একটি মান থেকে হিসাব করে বের হয়। “সত্যের একমাত্র উৎস” থাকলে দুই জায়গার তথ্য অমিল হওয়ার সুযোগ থাকে না।
- বাইরের স্তর: thin UI shell. ব্যবহারকারী বোতাম চাপলে shell ঘরগুলো থেকে মান পড়ে, core-কে দিয়ে নতুন state বানায়, তারপর
renderদিয়ে পাতা নতুন করে আঁকে। Shell যত পাতলা, তত কম জায়গায় বাগ লুকাতে পারে — কারণ এই অংশটিই টেস্ট করা সবচেয়ে কঠিন। - লাভটা দেখা যায় পরিবর্তনের সময়। পরে storage যোগ হবে, হয়তো React-এ নেওয়া হবে, হয়তো হিসাবের নিয়ম বদলাবে। দুই স্তর আলাদা বলে প্রতিটি পরিবর্তন একটি জায়গায় সীমিত থাকে। এই নকশার নাম functional core, imperative shell — ইতিহাস আছে ভাবনার প্রশ্ন ১-এ।
আমাদের Java আর Python কোর্সেও মাঝপথে একই “হিসাব খাতা” আছে, তবে terminal-এ চলা CLI টুল হিসেবে। Java সংস্করণে আছে Expense আর ExpenseBook class, যেখানে খাতা নিজের array বদলায় (mutable); Python সংস্করণে dict-এর list আর একটি কমান্ড-লুপ। JavaScript সংস্করণ চলে ব্রাউজারে, ফর্ম আর event দিয়ে, আর state কখনো বদলায় না — প্রতিবার নতুন state. একই সমস্যার তিনটি নকশা পাশাপাশি দেখতে চাইলে পড়ুন Java-র মাঝপথের প্রজেক্ট আর Python-এর মাঝপথের প্রজেক্ট.
৩ · ধাপ ১–২ — যাচাই আর addExpense: নতুন state, পুরোনোটি অক্ষত
বড় প্রোগ্রাম একবারে লেখা হয় না — ছোট ছোট ধাপে, প্রতিটি ধাপ শেষে চালিয়ে দেখে। আমাদের পাঁচটি ধাপ: (১) ইনপুট পড়া ও যাচাই, (২) addExpense, (৩) হিসাব — মোট, খাতওয়ারি, মাসওয়ারি, (৪) পাতায় দেখানো, (৫) undo আর export. প্রথম তিন ধাপে DOM লাগবেই না।
ধাপ ১ — লেখা থেকে পয়সা, আর যাচাই
ফর্মের প্রতিটি ঘরের মান আসে string হিসেবে — "85.50", এমনকি খালি "". পর্ব ১০-এর ফাঁদটি মনে আছে? Number("") দেয় 0, আর 19.99 * 100 দেয় 1998.9999999999998. তাই রূপান্তরের ফাংশনটি ছোট হলেও সতর্ক:
const toPaisa = (text) => (text.trim() === '' ? NaN : Math.round(Number(text) * 100));
function validate(e) {
if (!Number.isSafeInteger(e.amount) || e.amount <= 0) return 'টাকার পরিমাণ শূন্যের বেশি হতে হবে';
if (!Object.hasOwn(CATS, e.category)) return 'অজানা খাত: ' + e.category;
const d = new Date(e.date + 'T00:00:00Z');
if (Number.isNaN(d.getTime()) || d.toISOString().slice(0, 10) !== e.date) return 'তারিখ ঠিক নেই';
return null; // null মানে সব ঠিক
}- ফাঁকা লেখা আলাদা করে ধরা: নইলে খালি ঘর হয়ে যেত ০ টাকা।
NaNফেরত দিলে পরের যাচাই নিজেই সেটি বাতিল করে, কারণNumber.isSafeInteger(NaN)হলোfalse. Math.round:19.99 * 100-এর লুকানো ভুল গোল করে1999বানায়।Object.hasOwn,inনয়:'toString' in CATSদেয়true, কারণinprototype-এর জিনিসও দেখে (পর্ব ১৪, গভীরে পর্ব ২৫)। ES2022-এ যুক্তObject.hasOwnশুধু object-এর নিজের key দেখে।- তারিখের round-trip:
Date.parse('2026-02-30')V8-এ কোনো ভুল দেখায় না — মান গড়িয়ে ২ মার্চ হয়ে যায়। তাই তারিখ বানিয়ে আবার string-এ ফিরিয়ে মিলিয়ে দেখি; না মিললে তারিখটি আসলে নেই। - ভুল হলে throw নয়, বার্তা ফেরত: ব্যবহারকারীর ভুল ইনপুট স্বাভাবিক ঘটনা, প্রোগ্রামের দুর্ঘটনা নয়। এখানে বার্তা-ফেরতের সরল পথ বেছেছি;
throwআর custom error আসবে পর্ব ২৯-এ। প্রতিটি শর্ত একটি guard clause — ভুল পেলেই সাথে সাথে ফেরত।
ধাপ ২ — addExpense: বদলানো নয়, নতুন বানানো
সবচেয়ে সহজ পথ হতো state.items.push(e) — পুরোনো array-টিই বদলে দেওয়া, অর্থাৎ mutation. আমরা ইচ্ছা করেই তা করব না। addExpense পুরোনো state ছোঁবে না; spread দিয়ে নতুন array আর নতুন object বানাবে — এই কৌশলের নাম immutable update (পর্ব ১৭):
const addExpense = (s, e) => ({
items: [...s.items, { ...e, id: s.nextId }], // নতুন array, শেষে নতুন খরচ
nextId: s.nextId + 1,
});কেন এত ঝামেলা? কারণ পুরোনো state অক্ষত থাকলে সেটি একটি নির্ভরযোগ্য ছবি — undo করতে সেই ছবিটি ফিরিয়ে আনলেই হলো। আর ফাংশনটি side effect-মুক্ত, তাই একে একা টেস্ট করা যায়। নিচের কোডে core-এর চারটি ফাংশন চালিয়ে দেখুন — কোনো পাতা, কোনো ফর্ম ছাড়াই:
const emptyState = { items: [], nextId: 1 };
const addExpense = (s, e) => ({ items: [...s.items, { ...e, id: s.nextId }], nextId: s.nextId + 1 });
const total = (items) => items.reduce((sum, e) => sum + e.amount, 0);
const byCategory = (items) =>
items.reduce((m, e) => m.set(e.category, (m.get(e.category) ?? 0) + e.amount), new Map());
const s1 = addExpense(emptyState, { date: '2026-10-01', category: 'food', amount: 25000, note: 'ভাত' });
const s2 = addExpense(s1, { date: '2026-10-02', category: 'transport', amount: 6000, note: 'রিকশা' });
const s3 = addExpense(s2, { date: '2026-10-05', category: 'food', amount: 8550, note: 'চা' });
console.log(emptyState.items.length, s1.items.length, s3.items.length);
console.log(s1.items[0] === s3.items[0]); // পুরোনো খরচ কপি হয়নি, ভাগাভাগি
console.log(total(s3.items), total(s1.items));
console.log(byCategory(s3.items));
const past = [emptyState, s1, s2]; // undo stack
let state = s3;
state = past.pop(); // undo
console.log(state.items.length, total(state.items), past.length);প্রথম লাইন 0 1 3: তিনবার যোগের পরও emptyState আর s1 একদম আগের মতো আছে — কেউ তাদের বদলায়নি। দ্বিতীয় লাইন true দেখায় একটি সূক্ষ্ম কথা: spread নতুন array বানায়, কিন্তু ভেতরের পুরোনো খরচ-object কপি করে না — একই reference রাখে। একে বলে structural sharing, আর এ কারণেই প্রতিবার নতুন state বানানো সস্তা। তারপর 39550 25000 — নতুন আর পুরোনো state-এর মোট। Map(2) { 'food' => 33550, 'transport' => 6000 } হলো খাতওয়ারি মোট। শেষ লাইন 2 31000 2: undo মানে শুধু stack থেকে আগের state তুলে আনা — কোনো হিসাব উল্টাতে হয়নি।
৪ · ধাপ ৩ — হিসাব: reduce আর Map দিয়ে খাতওয়ারি ও মাসওয়ারি মোট
মোট বের করা সহজ: items.reduce((sum, e) => sum + e.amount, 0). খাতওয়ারি মোট একটু বেশি মজার। আমাদের দরকার একটি “খাত → পয়সা” তালিকা — ঠিক যে কাজের জন্য Map বানানো (পর্ব ১৮)। reduce একটি খালি Map দিয়ে শুরু করে প্রতিটি খরচে সেটি হালনাগাদ করে; Map-টিই এখানে accumulator. নিচে তিনটি খরচ দিয়ে ধাপে ধাপে দেখুন:
1items.reduce((m, e) =>2 m.set(e.category,3 (m.get(e.category) ?? 0) + e.amount),4 new Map());5// ফল: Map(2) { 'food' => 33550, 'transport' => 6000 }
| e | |
| m.get(e.category) | |
| m |
- শুরু: খালি Map.
reduce-এর দ্বিতীয় argumentnew Map()হলো accumulator-এর শুরুর মান। প্রতিটি খরচ দেখার পর callback যা ফেরত দেবে, সেটিই হবে পরের ধাপেরm. - প্রথম খরচ: food, ২৫০ টাকা। Map-এ এখনো
'food'নেই, তাইm.getদেয়undefined, আর?? 0সেটিকে ০ বানায় (পর্ব ০৮)। ০ + 25000 = 25000 রাখা হলো।m.setMap-টিই ফেরত দেয়, তাই আলাদাreturn mলাগে না। - দ্বিতীয় খরচ: transport. নতুন key, তাই আবার ০ থেকে শুরু:
'transport' → 6000. Map মনে রাখে কোন key আগে ঢুকেছে — তালিকা দেখানোর সময় এই ক্রমই আসবে। - তৃতীয় খরচ: আবার food. এবার
m.get('food')দেয় 25000; তার সাথে 8550 যোগ করে 33550. key-টি আগেই আছে, তাই তার জায়গা বদলায় না — শুধু মান হালনাগাদ হয়। - ফল: একটি Map. সব খরচ দেখা শেষে
reduceশেষ accumulator ফেরত দেয়। একটি লুপ, একবার পড়া, আর হাতে খাতওয়ারি মোট — খাত যতগুলোই হোক।
এক খাতের মান কয়েকবার m.set হলো — তাহলে এটি কি mutation নয়? হ্যাঁ, তবে Map-টি এই ফাংশনের ভেতরে জন্মেছে আর ভেতরেই বড় হয়েছে; বাইরের কেউ তাকে আগে দেখেনি। এমন “স্থানীয় mutation” ফাংশনকে impure বানায় না — বাইরে থেকে দেখলে একই ইনপুটে একই Map আসে। Pure মানে “কোথাও কিছু বদলায় না” নয়; মানে “বাইরের পৃথিবী কিছু টের পায় না”।
একই ছাঁচে মাসওয়ারি — আর একটি ছোট সাধারণীকরণ
মাসওয়ারি মোটে key হবে খাত নয়, মাস: '2026-10-05'.slice(0, 7) = '2026-10'. দুটি প্রায় একই ফাংশন লেখার বদলে key বের করার নিয়মটিকে argument বানাই — ফাংশন যখন আরেকটি ফাংশন নেয়, তাকে বলে higher-order function (পর্ব ১৩):
const sumBy = (items, keyOf) =>
items.reduce((m, e) => m.set(keyOf(e), (m.get(keyOf(e)) ?? 0) + e.amount), new Map());
const byCategory = (items) => sumBy(items, (e) => e.category);
const byMonth = (items) => sumBy(items, (e) => e.date.slice(0, 7));
const sortedByDate = (items) =>
items.toSorted((a, b) => a.date.localeCompare(b.date) || a.id - b.id);sortedByDate-এ দুটি সিদ্ধান্ত লক্ষ করুন। এক: sort নয়, toSorted (ES2023) — sort মূল array-টিই বদলে দেয়, যা আমাদের state-এর নিয়ম ভাঙত। দুই: একই তারিখে দুটি খরচ হলে localeCompare দেয় ০, তখন || a.id - b.id যোগ করার ক্রম ঠিক রাখে। আর 'YYYY-MM-DD' string অক্ষর ধরে তুলনা করলেই তারিখের ক্রম আসে, কারণ বড় একক (বছর) আগে আর প্রতিটি অংশ সমান দৈর্ঘ্যের। Map key ঢোকার ক্রম মনে রাখে বলে সাজানো array থেকে বানানো byMonth-এর মাসগুলোও আসে ক্রমানুসারে।
বাংলায় টাকা আর মাসের নাম — Intl
হিসাব শেষ, এবার শুধু দেখানো। পর্ব ১০ আর ২০-এর দুটি formatter একবার বানিয়ে বারবার ব্যবহার করি (বানানো তুলনামূলক ব্যয়বহুল, ব্যবহার সস্তা):
const tk = new Intl.NumberFormat('bn-BD', { style: 'currency', currency: 'BDT' });
const mon = new Intl.DateTimeFormat('bn-BD', { month: 'long', year: 'numeric', timeZone: 'Asia/Dhaka' });
const money = (paisa) => tk.format(paisa / 100); // 171600 → '১,৭১৬.০০৳'
const monthName = (ym) => mon.format(new Date(ym + '-15T12:00:00+06:00')); // '2026-10' → 'অক্টোবর ২০২৬'Intl.NumberFormat লাখ-কোটির নিয়মে কমা বসায়, বাংলা অঙ্ক দেয় আর ৳ চিহ্ন লাগায়। Intl.DateTimeFormat মাসের বাংলা নাম দেয়। monthName-এ মাসের ১৫ তারিখের দুপুর বেছে নেওয়ার কারণ: মাসের ১ তারিখ মধ্যরাত বেছে নিলে অন্য time zone-এ সেটি আগের মাসের শেষ দিন হয়ে যেতে পারত। মাসের মাঝখানে কোনো time zone-ই মাস বদলাতে পারে না, আর timeZone: 'Asia/Dhaka' ফলটিকে যেকোনো কম্পিউটারে একই রাখে।
৫ · ধাপ ৪ — পাতায় দেখানো: পাতলা UI আর একটি submit-এর যাত্রা
এবার পাতা। HTML অংশটি ছোট — একটি ফর্ম (টাকা, খাত, নোট, তারিখ), একটি বার্তার জায়গা, একটি ফিল্টার আর undo বোতাম, একটি তালিকা, আর JSON দেখানোর একটি ভাঁজ-করা অংশ:
<form id="f">
<input id="amount" inputmode="decimal" placeholder="টাকা">
<select id="cat"><option value="food">খাবার</option> … </select>
<input id="note" placeholder="নোট">
<input id="date" type="date" value="2026-10-06">
<button>যোগ করুন</button>
</form>
<p id="msg"></p>
<div id="tools">খাত: <select id="filter">…</select> <button type="button" id="undo">↶ undo</button> <b id="sum"></b></div>
<ul id="list"></ul>
<details><summary>JSON export</summary><pre id="json"></pre></details>JavaScript দিয়ে পাতার সাথে কথা বলতে আমরা মাত্র কয়েকটি টুল নেব: document.querySelector দিয়ে element খোঁজা, textContent দিয়ে লেখা বসানো, document.createElement আর append দিয়ে নতুন <li> বানানো, আর addEventListener দিয়ে বোতাম চাপা বা ফর্ম জমা শোনা।
এখানে DOM আর event-এর যা লিখছি, তা শুধু প্রজেক্ট চালানোর মতো ন্যূনতম। DOM tree, খোঁজা আর বদলানো আসবে পর্ব ৩৭-এ; event bubbling আর delegation পর্ব ৩৮-এ; ফর্ম আর validation-এর পুরো গল্প পর্ব ৩৯-এ। এখন শুধু ধরে নিন: querySelector('#list') মানে “id যার list, সেই element দাও”।
render — পুরো state থেকে পুরো পাতা
function render() {
const cat = $('#filter').value;
const shown = sortedByDate(state.items).filter((e) => cat === 'all' || e.category === cat);
$('#list').textContent = ''; // পুরোনো তালিকা মুছে
for (const e of shown) {
const li = document.createElement('li');
li.textContent = `${e.date} · ${CATS[e.category]} · ${money(e.amount)} · ${e.note}`;
$('#list').append(li);
}
$('#sum').textContent = `মোট ${money(total(shown))} (${shown.length}টি)`;
$('#json').textContent = JSON.stringify(state.items, null, 2);
}render-এর নিয়মটি সরল: পাতা = f(state). কোন <li> নতুন যোগ হলো, কোনটা সরল — এসব আলাদা করে হিসাব না করে প্রতিবার পুরো তালিকা নতুন করে আঁকি। দশ-বিশটি খরচে এটি চোখের পলকে হয়; হাজার হাজার সারিতে কী করতে হয়, সে প্রশ্ন ভাবনার প্রশ্ন ২-এ। আর একটি নিরাপত্তার কথা: নোট লেখেন ব্যবহারকারী, তাই সেটি innerHTML-এ নয়, textContent-এ বসাই। কেউ নোটে <img src=x onerror=…> লিখলে textContent সেটিকে নিছক লেখা হিসেবে দেখায়, কোড হিসেবে চালায় না — XSS নিয়ে বিস্তারিত পর্ব ৪৩-এ।
একটি submit-এর যাত্রা
ব্যবহারকারী “৮৫.৫০, খাবার, চা-শিঙাড়া” লিখে “যোগ করুন” চাপলেন। এরপর কী কী ঘটে, ধাপে ধাপে দেখুন:
- submit
preventDefaultঘর থেকে মান পড়া - যাচাই
validatenull বা বার্তা - pure core
addExpenseনতুন state - commit
past.pushপুরোনোটি stack-এ - render
textContentপাতা = f(state)
$('#f').addEventListener('submit', (ev) => {
ev.preventDefault(); // নইলে পাতা reload, সব হারাবে
tryAdd({ date: '2026-10-06', category: 'food',
amount: toPaisa('85.50'), note: 'চা-শিঙাড়া' });
});
toPaisa('85.50') → 8550
validate(e) → null // সব ঠিক
ভুল হলে: $('#msg').textContent = 'টাকার পরিমাণ …'; state বদলায় না
addExpense(state, e)
→ { items: [ …৪টি পুরোনো, { …e, id: 5 } ], nextId: 6 }
function commit(next) {
past.push(state); // পুরোনো ছবি undo-র জন্য
state = next;
render();
}
• 2026-09-28 · খাবার · ২৫০.০০৳ · দুপুরের খাবার
• …
• 2026-10-06 · খাবার · ৮৫.৫০৳ · চা-শিঙাড়া
মোট … (৫টি)
- submit শোনা। ফর্মে Enter চাপা বা বোতাম চাপা — দুটোই
submitevent তৈরি করে। ব্রাউজারের ডিফল্ট আচরণ হলো ফর্মের তথ্য নিয়ে নতুন পাতায় যাওয়া, যাতে আমাদের memory-র খাতা মুছে যেত।preventDefault()সেটি থামায়। তারপর shell প্রতিটি ঘরের মান পড়ে একটি খরচ-object বানায়। - যাচাই।
tryAddপ্রথমেvalidate-কে ডাকে। বার্তা এলে সেটি#msg-এ দেখায় আর থেমে যায় — state অক্ষত, তালিকা অক্ষত। ভুল ইনপুট কখনো খাতায় ঢোকার সুযোগ পায় না। - Pure core নতুন state বানায়।
addExpenseপুরোনো চারটি খরচের reference আর নতুন একটি খরচ নিয়ে নতুন array বানায়। এখানে DOM নেই, event নেই — এই ধাপটিই আমরা self-test দিয়ে প্রমাণ করি। - commit — একটিমাত্র দরজা। state বদলানোর সব পথ
commitদিয়ে যায়: পুরোনো state undo stack-এ রাখে, নতুনটি বসায়, তারপর আঁকে। এক দরজা মানে নিয়ম একবার লিখলেই হয় — পরে storage-এ সেভ করার লাইনটিও এখানেই বসবে (পর্ব ৪০)। - render. নতুন state থেকে পুরো তালিকা, মোট আর JSON নতুন করে আঁকা হয়। পাতায় যা দেখছেন তা সবসময় state-এর হুবহু প্রতিচ্ছবি — কারণ পাতা নিজে কিছু মনে রাখে না।
৬ · ধাপ ৫ — undo আর JSON export
সাধারণভাবে undo বানানো কঠিন: প্রতিটি কাজের উল্টো কাজ লিখতে হয় — যোগের উল্টো মোছা, edit-এর উল্টো আগের মান ফেরানো। আমাদের নকশায় undo প্রায় বিনামূল্যে, কারণ প্রতিটি পুরোনো state অক্ষত আছে। দরকার শুধু সেগুলো একটি array-তে রাখা — শেষে ঢোকে, শেষ থেকে বেরোয়। একে বলে undo stack:
let state = emptyState;
const past = []; // undo stack
function commit(next) { past.push(state); state = next; render(); }
$('#undo').addEventListener('click', () => {
if (past.length) { state = past.pop(); render(); }
});1let state = S0; const past = [];2commit(S1); // রিকশা ৬০3commit(S2); // বিল ১২০০.৫০4commit(S3); // চা ৮৫.৫০5state = past.pop(); // undo6state = past.pop(); // আবার undo
| state | |
| past | |
| মোট |
- শুরু। state হলো খালি খাতা S0, আর undo stack খালি। এই অবস্থায় undo চাপলে কিছুই হয় না —
if (past.length)সেটি নিশ্চিত করে, নইলেpopদিতundefined, আর অ্যাপ ভেঙে যেত। - প্রথম commit. নতুন state S1 বসার আগে পুরোনো S0 stack-এ ঢুকল। খেয়াল করুন, S0 বদলায়নি — সে এখনো খালি খাতার নিখুঁত ছবি।
- দ্বিতীয় commit. S1 stack-এ, S2 এখন state. প্রতিটি ছবি সম্পূর্ণ, কিন্তু structural sharing-এর কারণে রিকশার খরচ-object মেমরিতে একবারই আছে — S1 আর S2 দুজনেই তাকে দেখায়।
- তৃতীয় commit. stack-এ এখন তিনটি পুরোনো ছবি। চাইলে যেকোনো আগের মুহূর্তে ফিরে যাওয়া যায় — এটিই “time travel” ধারণার বীজ।
- undo.
past.pop()শেষে ঢোকা S2 তুলে আনে, আর সেটিই নতুন state. কোনো খরচ খুঁজে মুছতে হয়নি, কোনো মোট বিয়োগ করতে হয়নি — আগের পুরো ছবিটি ফিরে এসেছে। - আবার undo. এবার S1. শেষে ঢোকা, আগে বেরোনো — এই নিয়মকে LIFO (last in, first out) নিয়ম বলা হয়। বাস্তব অ্যাপে stack-এর মাপ সীমিত রাখা হয়, যেমন শেষ ৫০টি কাজ; পুরোনোগুলো
shiftদিয়ে বাদ পড়ে।
JSON export — খাতার একটি পাঠযোগ্য ছবি
Export-এর জন্য একটি লাইনই যথেষ্ট: JSON.stringify(state.items, null, 2). তৃতীয় argument 2 মানে প্রতিটি স্তরে দুই ঘর indentation — মানুষ পড়তে পারে, আবার অন্য প্রোগ্রাম JSON.parse দিয়ে হুবহু ফিরিয়েও নিতে পারে (পর্ব ১৯)। আমাদের ডেটা মডেল এখানে সুফল দেয়: string তারিখ আর পূর্ণসংখ্যা পয়সা JSON-এ কোনো রূপান্তর ছাড়াই যায়। Date object থাকলে সেটি ISO string হয়ে যেত, আর ফেরত পড়ার সময় আবার Date বানাতে হতো; Map থাকলে আসত শুধু {}.
৭ · পুরো অ্যাপ — একটি ব্লকে হিসাব খাতা
সব টুকরো এবার এক জায়গায়। নিচের কোড চালালে ওপরে আসল একটি ফর্মসহ খাতা দেখা যাবে। চালানোর সাথে সাথে কোডটি তিনটি কাজ করে: প্রথমে core-এর self-test, তারপর নির্দিষ্ট তারিখের পাঁচটি নমুনা খরচ যোগ (আর একটি ইচ্ছাকৃত ভুল খরচ), শেষে একবার undo — যাতে প্রতিবার একই আউটপুট আসে। তারপর ফর্মে নিজে খরচ লিখে দেখুন, খাত দিয়ে ফিল্টার করুন, undo চাপুন, “JSON export” খুলুন।
const CATS = { food: 'খাবার', transport: 'যাতায়াত', bills: 'বিল', other: 'অন্যান্য' };
const tk = new Intl.NumberFormat('bn-BD', { style: 'currency', currency: 'BDT' });
const mon = new Intl.DateTimeFormat('bn-BD', { month: 'long', year: 'numeric', timeZone: 'Asia/Dhaka' });
const money = (paisa) => tk.format(paisa / 100);
// ===== pure core: DOM নেই — ডেটা ঢোকে, নতুন ডেটা বেরোয় =====
const emptyState = { items: [], nextId: 1 };
const toPaisa = (text) => (text.trim() === '' ? NaN : Math.round(Number(text) * 100));
function validate(e) {
if (!Number.isSafeInteger(e.amount) || e.amount <= 0) return 'টাকার পরিমাণ শূন্যের বেশি হতে হবে';
if (!Object.hasOwn(CATS, e.category)) return 'অজানা খাত: ' + e.category;
const d = new Date(e.date + 'T00:00:00Z');
if (Number.isNaN(d.getTime()) || d.toISOString().slice(0, 10) !== e.date) return 'তারিখ ঠিক নেই';
return null;
}
const addExpense = (s, e) => ({ items: [...s.items, { ...e, id: s.nextId }], nextId: s.nextId + 1 });
const total = (items) => items.reduce((sum, e) => sum + e.amount, 0);
const sumBy = (items, keyOf) =>
items.reduce((m, e) => m.set(keyOf(e), (m.get(keyOf(e)) ?? 0) + e.amount), new Map());
const byCategory = (items) => sumBy(items, (e) => e.category);
const byMonth = (items) => sumBy(items, (e) => e.date.slice(0, 7));
const sortedByDate = (items) => items.toSorted((a, b) => a.date.localeCompare(b.date) || a.id - b.id);
const monthName = (ym) => mon.format(new Date(ym + '-15T12:00:00+06:00'));
// ===== self-test: core ঠিক আছে কি না, DOM ছাড়াই =====
let pass = 0;
const check = (name, ok) => (ok ? pass++ : console.error('FAIL:', name));
const s1 = addExpense(emptyState, { date: '2026-10-01', category: 'food', amount: 5000, note: '' });
check('পুরোনো state অক্ষত', emptyState.items.length === 0 && s1.items.length === 1);
check('পয়সায় রূপান্তর', toPaisa('19.99') === 1999 && Number.isNaN(toPaisa(' ')));
check('ঋণাত্মক বাতিল', validate({ ...s1.items[0], amount: -500 }) !== null);
check('toString খাত নয়', validate({ ...s1.items[0], category: 'toString' }) !== null);
check('৩০ ফেব্রুয়ারি নেই', validate({ ...s1.items[0], date: '2026-02-30' }) !== null);
console.log(`self-test: ${pass}/5 পাস`);
// ===== thin UI shell: DOM শুধু এখানে =====
let state = emptyState;
const past = []; // undo stack — আগের state-গুলো
const $ = (sel) => document.querySelector(sel);
function commit(next) { past.push(state); state = next; render(); }
function tryAdd(e) {
const error = validate(e);
$('#msg').textContent = error ?? '';
if (!error) commit(addExpense(state, e));
return error;
}
function render() {
const cat = $('#filter').value;
const shown = sortedByDate(state.items).filter((e) => cat === 'all' || e.category === cat);
$('#list').textContent = '';
for (const e of shown) {
const li = document.createElement('li');
li.textContent = `${e.date} · ${CATS[e.category]} · ${money(e.amount)} · ${e.note}`;
$('#list').append(li);
}
$('#sum').textContent = `মোট ${money(total(shown))} (${shown.length}টি)`;
$('#json').textContent = JSON.stringify(state.items, null, 2);
}
$('#f').addEventListener('submit', (ev) => {
ev.preventDefault(); // পাতা reload হতে দেব না
tryAdd({ date: $('#date').value, category: $('#cat').value,
amount: toPaisa($('#amount').value), note: $('#note').value.trim() });
});
$('#undo').addEventListener('click', () => { if (past.length) { state = past.pop(); render(); } });
$('#filter').addEventListener('change', render);
// ===== scripted demo: নির্দিষ্ট তারিখ, তাই আউটপুট সবসময় একই =====
[['2026-09-28', 'food', '250', 'দুপুরের খাবার'], ['2026-10-02', 'transport', '60', 'রিকশা'],
['2026-10-01', 'bills', '1200.50', 'ইন্টারনেট'], ['2026-10-05', 'food', '85.50', 'চা-শিঙাড়া'],
['2026-10-03', 'transport', '120', 'মেট্রো রেল'],
].forEach(([date, category, amt, note]) => tryAdd({ date, category, amount: toPaisa(amt), note }));
console.log('বাতিল:', tryAdd({ date: '2026-10-06', category: 'food', amount: toPaisa('-40'), note: '' }));
const report = (label) => console.log(label, state.items.length + 'টি, মোট', money(total(state.items)));
report('খাতায়:');
const sorted = sortedByDate(state.items);
console.log([...byCategory(sorted)].map(([c, p]) => `${CATS[c]} ${money(p)}`).join(', '));
console.log([...byMonth(sorted)].map(([m, p]) => `${monthName(m)}: ${money(p)}`).join(' | '));
$('#undo').click(); // বোতাম চাপার মতোই
report('undo-র পর:');প্রথম লাইন self-test: 5/5 পাস — পাঁচটি দাবি সত্যি: পুরোনো state অক্ষত, পয়সায় রূপান্তর, ঋণাত্মক টাকা বাতিল, toString খাত নয়, ৩০ ফেব্রুয়ারি নেই। কোনো দাবি মিথ্যা হলে console.error লাল রঙে FAIL দেখাত। তারপর বাতিল: টাকার পরিমাণ শূন্যের বেশি হতে হবে — −৪০ টাকার খরচ খাতায় ঢুকতে পারেনি। খাতায়: 5টি, মোট ১,৭১৬.০০৳ হলো ২৫০ + ৬০ + ১২০০.৫০ + ৮৫.৫০ + ১২০ টাকা। খাতওয়ারি লাইনে খাবার ৩৩৫.৫০৳, বিল ১,২০০.৫০৳, যাতায়াত ১৮০.০০৳ — ক্রমটি তারিখে সাজানো তালিকায় খাত প্রথম দেখা দেওয়ার ক্রম। মাসওয়ারি লাইনে সেপ্টেম্বর ২০২৬: ২৫০.০০৳ | অক্টোবর ২০২৬: ১,৪৬৬.০০৳. শেষে কোড নিজেই undo বোতামে click() চাপে, তাই সবশেষে যোগ হওয়া মেট্রো রেলের ১২০ টাকা বাদ যায়: undo-র পর: 4টি, মোট ১,৫৯৬.০০৳. ভুল খরচটি commit হয়নি বলে undo সেটিকে গোনেনি।
কোডটি পড়ার সময় চারটি অংশ চিনে নিন — প্রতিটির শুরুতে // ===== মন্তব্য আছে। Pure core অংশে একটিও document নেই। Self-test শুধু core-কে ডাকে। Thin UI shell-এ হিসাবের কোনো নিয়ম নেই — শুধু পড়া, ডাকা, আঁকা। আর scripted demo সেই একই tryAdd ডাকে, যা ফর্ম জমা দিলে ডাকা হয়; অর্থাৎ ডেমো আর আসল ব্যবহার একই পথে চলে।
এই ব্লক বাস্তবে ছোট ফাইলে ভাগ করা উচিত — core.js, ui.js. এখানে একটি ব্লকে রেখেছি, কারণ পাতার runner অন্য ফাইল import করতে পারে না। ফাইল ভাগ করা আর import/export আসবে পর্ব ৪৫-এ, আর নিজের check ফাংশনের বদলে আসল টেস্ট framework (Vitest) পর্ব ৪৮-এ।
৮ · হাতে টেস্ট — edge case-এর টেবিল
Self-test পাঁচটি দাবি প্রমাণ করে, কিন্তু ব্যবহারকারীর কল্পনা তার চেয়ে অনেক বড়। পেশাদার ডেভেলপাররা অ্যাপ হাতে দেওয়ার আগে edge case-এর একটি তালিকা ধরে নিজে চেষ্টা করেন — সীমানার মান, খালি মান, অদ্ভুত মান। ওপরের অ্যাপে প্রতিটি সারি নিজে চালিয়ে দেখুন:
| ইনপুট / কাজ | প্রত্যাশিত ফল | কোন লাইন সামলায় |
|---|---|---|
| টাকার ঘর খালি বা শুধু ফাঁকা | “টাকার পরিমাণ শূন্যের বেশি হতে হবে” | toPaisa → NaN |
0, -40 | একই বার্তা | e.amount <= 0 |
abc, 12টাকা | একই বার্তা | Number → NaN |
19.99 | ১৯.৯৯৳ (1999 পয়সা, 1998 নয়) | Math.round |
0.004 | বাতিল — গোল করে ০ পয়সা | Math.round + <= 0 |
1e3, 0x10 | গৃহীত — ১,০০০৳ আর ১৬৳ (জানা সীমাবদ্ধতা) | Number-এর নিয়ম; কড়া যাচাই পর্ব ৩০-এর regex-এ |
বিশাল সংখ্যা, যেমন 1e20 | বাতিল | Number.isSafeInteger |
| তারিখের ঘর খালি | “তারিখ ঠিক নেই” | round-trip তুলনা |
নোটে <b>হাই</b> | লেখাটি হুবহু দেখায়, মোটা অক্ষর হয় না | textContent |
| খাতা খালি অবস্থায় undo | কিছুই হয় না, কোনো error নেই | if (past.length) |
| ফিল্টার “বিল”, তারপর খাবারের খরচ যোগ | তালিকায় দেখায় না, কিন্তু JSON-এ আছে | render-এর filter |
| একই তারিখে দুটি খরচ | যোগ করার ক্রমে দেখায় | || a.id - b.id |
শেষ দিকের একটি সারি নিয়ে ভাবুন: ফিল্টার করা অবস্থায় যোগ করা খরচ তালিকায় দেখা যায় না। এটি বাগ, নাকি ঠিক আচরণ? কোড যা করার কথা তা-ই করছে, কিন্তু ব্যবহারকারী বিভ্রান্ত হতে পারেন — “আমার খরচ কোথায় গেল?” এমন প্রশ্নের উত্তর কোডে নেই, আছে চাহিদায়। সমাধান হতে পারে যোগ করার পর ফিল্টার “সব”-এ ফেরানো, বা একটি ছোট বার্তা দেখানো। Edge case-এর টেবিল শুধু বাগ ধরে না, নকশার অস্পষ্টতাও খুঁজে বের করে।
- Pure function কী? — একই ইনপুটে সবসময় একই আউটপুট দেয় আর বাইরের কিছু বদলায় না (DOM, global, ইনপুট object); যেমন
addExpense, কিন্তুrenderনয়। sortআরtoSorted-এর পার্থক্য? —sortমূল array বদলায় আর সেটিই ফেরত দেয়; ES2023-এরtoSortedনতুন সাজানো array দেয়, মূলটি অক্ষত।- reduce দিয়ে গ্রুপ করে যোগফল কীভাবে? — শুরুর মান
new Map(), প্রতিটি উপাদানেm.set(key, (m.get(key) ?? 0) + value), আরmফেরত। - submit handler-এ
preventDefaultকেন? — নইলে ব্রাউজার ফর্ম পাঠিয়ে পাতা reload করে, আর memory-র সব state হারায়। - ব্যবহারকারীর লেখা দেখাতে
innerHTMLকেন নয়? — লেখার ভেতরের HTML/script চলে যেতে পারে (XSS);textContentলেখাকে শুধু লেখা হিসেবে বসায়।
৯ · নিজের কম্পিউটারে চালানো আর পরের ধাপ
পাতার ভেতরের runner শেখার জন্য চমৎকার, কিন্তু আসল প্রজেক্ট থাকে আপনার কম্পিউটারে, নিজের ফোল্ডারে। পর্ব ০৩-এ VS Code বসিয়েছেন; এবার একটি ফোল্ডার hisab-khata বানিয়ে দুটি ফাইল রাখুন:
<!doctype html>
<html lang="bn">
<head>
<meta charset="utf-8">
<title>হিসাব খাতা</title>
<script src="script.js" defer></script>
</head>
<body>
<!-- এখানে পর্ব ২১-এর ফর্ম, #msg, #tools, #list আর details অংশটি -->
</body>
</html>script.js: ওপরের পুরো কোডটি কপি করে রাখুন।deferথাকায় script চলবে HTML পুরো পড়া শেষ হওয়ার পর — তাইquerySelectorelement খুঁজে পাবে (পর্ব ০৪)। চাইলে demo অংশটি মুছে দিন, তাহলে খাতা খালি শুরু হবে।- সবচেয়ে সহজ পথ:
index.html-এ ডাবল-ক্লিক করে ব্রাউজারে খুলুন। এই অ্যাপে কোনোfetchবা module নেই, তাইfile://থেকেও চলে। - ভালো পথ — Live Server: VS Code-এর Extensions থেকে “Live Server” বসিয়ে
index.html-এ ডান-ক্লিক → “Open with Live Server”. পাতা খোলেhttp://127.0.0.1:5500-এর মতো একটি ঠিকানায়, আর ফাইল সেভ করলেই নিজে থেকে reload হয়। পরে module আরfetchব্যবহার করলে এমন একটি local server লাগবেই। - Console খোলা রাখুন (F12): self-test-এর ফল, আর কোনো ভুল হলে লাল বার্তা সেখানেই দেখবেন। নিজের কম্পিউটারে
stateসরাসরি console-এ লিখে দেখতে পারবেন — শেখার জন্য খুব কাজের।
বাড়ানোর ধারণা — কোন পর্ব কোন দরজা খুলবে
💾 খাতা মনে রাখা
commit-এর ভেতরে একটি লাইন: localStorage.setItem('khata', JSON.stringify(state)), আর শুরুতে পড়া। আপনার কম্পিউটারে এটি এখনই চলবে; সীমা, ভুল আর IndexedDB পর্ব ৪০-এ।
🧪 আসল টেস্ট
নিজের check-এর বদলে Vitest দিয়ে core.js-এর টেস্ট, আর Playwright দিয়ে ফর্ম চেপে দেখা।
🌐 সার্ভারে খাতা
একাধিক ডিভাইসে একই খাতা — fetch দিয়ে API, পরে Express আর ডেটাবেস। core একই থাকবে।
⚛️ framework-এ নেওয়া
render-এর জায়গায় React বা Vue component; pure core হুবহু কপি করে নেওয়া যায়।
১০ · AI-এর যুগে — AI-কে pair programmer বানান, লেখক নয়
“একটি expense tracker বানিয়ে দাও” — যেকোনো AI এক মিনিটে দুইশো লাইনের একটি অ্যাপ দেবে, আর সেটি হয়তো চলবেও। কিন্তু তাতে আপনি কী শিখলেন? আর কোডটি ঠিক কি না, বুঝবেন কীভাবে? AI-এর দেওয়া পুরো অ্যাপে সাধারণত দেখা যায়: টাকা দশমিকে, innerHTML-এ ব্যবহারকারীর লেখা, sort দিয়ে state বদলানো, DOM আর হিসাব একই ফাংশনে মেশানো — এবং কোনো টেস্ট নেই।
ভালো পথ হলো pair programming-এর ঢঙে কাজ করা: চালকের আসনে আপনি, AI পাশে বসে রিভিউ করে। নিজে নকশা করুন, নিজে core লিখুন, তারপর AI-কে শুধু pure function দেখিয়ে রিভিউ আর edge case চান। ছোট, DOM-মুক্ত কোড AI খুব ভালো রিভিউ করে — আর তার প্রতিটি দাবি আপনি self-test-এ এক লাইনে যাচাই করতে পারেন।
নিচের prompt-এ আপনার নিজের core ফাংশনগুলো বসিয়ে দিন। লক্ষ করুন, এখানে কোড লিখে দিতে বলা হচ্ছে না — বলা হচ্ছে খুঁজে দিতে:
AI-এর রিভিউ হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলোই আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।
প্র ০১হিসাবের নিয়মকে UI থেকে আলাদা রাখা এত জরুরি কেন? “functional core, imperative shell” ধারণাটি কোথা থেকে এলো?
একটি রেস্টুরেন্টের কথা ভাবুন। রান্নাঘরে বাবুর্চি রান্না করেন, আর সামনে ওয়েটার অর্ডার নেন, খাবার পৌঁছে দেন। বাবুর্চি যদি নিজেই টেবিলে টেবিলে ঘুরে অর্ডার নিতেন, রান্নার মান যাচাই করা কঠিন হতো — কোন ভুল রান্নার, কোনটা পরিবেশনের, আলাদা করা যেত না। সফটওয়্যারেও একই কথা। যে ফাংশন একসাথে ফর্ম পড়ে, হিসাব করে আর পাতা বদলায়, তাকে টেস্ট করতে পুরো পাতা লাগে; তার একটি অংশ বদলালে বাকি অংশ ভাঙার ভয় থাকে।
এই সমস্যার একটি পরিষ্কার নাম দেন Gary Bernhardt — ২০১২ সালে তাঁর Destroy All Software screencast-এ “Functional Core, Imperative Shell” আর একই বছরের “Boundaries” নামের বক্তৃতায়। মূল কথা: সিদ্ধান্ত নেওয়ার সব যুক্তি রাখুন pure function-এর একটি “core”-এ, যেখানে কোনো input/output নেই — শুধু মান ঢোকে, মান বেরোয়। আর বাইরের জগতের সাথে সব কথা — DOM, নেটওয়ার্ক, ফাইল, সময় — রাখুন একটি পাতলা “shell”-এ, যেখানে প্রায় কোনো সিদ্ধান্ত নেই। Core-এর টেস্ট হয় দ্রুত আর অনেক বেশি (কোনো ব্রাউজার বা mock লাগে না); shell এত সরল যে কয়েকটি end-to-end টেস্টেই যথেষ্ট।
ধারণাটি একেবারে নতুন নয়। Alistair Cockburn-এর “hexagonal architecture” (ports and adapters, ২০০৫ সালের দিকে প্রকাশিত) একই কথা বলে — ব্যবসার যুক্তি মাঝখানে, বাইরের প্রযুক্তি চারপাশে adapter হিসেবে। Haskell-এর মতো pure functional ভাষা এই ভাগকে টাইপ দিয়েই বাধ্য করে: input/output করা কোডের টাইপ আলাদা। আর ১৯৭০-এর দশকের Smalltalk থেকে আসা MVC (Model-View-Controller) নকশাতেও model আর view আলাদা রাখার কথা ছিল।
আমাদের খাতায় এর লাভ সরাসরি দেখা যায়। Self-test-এ একটিও ফর্ম নেই, তবু হিসাবের সব নিয়ম প্রমাণিত। পর্ব ৪০-এ storage যোগ হলে শুধু commit-এ একটি লাইন বসবে; পর্ব ৫৪-এ React-এ নিলে render বদলাবে, core নয়; সার্ভারে Node.js-এ একই যাচাই চালাতে চাইলে core ফাইলটি হুবহু কপি করা যাবে — কারণ তাতে document নেই। এর দাম হলো শুরুতে একটু বেশি চিন্তা, আর কখনো কখনো ডেটা এক স্তর থেকে আরেক স্তরে পার করার বাড়তি কোড।
মূল উপলব্ধি: কোডকে “কী সিদ্ধান্ত নিতে হবে” আর “বাইরের জগতের সাথে কীভাবে কথা বলতে হবে” — এই দুই ভাগে আলাদা করলে সিদ্ধান্তের অংশটি টেস্টযোগ্য, পুনর্ব্যবহারযোগ্য আর প্রযুক্তি-বদলে টিকে থাকার মতো হয়।
প্র ০২“UI = f(state)” — পাতাকে state-এর একটি ফাংশন ভাবার ধারণা কেন এত প্রভাবশালী হলো? React এতে কী যোগ করল?
২০১০-এর দশকের শুরুতে ওয়েব অ্যাপ বানানোর সাধারণ পদ্ধতি ছিল সরাসরি DOM বদলানো — প্রায়ই jQuery দিয়ে। একটি খরচ যোগ হলে কোড নিজে খুঁজে বের করত কোন তালিকায় নতুন <li> বসাতে হবে, কোন মোট হালনাগাদ করতে হবে, কোন খালি-তালিকার বার্তা লুকাতে হবে। প্রতিটি পরিবর্তনের জন্য “আগের পাতা থেকে নতুন পাতায় যাওয়ার নির্দেশ” আলাদা করে লিখতে হতো। অ্যাপ বড় হলে এই নির্দেশগুলো একে অপরের সাথে জট পাকাত, আর পাতায় যা দেখা যাচ্ছে আর ডেটায় যা আছে, তা অমিল হয়ে যেত — বিখ্যাত উদাহরণ: “অপঠিত বার্তা ১” দেখায়, অথচ সব বার্তা পড়া।
২০১৩ সালের মে মাসে JSConf US-এ Facebook তাদের ভেতরে বানানো React library উন্মুক্ত করে; মূল নকশাকার ছিলেন Jordan Walke. React-এর কেন্দ্রীয় ধারণা ঠিক আমাদের render-এর মতো: আপনি শুধু বলবেন “এই state-এ পাতা দেখতে এমন” — অর্থাৎ declarative UI; কীভাবে আগের পাতা থেকে সেখানে যেতে হবে, সেটি library নিজে বের করবে। প্রথমে অনেকে হেসেছিলেন — প্রতিবার পুরো পাতা নতুন করে আঁকা তো ধীর! React-এর উত্তর ছিল virtual DOM: প্রতিবার একটি হালকা JavaScript object-এর গাছ বানানো, আগেরটির সাথে মিলিয়ে দেখা কী বদলেছে, আর আসল DOM-এ শুধু সেই পার্থক্যটুকু লাগানো। আমাদের render পুরো তালিকা মুছে আবার বানায় — দশটি খরচে সমস্যা নেই, দশ হাজারে হবে; React সেই জায়গাটিই সমাধান করেছে।
একই সময়ে Evan Czaplicki-র Elm ভাষা (২০১২) “model → view → update” চক্রকে আরও কড়াভাবে প্রতিষ্ঠা করে। Elm থেকে অনুপ্রাণিত হয়ে ২০১৫ সালে Dan Abramov আর Andrew Clark বানান Redux, যেখানে state বদলায় শুধু pure “reducer” ফাংশন দিয়ে — নামটিই এসেছে array-এর reduce থেকে: পুরোনো state আর একটি action নিয়ে নতুন state. Redux-এর প্রথম প্রদর্শনীতে Abramov দেখান “time-travel debugging” — পুরোনো state-গুলোর মধ্যে আগে-পিছে যাওয়া, ঠিক আমাদের undo stack-এর মতো।
আজ Vue, Svelte, SwiftUI, Jetpack Compose, Flutter — প্রায় সব আধুনিক UI টুল কোনো না কোনোভাবে এই নীতি মানে। আপনি এই পর্বে framework ছাড়াই সেই নীতিটি হাতে-কলমে বানালেন; পর্ব ৫৪-এ React দেখলে অনেক কিছুই চেনা লাগবে।
মূল উপলব্ধি: পাতা যখন state থেকে হিসাব করে বের হয়, তখন সত্য থাকে একটি জায়গায় — “কী দেখাব” বলা সহজ, “কীভাবে বদলাব” লিখে জট পাকানো কঠিন; ভালো টুল সেই কঠিন অংশটি আপনার কাছ থেকে সরিয়ে নেয়।
প্র ০৩প্রতিবার নতুন state বানানো কি মেমরির অপচয় নয়? Immutability আর undo-র সম্পর্ক কী, আর persistent data structure কী জিনিস?
প্রথম দেখায় মনে হয়, প্রতিটি খরচ যোগে পুরো খাতার নতুন কপি বানানো ভয়ংকর অপচয়। কিন্তু core-test-এর দ্বিতীয় লাইন মনে করুন: s1.items[0] === s3.items[0] দিয়েছিল true. নতুন array বানানো হয় ঠিকই, কিন্তু ভেতরের খরচ-object কপি হয় না — নতুন array শুধু পুরোনো object-গুলোর reference রাখে। পাঁচটি state-এর undo stack-এ খরচ-object মেমরিতে আছে মাত্র একবার করে; বাড়তি খরচ শুধু reference-এর array-গুলো। এই কৌশলের নাম structural sharing — যা বদলায়নি, তা ভাগাভাগি; যা বদলেছে, শুধু তার নতুন কপি।
এই ধারণাকে গবেষণার রূপ দেন James Driscoll, Neil Sarnak, Daniel Sleator আর Robert Tarjan — ১৯৮৬ সালে প্রকাশিত “Making Data Structures Persistent” প্রবন্ধে। Persistent data structure মানে এমন ডেটা কাঠামো, যার প্রতিটি পুরোনো সংস্করণ বদলের পরও অক্ষত ও ব্যবহারযোগ্য থাকে। ১৯৯৮ সালে Chris Okasaki-র বই “Purely Functional Data Structures” দেখায়, কীভাবে queue, tree, heap — সবই এভাবে দক্ষতার সাথে বানানো যায়। ২০০৭ সালে Rich Hickey-র Clojure ভাষা এই কাঠামোকে মূলধারায় আনে: তার vector আর map গাছের মতো গঠনের কারণে এক উপাদান বদলালে শুধু কয়েকটি ছোট node নতুন হয়, বাকি সব ভাগাভাগি। JavaScript-এ Facebook-এর Immutable.js (২০১৪) আর পরে Immer library একই ধারণা দেয়।
আমাদের সরল spread-পদ্ধতির একটি সীমা আছে: [...s.items, e] প্রতিবার পুরো reference-array কপি করে — n খরচে n ধাপ। দশ হাজার খরচ আর পঞ্চাশ ধাপের undo-তে সেটি টের পাওয়া যাবে; তখন আসল persistent কাঠামো বা অন্য কৌশল লাগে। বিকল্প কৌশল হলো Command pattern (১৯৯৪ সালের বিখ্যাত “Design Patterns” বইয়ে বর্ণিত): পুরো state না রেখে প্রতিটি কাজ আর তার উল্টো কাজ রাখা। মেমরি কম লাগে, কিন্তু প্রতিটি কাজের সঠিক উল্টো লেখা কঠিন আর ভুলপ্রবণ। Git-ও আসলে একটি persistent কাঠামো — প্রতিটি commit পুরো প্রজেক্টের ছবি, কিন্তু না-বদলানো ফাইল আগের commit-এর সাথে ভাগাভাগি করে।
Immutability-র আরেকটি লাভ undo-র চেয়েও বড়: “কী বদলেছে?” প্রশ্নের উত্তর এক তুলনায় — oldState === newState. পুরোনো object কেউ বদলায় না, তাই reference এক হলে ভেতরও এক। React-এর মতো library এই একটি তুলনা দিয়েই সিদ্ধান্ত নেয় পাতার কোন অংশ নতুন করে আঁকতে হবে।
মূল উপলব্ধি: Immutability মানে কপির পাহাড় নয় — structural sharing-এর কারণে নতুন সংস্করণ সস্তা, আর পুরোনো সংস্করণ অক্ষত থাকে বলে undo, time travel আর দ্রুত পরিবর্তন-শনাক্তকরণ প্রায় বিনামূল্যে আসে।
প্র ০৪JavaScript বন্ধ থাকলে বা লোড না হলে কি এই ফর্ম কাজ করত? Progressive enhancement কী, আর সেটি কখন জরুরি?
ধরুন রাফির ফোনে নেটওয়ার্ক দুর্বল, আর script.js লোড হতে আধা মিনিট লাগছে — বা কোনো কারণে লোডই হলো না। তিনি ফর্মে ৮৫.৫০ লিখে “যোগ করুন” চাপলেন। কী হবে? আমাদের submit listener তখনো বসেনি, তাই preventDefault-ও নেই। ব্রাউজার তার নিজের নিয়মে ফর্ম “পাঠাবে” — ফর্মে action নেই বলে একই পাতায়, আর পাতা reload হয়ে সব খালি। তাঁর চোখে অ্যাপটি কিছু না বলেই চুপচাপ ব্যর্থ।
২০০৩ সালে SXSW সম্মেলনে Steve Champeon আর Nick Finck এই সমস্যার জন্য একটি দর্শন তুলে ধরেন: progressive enhancement. মূল কথা: প্রথমে এমন একটি ভিত্তি বানান যা সবার জন্য কাজ করে — শুধু HTML, যা যেকোনো ব্রাউজার বোঝে; তার ওপর CSS দিয়ে সাজান; তারপর JavaScript দিয়ে অভিজ্ঞতা আরও ভালো করুন। JavaScript সেখানে “উন্নতি”, “শর্ত” নয়। এর বিপরীত পুরোনো পদ্ধতি ছিল “graceful degradation” — আগে সবচেয়ে আধুনিক ব্রাউজারের জন্য বানানো, তারপর পুরোনোগুলোতে যতটা সম্ভব চালানো।
আসল progressive enhancement-এ খরচের ফর্মটির action="/expenses" method="post" থাকত, আর সার্ভার খরচটি সেভ করে নতুন পাতা পাঠাত — JavaScript ছাড়াই পুরো কাজ হতো। JavaScript লোড হলে সেটি submit ধরে পাতা reload ছাড়াই একই কাজ দ্রুত করত। HTML নিজেও যাচাই করতে পারে: type="number", min="0.01", step="0.01", required দিলে ব্রাউজার JavaScript ছাড়াই খালি বা ঋণাত্মক টাকা আটকায় (পর্ব ৩৯)। তবে ব্রাউজারের যাচাই শুধু সুবিধা — সার্ভার বা আমাদের validate-এর যাচাই সবসময় লাগবে, কারণ ব্যবহারকারী চাইলে HTML বদলে দিতে পারেন।
আমাদের খাতার বাস্তবতা হলো, কোনো সার্ভার নেই বলে JavaScript ছাড়া এর কোনো কাজ করার পথ নেই — এটি একটি পুরোপুরি client-side অ্যাপ, আর এমন ক্ষেত্রে এটি গ্রহণযোগ্য সিদ্ধান্ত। কিন্তু সরকারি সেবার ফর্ম, ব্যাংকের আবেদন বা পরীক্ষার ফল দেখার পাতার মতো জায়গায়, যেখানে সবার কাছে পৌঁছানো জরুরি — পুরোনো ফোন, দুর্বল নেটওয়ার্ক, screen reader — সেখানে এই দর্শন প্রায় বাধ্যতামূলক। যুক্তরাজ্যের GOV.UK-এর নকশা-নির্দেশিকা এর পরিচিত উদাহরণ। আর আজকের React-ভিত্তিক framework-গুলোও (যেমন Remix, Next.js-এর server action) আবার এই দিকেই ফিরছে।
মূল উপলব্ধি: জিজ্ঞেস করুন “JavaScript না চললে ব্যবহারকারী কী দেখবেন?” — উত্তর যদি “নিঃশব্দ ব্যর্থতা” হয়, তাহলে অন্তত একটি স্পষ্ট বার্তা দিন; আর যেখানে সবার কাছে পৌঁছানো জরুরি, সেখানে HTML দিয়ে কাজ করে এমন ভিত্তি আগে বানান।
প্র ০৫টাকাকে পূর্ণসংখ্যা পয়সায় রাখার জেদ কেন? দশমিক টাকায় রাখলে ঠিক কী ভাঙে?
পর্ব ১০-এ দেখেছেন, JavaScript-এর number একটি IEEE 754 double, আর 0.1, 0.2, 0.3-এর মতো দশমিক সেখানে হুবহু থাকে না। খাতায় এর ফল কী হতো? ধরুন রাফি এক মাসে ৩০ দিন ৮৫.১০ টাকার চা খেলেন। দশমিক টাকায় যোগ করলে মোট হয় 2552.9999999999986 — যা toFixed(2) দেখালে ঠিক লাগে, কিন্তু total === 2553 তুলনা false দেয়। বাজেট-সীমা পরীক্ষা (“২৫৫৩ টাকা ছাড়ালে সতর্ক করো”) ভুল দিকে পড়তে পারে, আর দুই রিপোর্টের যোগফল এক পয়সা অমিল দেখাতে পারে। একজন ব্যবহারকারীর কাছে এক পয়সা তুচ্ছ; কিন্তু হিসাবের সফটওয়্যারে “যোগফল মেলে না” মানে আস্থা শেষ।
পয়সায় রাখলে প্রতিটি মান একটি পূর্ণসংখ্যা, আর 253 − 1-এর নিচে পূর্ণসংখ্যার যোগ-বিয়োগ হুবহু নির্ভুল — প্রায় ৯০ লক্ষ কোটি টাকা পর্যন্ত, ব্যক্তিগত খাতার জন্য যথেষ্টের চেয়ে অনেক বেশি। আমাদের validate-এ Number.isSafeInteger ঠিক এই সীমাটি পাহারা দেয়। দশমিক আসে মাত্র দুই জায়গায়: ইনপুটে (toPaisa-এ একবার Math.round) আর আউটপুটে (money-এ একবার ১০০ দিয়ে ভাগ)। মাঝের সব হিসাব পূর্ণসংখ্যায় — “সীমানায় রূপান্তর, ভেতরে নির্ভুল” নীতি।
এটি শুধু শিক্ষার কৌশল নয়, শিল্পের প্রচলিত রীতি। Stripe-এর মতো অনেক পেমেন্ট API টাকার পরিমাণ নেয় মুদ্রার সবচেয়ে ছোট এককে, পূর্ণসংখ্যা হিসেবে — ১০ ডলার মানে 1000 সেন্ট। Martin Fowler তাঁর “Patterns of Enterprise Application Architecture” (২০০২) বইয়ে “Money” pattern বর্ণনা করেন: পরিমাণ ছোট এককে পূর্ণসংখ্যা হিসেবে, সাথে মুদ্রার পরিচয়, আর ভাগ করার সময় পয়সা যেন হারিয়ে না যায় সেই নিয়ম। ডেটাবেসে সাধারণত DECIMAL বা পূর্ণসংখ্যার কলাম ব্যবহার করা হয়, FLOAT নয়।
সীমাও আছে। ভাগ বা শতাংশে (VAT, সুদ, বিল ভাগ) দশমিক আসবেই — তখন কোথায় আর কোন নিয়মে গোল হবে, সেটি ব্যবসার সিদ্ধান্ত, লিখে রাখতে হয় (পর্ব ১০-এর বিল ভাগের “ভাগফল + ভাগশেষ বিলি” কৌশল)। একাধিক মুদ্রা থাকলে প্রতিটির ছোট একক আলাদা — জাপানি ইয়েনে কোনো ভগ্নাংশ একক নেই, কিছু মুদ্রায় তিন দশমিক। আর সত্যিই বিশাল অঙ্কে BigInt আছে। ভবিষ্যতে JavaScript-এ একটি দশমিক টাইপ যোগ করার প্রস্তাবও TC39-এ আলোচনায় আছে, তবে এখনো মানদণ্ডে আসেনি।
মূল উপলব্ধি: টাকার প্রতিনিধিত্ব একটি নকশার সিদ্ধান্ত, পরে বদলানো কঠিন — তাই প্রথম দিন থেকেই পূর্ণসংখ্যা পয়সায় রাখুন, রূপান্তর শুধু সীমানায় করুন, আর গোল করার নিয়ম স্পষ্ট লিখে রাখুন।
অনুশীলন
চারটি কাজই খাতাকে বাড়ানো। প্রতিটির জন্য আগে pure function লিখুন আর self-test-এ একটি check যোগ করুন, তারপর UI-তে জুড়ুন — নিজের কম্পিউটারে, বা পুরো অ্যাপের ব্লকে বদলে।
-
একটি খরচ মোছা:
removeExpense(state, id)নামে একটি pure function লিখুন, যা ওই id-র খরচ বাদ দিয়ে নতুন state দেয়। ইঙ্গিত:filter. তারপর ভাবুন — তালিকার প্রতিটি সারির পাশে “মুছুন” বোতাম দিলে undo কি কোনো বাড়তি কোড ছাড়াই কাজ করবে?const removeExpense = (s, id) => ({ ...s, items: s.items.filter((e) => e.id !== id) });—nextIdবদলায় না, যাতে মোছা id আর কখনো ফিরে না আসে। টেস্ট:check('মোছা', removeExpense(s1, 1).items.length === 0 && s1.items.length === 1). UI-তেcommit(removeExpense(state, e.id))ডাকলেই হলো —commitপুরোনো state stack-এ রাখে, তাই undo বাড়তি কোড ছাড়াই মোছা খরচ ফিরিয়ে আনে। এটিই নকশার সুফল। প্রতিটি সারির বোতামে listener বসানোর বদলে তালিকায় একটি listener দিয়ে কাজ করার কৌশল (event delegation) শিখবেন পর্ব ৩৮-এ। -
খাতওয়ারি বাজেট:
const BUDGET = { food: 300000, transport: 100000 }(পয়সায়)।overBudget(items, budget, ym)লিখুন, যা একটি মাসে যেসব খাত বাজেট ছাড়িয়েছে তাদের তালিকা দেয়, যেমন[ ['food', 15000] ](কত পয়সা বেশি)। ইঙ্গিত: আগে মাস দিয়েfilter, তারপরbyCategory.const overBudget = (items, budget, ym) => [...byCategory(items.filter((e) => e.date.startsWith(ym)))].filter(([c, p]) => Object.hasOwn(budget, c) && p > budget[c]).map(([c, p]) => [c, p - budget[c]]);— সাজানো ফাংশনগুলো জোড়া দিয়েই নতুন বৈশিষ্ট্য, একটিও নতুন লুপ লাগেনি। সীমানার প্রশ্ন: ঠিক বাজেটের সমান হলে? আমরা>নিয়েছি, অর্থাৎ সমান হলে “ছাড়ায়নি” — এটি চাহিদার সিদ্ধান্ত, টেস্টে লিখে রাখুন। বাজেট নেই এমন খাত (bills)Object.hasOwnবাদ দেয়। UI-তেrender-এর শেষে ফল খালি না হলে#msg-এ সতর্কবার্তা দেখান। -
JSON থেকে খাতা ফেরানো (import): export করা লেখা একটি
<textarea>-এ পেস্ট করে খাতা ফিরিয়ে আনারimportItems(text)লিখুন। ইঙ্গিত:JSON.parseভুল লেখায় error ছোড়ে; প্রতিটি খরচ আবারvalidateদিয়ে যাচাই করুন — বাইরের ডেটা কখনো বিশ্বাস করবেন না।একটি সম্ভাব্য রূপ:
JSON.parse-কেtry { … } catch { return 'JSON পড়া গেল না'; }-এর ভেতরে রাখুন (try/catch-এর বিস্তারিত পর্ব ২৯-এ)। তারপর দেখুন ফলটি array কি না (Array.isArray), প্রতিটি খরচেvalidatenullদেয় কি না, আর id-গুলো সংখ্যা ও আলাদা কি না (new Set(ids).size === ids.length, পর্ব ১৮)। সব ঠিক হলে নতুন state:{ items, nextId: Math.max(0, ...ids) + 1 }, আরcommitদিয়ে বসান — তাহলে ভুল import-ও undo করা যাবে। একটিও খারাপ খরচ থাকলে পুরো import বাতিল করা নিরাপদ — অর্ধেক ঢোকা খাতা পুরো ভুলের চেয়েও বিভ্রান্তিকর। -
Redo যোগ করুন: undo করার পর “redo” চাপলে বাতিল কাজটি ফিরে আসবে। ইঙ্গিত: দ্বিতীয় একটি stack
future. নতুন খরচ যোগ হলেfuture-এর কী হওয়া উচিত?undo:
if (past.length) { future.push(state); state = past.pop(); render(); }. redo ঠিক উল্টো:if (future.length) { past.push(state); state = future.pop(); render(); }. আরcommit-এ নতুন একটি লাইন:future.length = 0;— কারণ undo-র পর নতুন কাজ করলে ইতিহাস একটি নতুন শাখায় চলে গেছে; পুরোনো “ভবিষ্যৎ” আর এই শাখার সাথে খাপ খায় না। প্রায় সব text editor ঠিক এভাবে কাজ করে। কিছু editor (যেমন Vim-এর undo tree) পুরোনো শাখাও মনে রাখে — persistent data structure থাকলে সেটিও সম্ভব, কারণ কোনো state কখনো নষ্ট হয় না।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমpure core + thin UI নকশার একটি খরচের অ্যাপে ফর্ম জমা দেওয়ার পর ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- submit event-এ
preventDefault()দিয়ে পাতা reload থামানো - ঘরগুলো থেকে মান পড়ে টাকাকে পূর্ণসংখ্যা পয়সায় রূপান্তর
validateদিয়ে যাচাই — ভুল হলে বার্তা দেখিয়ে থামা- pure
addExpenseদিয়ে নতুন state বানানো - পুরোনো state undo stack-এ রেখে নতুন state বসানো
- নতুন state থেকে পুরো তালিকা আর মোট নতুন করে আঁকা
নিচের কোন JavaScript ফাংশনটি pure function?
(s, e) => { s.items.push(e); return s; }(p) => { document.querySelector('#sum').textContent = p; }(s, e) => ({ ...s, items: [...s.items, e] })(e) => ({ ...e, savedAt: Date.now() })
const add = (s, e) => ({ items: [...s.items, e] }); const a = { items: [1] }; const b = add(a, 2); চালানোর পর a.items.length আর b.items.length কত?
2আর21আর21আর1- TypeError
[{c:'a',v:2},{c:'b',v:3},{c:'a',v:5}].reduce((m, x) => m.set(x.c, (m.get(x.c) ?? 0) + x.v), new Map()) কী দেয়?
10Map(3) { 'a' => 2, 'b' => 3, 'a' => 5 }Map(2) { 'a' => NaN, 'b' => NaN }Map(2) { 'a' => 7, 'b' => 3 }
JavaScript দিয়ে চালানো একটি ফর্মের submit listener-এ event.preventDefault() কেন ডাকা হয়?
- ব্রাউজারের ডিফল্ট কাজ — ফর্ম পাঠিয়ে পাতা reload — থামাতে, যাতে state না হারায়
- ফর্মের ঘরগুলো নিজে থেকে যাচাই করাতে
- একই event অন্য কোনো listener যেন না পায়, তা নিশ্চিত করতে
- জমা দেওয়ার পর ঘরগুলো খালি করতে
ব্যবহারকারীর লেখা একটি নোট তালিকায় দেখাতে li.textContent = note লেখা li.innerHTML = note-এর চেয়ে নিরাপদ কেন?
- textContent অনেক দ্রুত, তাই
- innerHTML বাংলা লেখা দেখাতে পারে না
- textContent লেখার ভেতরের HTML বা script চালায় না, শুধু লেখা হিসেবে দেখায়
- innerHTML দিয়ে li-তে কিছু বসানোই যায় না
খরচের হিসাবের অ্যাপে টাকার পরিমাণ পূর্ণসংখ্যা পয়সায় (যেমন ১২০০.৫০ টাকা → 120050) রাখার প্রধান কারণ কী?
- পূর্ণসংখ্যা কম মেমরি নেয়
- দশমিক number-এ যোগের ছোট ভুল জমে, কিন্তু পূর্ণসংখ্যার যোগ নির্ভুল
- Intl.NumberFormat দশমিক সংখ্যা ফরম্যাট করতে পারে না
- JSON-এ দশমিক সংখ্যা রাখা যায় না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- কোড লেখার আগে চাহিদা, পরিধির সীমা আর ডেটা মডেল লিখুন — টাকা পূর্ণসংখ্যা পয়সায়, তারিখ
'YYYY-MM-DD'string, প্রতিটি খরচের id. - Pure core + thin UI: হিসাবের নিয়ম DOM-মুক্ত pure function-এ; shell শুধু পড়ে, core-কে ডাকে, আঁকে — তাই core টেস্টযোগ্য ও পুনর্ব্যবহারযোগ্য।
- State বদলাবেন না, নতুন বানান: spread আর
toSorted; structural sharing নতুন state-কে সস্তা রাখে, আর পুরোনো state-এর stack-ই undo. reduce+Mapদিয়ে গ্রুপ করে যোগফল;Intl.NumberFormatআরIntl.DateTimeFormat(timeZone: 'Asia/Dhaka') শুধু দেখানোর সময়।- UI-তে
preventDefault, ব্যবহারকারীর লেখাtextContent-এ, edge case-এর টেবিলে হাতে টেস্ট — আর AI-কে লেখক নয়, pure function-এর রিভিউয়ার বানান।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২২ · scope chain আর closureপরবর্তী পর্বModule ৪-এর শুরু — ফাংশন কীভাবে তার চারপাশ মনে রাখে।
- JavaScript প্রোগ্রামিং · কোর্স হোম৫৯টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- Java · মাঝপথের প্রজেক্টপাশের ট্র্যাকএকই হিসাব খাতা — class, interface আর Command pattern দিয়ে terminal টুল।
- Python · মাঝপথের প্রজেক্টপাশের ট্র্যাকফাংশন, dict আর একটি কমান্ড-লুপ দিয়ে একই ধরনের CLI প্রজেক্ট।