ناعدد: NaN ملی یا یه فیچر خفن؟

چند روز پیش عکسی از صفحه‌ی «ثبت با موفقیت» اسنپ‌فود توی توییتر منتشر شد که به خاطر یه باگی، به جای قیمت‌ها، نوشته شده بود «ناعدد».

باگ «ناعدد تومان» اسنپ‌فود
باگ «ناعدد تومان» اسنپ‌فود


دوستان به شوخی و مزاح، شروع کردن به گفتن این که «عه NaN بومی‌سازی شده!» یا «NaN ملی رسید». البته اون دوستان میدونن که قضیه چیه و واقعا چه اتفاقی افتاده اما بعضی از افراد واقعا فکر میکردن که دولوپرای اسنپ‌فود واقعا کار خاصی کردن و از این موضوع ناراحت بودن!

به همین خاطر تصمیم گرفتم از یه فیچر خیلی خیلی زیبا و تو دل بروی ECMAScript و به نام آبجکت Intl و به خصوص NumberFormat بنویسم. اگه حوصله‌ی خوندن ندارین، فقط این CodePen رو ببینید.

سلب مسئولیت: (همون disclaimer خودمون) من هیچ ارتباطی با بچه‌های اسنپ‌فود ندارم و حدس میزنم دلیل «ناعدد» اینی باشه که من میگم. اون‌ها ممکنه از یه تکنیک دیگه استفاده کرده باشن.
شرمنده که من خیلی توی جزئیات نمیرم و جوری مینویسم تا کسایی که تجربه‌ی کمتری هم با Js دارن بتونن دنبال کنن.


تعریف مسئله

حتما شما هم به عنوان یک توسعه‌دهنده‌ی سمت کلاینت نیاز داشتید تا یک سری عددی رو به کاربرتون به صورت فارسی نمایش بدید.

مدار ۶۹ درجه جنوبی، دایره‌ای از عرض جغرافیایی است که در 69 درجهٔ جنوبی خط استوا قرار دارد.

اما حتما متوجه شدید که بعضی اوقات اعداد به صورت انگلیسی نمایش داده میشن (مثل 69) و بعضی اوقات هم به صورت فارسی (مثل ۶۹).

چرا؟ چون اون شخصی که متن رو نوشته، برای اعداد فارسی، کیبورد و عمه‌ی خودش هیچ احترامی قائل نبوده.

  • کاربرای گنو/لینوکسی همگی تاج سر هستند چون چیدمان کیبورد فارسیشون به صورت پیش‌فرض از اعداد فارسی ساپورت میکنه.
  • کاربرای macOS رو نمیدونم حقیقتا چون هیچوقت پول نداشتم که با مک کار کنم و خیلی نامردین که برای تولدم که ۱۰ روز پیش بود مک‌بوک نگرفتین.
  • کاربرای Windows امااا! کیبورد فارسی که به صورت پیش‌فرض برای زبان فارسی نصب میشه استاندارد نیست و خود کاربر باید کیبورد Persian (Standard) رو نصب کنه تا بتونه تا زمانی که فارسی تایپ میکنه، اعداد رو هم فارسی بنویسه (البته اعداد num pad همیشه انگلیسی تایپ میشن که همین هم ازشون انتظار میره). اگه شما هم کاربر ویندوز هستین شدیدا پیشنهاد میکنم در مورد چیدمان استاندارد فارسی بخونین و اون رو روی سیستمتون نصب کنین.

بجز اعداد، علائم نگارشی دیگه‌ای هم مثل ویرگول، جداکننده‌ی هزارگان، ممیز و خیلی چیزهای دیگه توی فارسی و انگلیسی با هم متفاوت هستن که حتما باید بهشون توجه بشه. در واقع برای توافق روی این موارد، خون‌های زیادی ریخته شده و آدم‌های زیادی هیچوقت به خونه برنگشتن.

تفاوت اعداد فارسی، انگلیسی و عربی (تایپ‌فیس وزیر)
تفاوت اعداد فارسی، انگلیسی و عربی (تایپ‌فیس وزیر)


