Module ২ · Variable আর data type
Floating point: float, double, আর 0.1 কেন ঠিক 0.1 নয়
এই lesson-এ যা শিখবে
- বুঝেশুনে
floatবাdoubleবাছতে পারবে, আর এই track কেন সবসময়doubleনেয় বলতে পারবে। - এক অনুচ্ছেদে বোঝাতে পারবে
0.1 + 0.2কেন0.3না। - দুইটা দশমিক সংখ্যা
==দিয়ে না মিলিয়ে একটা সহনসীমা দিয়ে মেলাতে পারবে।
Maria-র দোকানের till তিনটা দাম যোগ করে এমন একটা মোট ছাপে, যেটা এক পয়সা কম। ও চারবার কাগজে মিলিয়ে দেখে। কাগজও ঠিক, মেশিনও ঠিক, অথচ দুইটা মেলে না।
কেউ ভুল করেনি। একটা দশমিক সংখ্যা binary বাক্সে ঠিকঠাক ধরে না, যেমন এক তৃতীয়াংশ দশমিকে ধরে না। এই lesson সেটা নিয়ে চলার কৌশল।
Binary বাক্সে একটা দশমিক সংখ্যা
এক তৃতীয়াংশ দশমিকে লেখো, পাও 0.333333, যতক্ষণ লিখতে চাও ততক্ষণ। যেখানেই থামো, তুমি এমন কিছু লিখেছ যেটা এক তৃতীয়াংশ না।
Computer-রও একই সমস্যা, দুই ভিত্তিতে। ওর অঙ্কগুলো হলো অর্ধেক, চার ভাগের এক, আট ভাগের এক আর ষোলো ভাগের এক, আর ওগুলোর কোনো যোগফলই ঠিক এক দশমাংশে গিয়ে পড়ে না।
তাই তুমি C-তে 0.1 লিখলে মেশিন তার সবচেয়ে কাছের যে মানটা বানাতে পারে সেটাই রাখে। ওই মানটা কাছাকাছি, আর ওটা 0.1 না।
#include <stdio.h>
int main(void)
{
double a = 0.1;
double b = 0.2;
printf("0.1 is stored as : %.20f\n", a);
printf("0.2 is stored as : %.20f\n", b);
printf("0.3 is stored as : %.20f\n", 0.3);
return 0;
}
0.1 is stored as : 0.10000000000000000555
0.2 is stored as : 0.20000000000000001110
0.3 is stored as : 0.29999999999999998890
%.20f বিন্দুর পরে বিশটা ঘর চায়, আর ওভাবেই তুমি দেখতে পাও ভেতরে আসলে কী আছে। তোমার লেখা আর সব program ছয় ঘর ছেপেছে আর এই পুরোটা লুকিয়ে রেখেছে।
তাই তোমার source-এ লেখা একটা দশমিক literal হলো একটা অনুরোধ, আর তুমি পাও মেশিনের বানাতে পারা সবচেয়ে কাছের মানটা।
float আর double: কে কতটা অঙ্ক ঠিক রাখে
দশমিকের জন্য C তোমাকে দুই মাপের বাক্স দেয়, যেগুলো তুমি সত্যিই ব্যবহার করবে।
| Type | মাপ | ঠিক রাখে যত অঙ্ক | সবচেয়ে বড় মান | printf |
|---|---|---|---|---|
float | 4 byte | প্রায় 7 | প্রায় 3.4 গুণ 10-র 38 ঘাত | %f |
double | 8 byte | প্রায় 15 বা 16 | প্রায় 1.7 গুণ 10-র 308 ঘাত | %f |
long double | এখানে 16 byte | 18 বা তার বেশি | আরও বড় | %Lf |
"ঠিক রাখে যত অঙ্ক" সংখ্যাটাই আসল, আর ওটা ফুরিয়ে যেতে তুমি চোখে দেখতে পারো।
#include <stdio.h>
int main(void)
{
float as_float = 3.141592653589793f;
double as_double = 3.141592653589793;
printf("float : %.15f\n", as_float);
printf("double: %.15f\n", as_double);
return 0;
}
float : 3.141592741012573
double: 3.141592653589793
দুইটা লাইন অঙ্ক ধরে ধরে মেলাও। সাতটা অঙ্ক পর্যন্ত মেলে, তারপর float বানিয়ে বানিয়ে বলছে।
সাধারণভাবে double নাও। তুমি যেসব processor ব্যবহার করবে তার কোনোটাতেই ওটা ধীর না, আর বাঁচানো 4 byte কখনো হারানো আট অঙ্কের সমান না।
প্রথম literal-র শেষের f-টা খেয়াল করো। ওটা ছাড়া সংখ্যাটা একটা double হয়, তারপর সেটা float-এ চেপে ঢোকে, মানে একই উত্তর একটু ঘুরপথে।
0.1 যোগ 0.2-র সমস্যা, বিশ ঘর পর্যন্ত ছাপা
এই program-টা প্রতিটা C শিক্ষার্থীর ঠিক একবার চালানো উচিত, আর তারপর সারাজীবন মনে রাখা উচিত।
#include <stdio.h>
int main(void)
{
double a = 0.1;
double b = 0.2;
double sum = a + b;
printf("0.1 + 0.2 = %.20f\n", sum);
printf("0.3 = %.20f\n", 0.3);
printf("are they the same? %d\n", sum == 0.3);
return 0;
}
0.1 + 0.2 = 0.30000000000000004441
0.3 = 0.29999999999999998890
are they the same? 0
0.3-র কাছের দুইটা মান যোগ হলো, আর উত্তরটা এমন একটা বেড়ার খুঁটিতে গিয়ে পড়ল, যেটা 0.3 নিজে যেখানে পড়ে তার থেকে আলাদা।
শেষ লাইনটা আরও একটা জিনিস দেখায়, যেটা আগেভাগে জেনে রাখা ভালো। C-তে একটা তুলনা হলো একটা expression, আর তার মান 1 বা 0। আমরা এইমাত্র যেভাবে করলাম, সেভাবে তুমি সেটা ছাপতে পারো।
তাই দুইটা দশমিক সংখ্যায় == জিজ্ঞেস করে, দুইটা খুঁটি কি একই খুঁটি। তুমি প্রায় কখনোই ওই প্রশ্নটা করতে চাওনি।
সহনসীমা দিয়ে মেলানো
তুমি আসলে জানতে চেয়েছিলে "এরা কি এতটা কাছে যে এক ধরা যায়"। তাই সেটাই জিজ্ঞেস করো।
<math.h>-র fabs একটা পার্থক্যের মাপ দেয়, চিহ্ন বাদ দিয়ে। ওই মাপটাকে একটা ছোট সংখ্যার সঙ্গে মেলাও, যাকে প্রায়ই বলে epsilon।
#include <stdio.h>
#include <math.h>
int main(void)
{
double sum = 0.1 + 0.2;
double epsilon = 1e-9;
printf("the difference : %.20f\n", fabs(sum - 0.3));
printf("close enough? : %d\n", fabs(sum - 0.3) < epsilon);
return 0;
}
the difference : 0.00000000000000005551
close enough? : 1
1e-9 হলো 0.000000001-র বৈজ্ঞানিক লেখা। সাধারণ মাপের সংখ্যার জন্য এটা যুক্তিসঙ্গত epsilon, আর খুব বড় বা খুব ছোট সংখ্যার জন্য খারাপ।
কিছু system-এ math library জোড়া লাগাতে command line-এ -lm লাগে। Playground ওটা আগে থেকেই পাঠায়, তাই সেখানে fabs বাড়তি কিছু ছাড়াই চলে।
তাই নিয়মটা হলো: দুইটা দশমিক সংখ্যার মধ্যে কখনো == না, সবসময় একটা পার্থক্যকে বুঝেশুনে বাছা সহনসীমার সঙ্গে মেলানো।
Integer ভাগ, আর গড়ের ফাঁদ
C-তে দুইটা পূর্ণসংখ্যা ভাগ করলে পূর্ণসংখ্যাই পাও। ভগ্নাংশটা round হয় না; ওটা ফেলে দেওয়া হয়।
#include <stdio.h>
int main(void)
{
int a = 7;
int b = 2;
printf("int / int : %d\n", a / b);
printf("stored in a double: %f\n", (double)(a / b));
printf("done as decimals : %f\n", a / (double)b);
return 0;
}
int / int : 3
stored in a double: 3.000000
done as decimals : 3.500000
মাঝের লাইনটাই মানুষকে ধরে। Variable-টা একটা double, আর যে মানটা ওতে ঢুকেছে সেটা ঢোকার আগেই নষ্ট হয়ে গেছে।
সমাধানটা ভাগের উপর বসে, ফলের উপর না। এক পাশকে দশমিক type বানাও, আর পুরো ভাগটাই দশমিকে হবে।
তাই (a + b + c) / 3 একটা পূর্ণসংখ্যা, আর (a + b + c) / 3.0 না। একটা অক্ষর।
ছাপা: %f, %.2f, %e আর %g
একটা double-র মান একটাই, আর সেটা দেখানোর রকম অনেক।
একই সংখ্যা ছাপার চার রকম
double v = 1234.5678;
printf("%f", v) 1234.567800 ছয় ঘর, সবসময়
printf("%.2f", v) 1234.57 দুই ঘর, round করা
printf("%e", v) 1.234568e+03 বৈজ্ঞানিক লেখা
printf("%g", v) 1234.57 ছোট রূপ, ও নিজেই বাছে
%fছয় ঘর ছাপে, ওগুলোর মানে থাকুক বা না থাকুক।- Judge সাধারণত
%.2fচায়। বিন্দুর পরের সংখ্যাটা ঘরের গোনা। - যে মানের মাপ আগে থেকে জানো না, তার জন্য
%eভালো। scanf-এ একটাdouble-র জন্য লাগে%lf,%fনা। এটা সবাইকে একবার ধরে।
ছাপার সময় যে মানটা রাখা আছে সেটাই round হয়, তুমি যা লিখেছিলে সেটা না। সেজন্যই 1.005-র %.2f হলো 1.00, যেমনটা পরের Example দেখায়।
দশমিক type কখন একদমই ব্যবহার করবে না
টাকা আর গোনা দশমিক সংখ্যা না। ওগুলো সবচেয়ে ছোট এককের পূর্ণসংখ্যা, আর ওভাবেই রাখা উচিত।
ব্যাংক পয়সা রাখে, টাকা না। দোকান পয়সা রাখে। Invoicing system সেন্ট রাখে। তখন প্রতিটা যোগ নিখুঁত, আর দশমিক বিন্দুটা কেবল ছাপার সময় ফেরত বসে।
তাই যে নিয়মটা প্রতিটা পেশাদার programmer-র সঙ্গে সঙ্গে ঘোরে: দশমিক মাপার জন্য, পূর্ণসংখ্যা গোনার জন্য। টাকা গোনার জিনিস।
এক লাইন হিসাব, দুইবার ছাপা। ছয় ঘর সমস্যাটা লুকায় আর বিশ ঘর দেখিয়ে দেয়।
#include <stdio.h>
int main(void)
{
double third = 1.0 / 3.0;
printf("six places : %f\n", third);
printf("twenty places: %.20f\n", third);
return 0;
}
six places : 0.333333
twenty places: 0.33333333333333331483
দ্বিতীয় লাইনটা প্রায় ষোলো অঙ্কের পরে আর 3 থাকে না। Table-র ওই "প্রায় 15 বা 16 অঙ্ক" কথাটা এখানে সরাসরি চোখে দেখা যাচ্ছে।
Run in Compilerদুইটা মান যাদের সমান হওয়ার কথা, এক program-এ দুইভাবে মেলানো।
#include <stdio.h>
#include <math.h>
int main(void)
{
double sum = 0.1 + 0.2;
double target = 0.3;
double epsilon = 1e-9;
printf("with == : %d\n", sum == target);
printf("with tolerance : %d\n", fabs(sum - target) < epsilon);
printf("the difference : %.20f\n", fabs(sum - target));
return 0;
}
with == : 0
with tolerance : 1
the difference : 0.00000000000000005551
পার্থক্যটা প্রায় এক কোটি ভাগের এক কোটি ভাগের পাঁচ শতাংশের মতো। Epsilon একশো কোটি ভাগের এক, যেটা অনেক বড়, তাই দ্বিতীয় উত্তরটা 1।
Run in Compiler2.675 টাকার একটা bill-র লাইন। একবার double হিসেবে, একবার long long-এ পূর্ণ পয়সা হিসেবে।
#include <stdio.h>
int main(void)
{
double taka = 2.675;
long long paisa = 268; /* the same price, rounded to paisa first */
printf("as a double, 2 places : %.2f\n", taka);
printf("as a double, 20 places: %.20f\n", taka);
printf("as whole paisa : %lld.%02lld\n", paisa / 100, paisa - (paisa / 100) * 100);
return 0;
}
as a double, 2 places : 2.67
as a double, 20 places: 2.67499999999999982236
as whole paisa : 2.68
Till ছাপল 2.67 আর ক্রেতা আশা করেছিল 2.68। খারাপভাবে round কিছুই হয়নি: রাখা মানটা সত্যিই 2.675-র নিচে, তাই দুই ঘর মানে 2.67।
তৃতীয় লাইনটাই পেশাদার উত্তর। System-র কিনারায় একবার পয়সায় round করো, তারপর প্রতিটা যোগ পূর্ণ পয়সায় করো, যেখানে কিছুই সরে যেতে পারে না।
Run in Compilerএটা কোথায় কাজে লাগছে
- খেলার physics loop। Unreal আর Godot দুইটাই অবস্থান আর গতি
float-এ রাখে, কারণ এক frame-এ ওরকম লক্ষ লক্ষ লাগে আর এক মিটারের জন্য 7 অঙ্কই যথেষ্ট। বিশাল খোলা দুনিয়ায় সীমাটা ছুঁয়ে যায়, তখনdouble-এ যেতে হয়। - GPS-র স্থানাঙ্ক।
float-এ অক্ষাংশ প্রায় এক মিটার পর্যন্ত নিখুঁত;double-এ এক মিলিমিটারেরও অনেক কম পর্যন্ত। Mapping-র software ঠিক এই কারণেইdoubleব্যবহার করে। - Audio। একটা WAV file-র sample হলো 16 bit integer, কিন্তু audio engine-র ভেতরে মেশানো আর filter করার প্রতিটা ধাপ
float-এ হয়, তারপর বেরোনোর সময় আবার বদলে নেওয়া হয়। - ব্যাংকিং। Core banking system আর Stripe-র API, দুইটাই টাকার পরিমাণ রাখে সবচেয়ে ছোট এককের পূর্ণসংখ্যা হিসেবে। Stripe-র API 1099 নেয় আর বোঝায় 10.99, একটা integer ঘরে।
- Patriot missile battery, Dhahran, 1991। System সময় গুনত দশ ভাগের এক second-এ, একটা 24 bit fixed-point মানে, যেটা এক দশমাংশ ঠিকভাবে ধরতে পারত না। একশো ঘণ্টা চলার পরে ঘড়িটা প্রায় এক তৃতীয়াংশ second সরে গিয়েছিল, যেটা missile-র হিসাবে 600 মিটার।
যে ভুলগুলো সবাই করে
১. দশমিক মান %d দিয়ে ছাপা।
double price = 3.5;
printf("%d\n", price);
Playground-র GCC 12 কিছুই বলে না, কারণ format-র পরীক্ষাটা -Wall-র পিছনে থাকে। আমাদের দৌড়ে ওটা ছেপেছে 0, আর তোমার জন্যও একই ছাপবে এমন কোনো কথা নেই। নিজের মেশিনে gcc -Wall বলে warning: format '%d' expects argument of type 'int', but argument 2 has type 'double'। %f ব্যবহার করো।
২. double-কে %f দিয়ে পড়া।
double d = 0.0;
scanf("%f", &d);
printf("%f\n", d);
Playground-এ নীরব; input 3.5 দিলে ছেপেছে 0.000000। scanf-কে বলা হয়েছিল 8 byte-র বাক্সে 4 byte লিখতে। printf-এ একটা float নিজেই double হয়ে যায়, তাই %f দুইটারই কাজ চালায়; scanf-এ কিছুই বদলায় না, তাই double-র জন্য %lf লাগে।
৩. দুইটা integer ভাগ করে ভগ্নাংশ আশা করা।
int total = 7;
int count = 2;
double average = total / count;
printf("%f\n", average);
কোথাও কোনো message নেই। তুমি আশা করো 3.500000 আর ও ছাপে 3.000000। double উত্তরটা দেখার আগেই ভাগটা পূর্ণসংখ্যার কাজ হিসেবে শেষ হয়ে গেছে। লেখো total / (double)count।
৪. দুইটা দশমিক মান == দিয়ে পরীক্ষা করা।
double sum = 0.1 + 0.2;
printf("%d\n", sum == 0.3);
কোনো message নেই, আর ছাপে 0। মান দুইটা সতেরো নম্বর দশমিক ঘরে আলাদা, আর == সবগুলো ঘর নিয়েই প্রশ্ন করে। বরং একটা পার্থক্যকে epsilon-র সঙ্গে মেলাও।
Zara গোল টেবিল-mat কাটছে আর প্রতিটার ক্ষেত্রফল দরকার।
Input. এক লাইনে একটা দশমিক সংখ্যা r, সেন্টিমিটারে ব্যাসার্ধ।
Output. এক লাইনে ক্ষেত্রফল, ঠিক দুই দশমিক ঘর পর্যন্ত।
Constraints. 0.01 <= r <= 1000.00। পাই-র জন্য 3.14159265358979 ব্যবহার করো।
Sample. Input 2.5 দিলে 19.63।
#include <stdio.h>
int main(void)
{
double r = 0.0;
scanf("%lf", &r);
/* One printf, with %.2f. */
return 0;
}
circle-area নামে judge করা হয়। scanf-র %lf-টা ইচ্ছে করেই দেওয়া; ওটাকে %f করলে একসঙ্গে প্রতিটা test ব্যর্থ হয়।
Lesson 1-র অনুশীলন ৩ এবার শেষ করো, এখন যখন জানো ওটা কঠিন ছিল কেন। তিনটা পূর্ণসংখ্যা নম্বর, একটা গড়, দুই দশমিক ঘর।
Input. এক লাইনে তিনটা পূর্ণসংখ্যা a b c।
Output. এক লাইনে ওদের গড়, দুই দশমিক ঘর পর্যন্ত।
Constraints. 0 <= a, b, c <= 100।
Sample. Input 1 1 2 দিলে 1.33।
#include <stdio.h>
int main(void)
{
int a = 0;
int b = 0;
int c = 0;
scanf("%d %d %d", &a, &b, &c);
/* Change one character of (a + b + c) / 3 and it works. */
printf("%.2f\n", 0.0);
return 0;
}
এই module-এ judge করা হয় না। 1 1 2 input দিয়ে / 3 আর / 3.0 দুইটাই চালাও, আর দুইটা output পাশাপাশি রাখো।
Amara-র আবহাওয়ার পাতায় তাপমাত্রা দুই মাপেই দেখানো হয়। একটা Celsius পাঠকে Fahrenheit-এ বদলাও।
Input. এক লাইনে একটা দশমিক সংখ্যা c।
Output. এক লাইনে Fahrenheit মান, ঠিক এক দশমিক ঘর পর্যন্ত।
Constraints. -100.0 <= c <= 100.0। Fahrenheit হলো c গুণ 9 ভাগ 5, যোগ 32।
Sample. Input 37.0 দিলে 98.6।
#include <stdio.h>
int main(void)
{
double c = 0.0;
scanf("%lf", &c);
/* Write 9.0 and 5.0, not 9 and 5, and say why to yourself. */
printf("%.1f\n", 0.0);
return 0;
}
এই module-এ judge করা হয় না। c * 9 / 5 এখানে কাকতালীয়ভাবে কাজ করে, কারণ c আগে থেকেই একটা double। 9 / 5 * c করে না, আর কেন করে না সেটা বের করাই অনুশীলন।
এই lesson যে তুলনাটার পক্ষে যুক্তি দিচ্ছিল, সেটা বানাও। তিনটা দশমিক সংখ্যা পড়ো আর বলো প্রথম দুইটা যোগ করলে তৃতীয়টা হয় কি না।
Input. এক লাইনে তিনটা দশমিক সংখ্যা a b c।
Output. এক লাইনে: a + b যদি c-র 0.000000001-র মধ্যে থাকে তবে 1, নাহলে 0।
Constraints. -1000000 <= a, b, c <= 1000000। একটা লুকানো test হলো 0.1 0.2 0.3, আর সেখানে আশা করা উত্তর 1।
Sample. Input 0.1 0.2 0.3 দিলে 1। Input 1.0 1.0 3.0 দিলে 0।
#include <stdio.h>
#include <math.h>
int main(void)
{
double a = 0.0;
double b = 0.0;
double c = 0.0;
scanf("%lf %lf %lf", &a, &b, &c);
/* One printf with %d. A comparison is already a 1 or a 0. */
return 0;
}
close-enough নামে judge করা হয়। a + b == c লিখলে দ্বিতীয় sample-টা পাশ করে আর প্রথমটা ব্যর্থ হয়, আর সেটাই এই সমস্যার পুরো কথা।
যে প্রশ্নগুলো সবার মনে আসে
এটা কি C-র একটা bug?
না। Python, Java, JavaScript আর তোমার phone-র calculator সবাই একই কাজ করে, কারণ সবাই একই IEEE 754 রূপ ব্যবহার করে। C কেবল সেই ভাষা, যেটা তোমাকে ভেতরটা দেখতে দেয়।
Epsilon কত হওয়া উচিত?
1-র আশপাশের সংখ্যার জন্য
1e-9ভালো একটা শুরু। খুব বড় সংখ্যার ক্ষেত্রে পাশাপাশি দুইটা মানের ফাঁকই বড় হয়ে যায়, তাই নির্দিষ্ট epsilon আর কাজ করে না, তখন আপেক্ষিক পার্থক্য মেলাতে হয়।printfdouble-র জন্য%fনেয়, অথচscanf%lfনেয় কেন?printf-কে দেওয়াfloatনিজে থেকেইdoubleহয়ে যায়, তাই একটা specifier দুইটারই কাজ চালায়।scanfএকটা address পায় আর ওকে ঠিক কত byte লিখতে হবে বলে দিতে হয়।সবসময়
%.2fদিয়ে ছাপলে কি টাকাdouble-এ রাখা যায়?স্কুলের অনুশীলনের জন্য যায়। সত্যিকারের কিছুতে যায় না: অনেকগুলো যোগ পেরিয়ে ভুলগুলো জমতে থাকে, আর একদিন একটা মোট এক পয়সা সরে যায় যেটা কেউ খুঁজে পায় না।
কোন দশমিক সংখ্যাগুলো ঠিকঠাক রাখা যায়?
যেগুলোর ভগ্নাংশ অর্ধেকের যোগফল: 0.5, 0.25, 0.75, 0.125 আর এরকম। একটা
double-এ প্রায় 9 কোয়াড্রিলিয়ন পর্যন্ত প্রতিটা পূর্ণসংখ্যাও নিখুঁত।
মূল কথা
- একটা দশমিক literal সবচেয়ে কাছের binary মান হিসেবে রাখা হয়, যেটা সাধারণত তোমার লেখা মান না।
doubleপ্রায় 15 অঙ্ক রাখে আরfloatপ্রায় 7; এই track সব জায়গায়doubleনেয়।0.1 + 0.2 == 0.3হলো 0, আর%.20f-এ ছাপলে কারণটা চোখে দেখা যায়।- মেলাও
fabs(a - b) < epsilonদিয়ে, কখনো==দিয়ে না। - পূর্ণসংখ্যা ভাগ পূর্ণসংখ্যা মানে পূর্ণসংখ্যা, ফলটা যত চওড়া বাক্সেই রাখো।
- টাকা আর গোনা সবচেয়ে ছোট এককের পূর্ণসংখ্যা; দশমিক মাপার জিনিস।
পরের lesson দুইটা type নিয়ে, যারা সংখ্যা নয় বলে ভান করে, আর একটা তুলনাকে আরেকটা তুলনার সঙ্গে যোগ করা যায় কেন সেটা নিয়ে।
lesson ৩ শেষ
শেষ হলে চিহ্ন দিন, অগ্রগতি আপনার সাথে থাকবে।
পরেরটা: Char, bool, আর যে type-গুলো আসলে সংখ্যা