کارکترهای این اعداد با هم متفاوت هستن و توی جدول یونیکد، در جاهای مختلفی قرار میگیرن. پس واضحه که:

راه حل

حالا که این مشکل وجود داره، چطوری میتونیم حلش کنیم؟ چطوری میتونیم به کاربر همه‌ی اعداد رو فارسی نشون بدیم؟

  • درست بنویسیم.
  • از طراحان فونت بخواییم که برامون جای 6 هم ۶ رو بزارن.
  • دستی همه‌ی اعداد رو خودمون (دولوپرها) عوض کنیم.
  • ؟؟

تجربه نشون داده همیشه نمیشه کاربر رو مجبور کرد که درست بنویسه. در ضمن، گاهی اوقات اعداد باید سمت سرور به صورت integer ذخیره بشن و سمت کلاینت فارسی نمایش داده بشن (مثل همین قیمت غذا).

تا به حال هم طراح فونتی رو ندیدم که قبول نکنه فونتی با اعداد فارسی بسازه (البته هیچ کدومشون هم خوشحال نیستن از این کار و تاییدش نمیکنن). فرض کنین کاربر متن رو کپی کنه جایی که پیست میکنه، فونتش اعداد رو به صورت فارسی نشون نده.

عوض کردن دستی تمام اعداد سمت کلاینت (در این جا Js) آخرین راه‌حل موجود هست. قبلا اینطوری بود که با regex یا جستجوی بین unicodeها هر عدد انگلیسی رو با مترادف فارسیش جا به جا میکردن و جوری که اتفاقاتی بشدت زشت و زننده رخ میداد (تصویر زیر برای افراد زیر ۱۸ سال، خانم‌های باردار و بیماران قلبی مناسب نمی‌باشد).

لطفا این کار رو نکنین!
لطفا این کار رو نکنین!


سالیان سال فرانت‌اند دولوپرها با این روش اعدادشون رو ترجمه میکردن. این راه حل جواب میداد ولی شکار گوزن با چوب و سنگ آتشفشانی هم جواب میداد.

راه حل متمدنانه برای ترجمه کردن اعداد انگلیسی به فارسی توی Js، استفاده کردن از Intl است.

استفاده از Intl.NumberFormat

آبجکت Intl نام API بین‌المللی‌سازی ECMAScript هست که به ما امکان فرمت کردن اعداد و تاریخ و استرینگ‌ها رو به زبان‌های مختلف میده.

با استفاده از کانستراکتورهای اون میشه:

  • دو استرینگ رو با هم مقایسه کرد
  • تاریخ رو فرمت کرد (خداحافظ moment.js!)
  • لیست ساخت (بین هر آیتم «،» گذاشت و برای آخری «و»!)
  • اعداد رو فرمت کرد (همین کاری که ما میخواییم بکنیم و بیشتر!)
  • اسامی رو جمع بست (برای فارسی کار نمیکنه متاسفانه ولی برای عربی فوق‌العاده‌است)
  • و نسب زمانی رو ساخت (مثلا گفت «دو ساعت پیش». هیچوقت برنگرد moment.js!)

اوصیکم به شخم زدن داکیومنت‌های مربوط به این آبجکت.

فقط قبل از این که بریم سراغ نحوه‌ی کار کردن باهاش، باید یه توضیحی در مورد استاندارد‌های زبانی بدم که متاسفانه فراخی اجازه نمیده. اگه دوست داشتید در مورد ISO 639-2، Unicode و فلربو بخونین (آخری هیچ ربطی به مقاله نداره).

کل چیزهایی که اینجا توضیح میدم رو توی CodePen هم گذاشتم.


کانستراکتور Intl.NumberFormat دقیقا همون کاری رو میکنه که ما لازم داریم. فقط کافیه بهش بگیم چه زبانی رو مد نظر داریم (در اینجا فارسی - 'fa' ) و مقدار عدد رو فرمت کنیم:

به همین سادگی. به همین خوشمزگی
به همین سادگی. به همین خوشمزگی


فقط توجه کنین که مقدار englishNumber یک عدد بود ولی الزامی وجود نداره که نوعش هم number باشه (ینی هم 123 قابل قبول هست و هم '123' ولی '12c' نیست) ولی خروجی همیشه نوعش string هست.

خوبی استفاده از Intl.NumberFormat اینه که اگه نیاز به جداکننده‌ی هزارگان یا ممیز باشه، خودش اونا رو هم اضافه و ترجمه میکنه!

توجه کنین که «٬» (ممیز) با «٬» (جداکننده‌ی هزارگان) فرق داره
توجه کنین که «٬» (ممیز) با «٬» (جداکننده‌ی هزارگان) فرق داره


هر چند میشه این فیچر گروه‌بندی رو با پاس دادن یه آبجکت به عنوان پارامتر دوم Intl.NumberFormat (که میشه تنظیماتش) با مقدار useGrouping: false غیرفعال کرد.

یکی دیگه از کارهای فوق‌العاده‌ای که میشه کرد، تبدیل کردن عدد به پول هست:

برای این کار، به عنوان آپشن، میگیم استایل این عدد پول باشه (currency) که در این صورت حتما باید واحد پول رو بعدش مشخص کنیم.

هر چقدر هم که باورش سخت باشه، واحد پولی ایران ریال هست که با کد IRRنمایش داده میشه. لیست کد تمامی واحد‌های پولی رو میشه از اینجا دریافت کرد.

بجز currency که نمایش پولی هست، میشه استایل‌های دیگه‌ای مثل percent که نمایش درصدی هست هم داشت.

باز هم میگم: حتما حتما حتما داکیومنت‌های مروبط به این آبجکت رو بخونین چون کلی فیچر و آپشن دیگه‌ای هم داره.

یعنی ازش استفاده کنیم دیگه؟

بله حتما.

ولی فکر نکنین که مشکلاتی وجود نداره.

اول از همه، در زمان نوشته شدن این متن، مرورگر حدود ۹۵٪ از کاربرای ایرانی Intl.NumberFormat رو ساپورت میکنه (که به نظر من خیلی خوبه). اون‌هایی هم که ساپورت نمیشن، مرورگرهای قدیمی روی موبایل هستن.

از سایت caniuse.com
از سایت caniuse.com


مورد بعدی این که اعدادتون برای همیشه عوض میشن. کاربر هیچوقت نمیتونه فاکتورش رو کپی کنه و توی ماشین حساب یا مثلا اکسل پیست کنه.

زمانی هم که از استایل پول استفاده میکنیم، واحد پولی بعد از خود مبلغ قرار میگیره و چون توی فارسی اعداد از چپ به راست هستن (پول هم عدده قاعدتا) «ریال» سمت راست قرار میگیره.

مورد آخر این که معمولا همه سعی میکنیم پول رو با «تومان» به کاربرامون نمایش بدیم که خب چون استاندارد نیست، باید دستی محاسبه‌اش کنیم.

ولی نگفتی ناعدد از کجا اومده

آهاااا! همه‌ی فرانت‌اند دولوپرها میدونن که وقتی یک چیزی عدد نباشه NaN یا همون Not-a-Number هست. مثلا هم 85 و هم '85' عدد هستن (هر چند اولی نوعش number هست و دومی نوعش string) ولی D Cup اصلا عدد نیست.

یادتونه قبلا گفتم که مقداری که میخوایین فرمت کنین، حتما باید عدد باشه؟ نمیشه که گفت برای «هندونه» ممیز بزار که.

به همین خاطر، اگه ورودی شما «هندونه» باشه، چیزی که برمیگرده، ارور هست و میگه چی؟ آفرین! NaN!
حالا چون Intl.NumberFormat خودش همه چیز مربوط به اعداد رو ترجمه میکنه، خروجی برای فرمت‌هایی که به زبان فارسی بودن میشود: «ناعدد».


آپدیت: دولوپرها اسنپ‌فود تایید کردن این حدس منو. اون‌ها از پلاگین react-intl استفاده میکنن که در واقع یه wrapper برای Intl هست.