٢٥ مارس ٢٠٢١

النوع Date: التاريخ والوقت

حان وقت الحديث عن كائن آخر مضمّن في اللغة: التاريخ Date. يخزّن هذا الكائن التاريخ والوقت ويقدّم توابِع تُدير أختام التاريخ والوقت. يمكننا مثلًا استعماله لتخزين أوقات الإنشاء/التعديل أو حساب الوقت أو طباعة التاريخ الحالي في الطرفية.

الإنشاء

استدعِ new Date()‎ بتمرير واحدًا من الوُسطاء الآتية فتصنع كائن Date جديد:

new Date()
بلا وُسطاء: يُنشئ كائن Date بالتاريخ والوقت الحاليين
let now = new Date();
alert( now ); // نعرض التاريخ والوقت الحاليين

إليك كيفية إنشاء كائن Date: new Date(milliseconds)‎ يُنشئ كائن Date إذ تساوي قيمته عدد المليثوان الممرّرة (المليثانية هي 1/1000 من الثاني) حسابًا من بعد الأول من يناير عام ١٩٧٠ بتوقيت UTC+0.

// UTC+0
// 01.01.1970 نعني بـ 0 التاريخ
let Jan01_1970 = new Date(0);
alert( Jan01_1970 );

// نضيف الآن 24 ساعة لنحصل على  02.01.1970
let Jan02_1970 = new Date(24 * 3600 * 1000);
alert( Jan02_1970 );

يُسمّى العدد الصحيح الذي يمثّل عدد المليثوان التي مرّت من بداية عام 1970 بالختم الزمني (بصمة وقت). وهو يمثّل التاريخ بنحوٍ عددي خفيف (lightweight). يمكننا إنشاء التواريخ من الأختام الزمنية باستعمال new Date(timestamp)‎ وتحويل كائن التاريخ Date الموجود إلى ختم زمني باستعمال التابِع date.getTime()‎ (طالع أسفله).

والتواريخ قبل الأول من يناير 1970 أختامها سالبة:

// ‫31 ديسمبر 1969
let Dec31_1969 = new Date(-24 * 3600 * 1000);
alert( Dec31_1969 );

new Date(datestring)‎ لو كان هناك وسيط واحد وكان سلسلة نصيّة، فسيحلّله المحرّك تلقائيًا. الخوازرمية هنا هي ذات التي يستعملها Date.parse. لا تقلق، سنتكلم عنه لاحقًا.

let date = new Date("2017-01-26");
alert(date);

نجد في هذا المثال أن الوقت غير محدد لذا يكون بتوقيت GMT منتصف الليل، ويحدد وفقًا للمنطقة الزمنية التي تنفذ الشيفرة ضمنها، فالنتيجة يمكن أن تكون Thu Jan 26 2017 11:00:00 للبلدان ذات المنطقة الزمنية GMT+1100 أو يمكن أن تكون Wed Jan 25 2017 16:00:00 للبلدان الواقعة في المنطقة الزمنية GMT-0800.

new Date(year, month, date, hours, minutes, seconds, ms)‎ يُنشئ تاريخًا بالمكوّنات الممرّرة حسب المنطقة الزمنية المحلية. أوّل وسيطين إلزاميين أما البقية اختيارية.

يجب أن يكون العام year بأربع خانات: 2013 صح، 98 خطأ. يبدأ الشهر month بالرقم 0 (يناير) وينتهي بالعدد 11 (ديسمبر). مُعامل التاريخ date هو رقم اليوم من الشهر. لو لم يكن موجودًا فسيعدّه الكائن 1. لو لم تكن مُعاملات الساعة والدقيقة والثانية والمليثانية hours/minutes/seconds/ms موجودة، فسيعدّها الكائن 0.

مثال:

new Date(2011, 0, 1, 0, 0, 0, 0); // 1 Jan 2011, 00:00:00
new Date(2011, 0, 1); // نفس تلك. الساعات والدقائق وغيرها 0 مبدئيًا
The maximal precision is 1 ms (1/1000 sec):

أدنى دقّة للتاريخ هي مليثانية واحدة (واحد من ألف من الثانية):
let date = new Date(2011, 0, 1, 2, 3, 4, 567);
alert( date ); // 1.01.2011, 02:03:04.567

الوصول إلى مكوّنات التاريخ

إليك التوابِع التي تتيح لك الوصول إلى العام والشهر وغيرها داخل كائن Date:

إياك بـ getYear()‎ بل getFullYear()‎ تقدّم الكثير من محرّكات جافا سكريبت التابِع غير القياسي getYear()‎. هذا التابِع أصبح بائدًا، فهو يُعيد العام بخانتين أحيانًا. من فضلك لا تستعمله أبدًا، بل getFullYear()‎ لتجلب العام.

كما يمكن أيضًا جلب رقم اليوم من الشهر:

getDay()‎ يجلب رقم اليوم من الأسبوع، بدءًا بِـ 0 (الأحد) وحتى 6 (السبت). أوّل يوم هو الأحد دومًا. صحيح أنّ في بعض الدول هذا غير صحيح، لكن لا يمكن تغيير القيمة إطلاقًا.

تُعيد كلّ التوابِع أعلاه المكوّنات حسب المنطقة الزمنية المحلية.

توجد أيضًا مثيلاتها بنظام UTC حيث تُعيد اليوم والشهر والعام وغيرها في المنطقة الزمنية UTF+0:‏ getUTCFullYear()‎ وgetUTCMonth()‎ وgetUTCDay()‎. ضع كلمة "UTC" بعد "get" وستجد المثيل المناسب.

لو كانت منطقتك الزمنية المحلية بعيدة عن UTC، فستعرض الشيفرة أدناه الساعات مختلفة عن بعضها البعض:

// التاريخ الحالي
let date = new Date();

// الساعة حسب المنطقة الزمنية التي أنت فيها
alert( date.getHours() );

// ‫الساعة حسب المنطقة الزمنية بتوقيت UTC+0 (أي توقيت لندن بدون التوقيت الصيفي)
alert( date.getUTCHours() );

هناك (إضافةً إلى هذه التوابِع) تابِعان آخران مختلفان قليلًا ليس لهما نُسخ بتوقيت UTC:

getTime()‎ يُعيد ختم التاريخ الزمني، أي عدد المليثوان التي مرّت منذ الأول من يناير عام 1970 بتوقيت UTC+0.

getTimezoneOffset()‎ يُعيد الفرق بين المنطقة الزمنية الحالية وتوقيت UTC (بالدقيقة):

// ‫لو كانت منطقتك الزمنية UTC-1، فالناتج 60
// لو كانت منطقتك الزمنية ‫UTC+3، فالناتج ‎-180
alert( new Date().getTimezoneOffset() );

ضبط مكوّنات التاريخ

تتيح لك التوابِع الآتية ضبط مكوّنات التاريخ والوقت:

لدى كلّ تابع منها نسخة بتوقيت UTC (عدا setTime()‎). مثال: setUTCHours()‎.

كما رأيت فيمكن لبعض التوابِع ضبط عدّة مكوّنات في آن واحد مثل setHours. المكوّنات التي لا تُمرّر لا تُعدّل.

مثال:

let today = new Date();

today.setHours(0);
alert(today); // ما زال اليوم نفسه، ولكن الساعة تغيّرت إلى 0

today.setHours(0, 0, 0, 0);
alert(today); // ما زال اليوم نفسه، ولكنّا عند 00:00:00 تمامًا.

التصحيح التلقائي

ميزة التصحيح التلقائي في كائنات التواريخ Date مفيدة جدًا لنا، إذ يمكن أن نضع قيم تاريخ لامنطقية (مثل الخمسون من هذا الشهر) وسيُعدّلها الكائن بنفسه.

مثال:

let date = new Date(2013, 0, 32); // ‫الثاني والثلاثين من يناير 2013؟!
alert(date); // ‫...آه، تقصد الأول من فبراير 2013!

تترتّب المكوّنات اللامنطقية تلقائيًا. فمثلًا لو أضفت على التاريخ ”28 فبراير 2016“ يومين اثنين، فيمكن أن يكون ”الثاني من مارس“ أو ”الأول من مارس“ لو كانت السنة كبيسة. بدل أن نفكّر بهذا الحساب، نُضيف يومين ونترك الباقي على كائن Date:

let date = new Date(2016, 1, 28);
date.setDate(date.getDate() + 2);

alert( date ); // ‫1 مارس 2016

غالبًا ما تُستعمل هذه الميزة لنجلب التاريخ بعد فترة محدّدة من الزمن. فلنقل مثلًا نريد تاريخ ”70 ثانية من الآن“:

let date = new Date();
date.setSeconds(date.getSeconds() + 70);

alert( date ); // يعرض التاريخ الصحيح

يمكننا أيضًا ضبط القيمة لتكون صفرًا أو حتّى بالسالب. مثال:

let date = new Date(2016, 0, 2); // ‫2 يناير 2016

date.setDate(1); // نضبط التاريخ على أول يوم من الشهر
alert( date );

date.setDate(0); // أقل يوم ممكن هو 1، إذًا فيعدّ الكائن أنّ 0 هو آخر يوم من الشهر الماضي
alert( date ); // ‫31 ديسمبر 2015

تحويل التاريخ إلى عدد، والفرق بين تاريخين

حين يتحوّل كائن Date إلى عدد يصير ختمًا زمنيًا مطابقًا لختم date.getTime()‎:

let date = new Date();
alert(+date); // ‫عدد المليثوان، نفس ناتج date.getTime()‎

تأثير هذا المهم والخطير هو أنّك تستطيع طرح التواريخ من بعض، والناتج سيكون الفرق بينهما بالمليثانية. يمكن استعمال الطرح لحساب الأوقات:

let start = new Date(); // نبدأ قياس الوقت

// إلى العمل
for (let i = 0; i < 100000; i++) {
  let doSomething = i * i * i;
}

let end = new Date(); // ننتهي من قياس الوقت

alert( `The loop took ${end - start} ms` );

التاريخ الآن

لو أردنا قياس الوقت فقط فلا نحتاج كائن Date، بل هناك تابِعًا خاصًا باسم Date.now()‎ يُعيد لنا الختم الزمني الحالي.

يُكافئ هذا التابِع الجملةَ new Date().getTime()‎ إلّا أنّه لا يُنشئ كائن Date يتوسّط العملية، ولهذا هو أسرع ولا يزيد الضغط على عملية كنس المهملات. غالبًا ما يُستعمل التابِع لأنّه أسهل أو لأنّ الأداء في تلك الحالة مهم، مثلما في الألعاب بلغة جافا سكريبت أو التطبيقات المتخصّصة الأخرى.

ولهذا قد يكون الأفضل كتابة الشيفرة أدناه بدل تلك:

let start = Date.now(); // ‫تبدأ المليثوان من تاريخ 1 يناير 1970

// إلى العمل
for (let i = 0; i < 100000; i++) {
  let doSomething = i * i * i;
}

let end = Date.now(); // انتهينا

alert( `The loop took ${end - start} ms` ); // نطرح الأعداد لا التواريخ

قياس الأداء

لو أردنا قياس أداء دالة شرهة في استعمال المعالج، فعلينا أن نكون حذرين، هذا لو أردنا التعويل على القياس.

فلنقيس مثلًا دالتين اثنتين تحسبان الفرق بين تاريخين: أيهما أسرع؟ نُطلق على قياسات الأداء هذه… قياسات أداء ”Benchmark“.

// ‫أمامنا date1 وdate2، أيّ دالة ستُعيد الفرق بينهما (بالمليثانية) أسرع من الأخرى؟‫ هذه...
function diffSubtract(date1, date2) {
  return date2 - date1;
}

// ‫أم هذه...
function diffGetTime(date1, date2) {
  return date2.getTime() - date1.getTime();
}

وظيفة الدالتين متطابقة تمامًا، إلّا أن الثانية تستعمل التابِع date.getTime()‎ الصريح لتجلب التاريخ بالمليثانية، بينما الأخرى تعتمد على تحويل التاريخ إلى عدد. الناتج متطابق دومًا.

إذًا بهذه المعطيات، أيّ الدالتين أسرع؟

أوّل فكرة تخطر على البال هو تشغيل كلّ واحدة مرات عديدة متتابعة وقياس فرق الوقت. الدوال (في حالتنا هذه) بسيطة جدًا، ولهذا علينا تشغيل كلّ واحدة مئة ألف مرة على الأقل.

هيًا نقيس الأداء:

function diffSubtract(date1, date2) {
  return date2 - date1;
}

function diffGetTime(date1, date2) {
  return date2.getTime() - date1.getTime();
}

function bench(f) {
  let date1 = new Date(0);
  let date2 = new Date();

  let start = Date.now();
  for (let i = 0; i < 100000; i++) f(date1, date2);
  return Date.now() - start;
}

alert( 'Time of diffSubtract: ' + bench(diffSubtract) + 'ms' );
alert( 'Time of diffGetTime: ' + bench(diffGetTime) + 'ms' );

عجبًا! استعمال التابِع getTime()‎ أسرع بكثير! يعزو ذلك بسبب انعدام وجود تحويل للنوع (type conversion)، وهذا يسهّل على المحرّكات تحسين الأداء.

جميل، وصلنا إلى شيء، ولكنّ هذا القياس ليس قياسًا طيبًا بعد. تخيّل أنّ المعالج كان ينفّذ أمرًا ما بالتوازي مع تشغيل bench(diffSubtract)‎ وكان يستهلك الموارد، وما إن شغّلنا bench(diffGetTime)‎ كان ذلك الأمر قد اكتمل. هذا التخيّل هو تخيّل طبيعي لأمر واقعيّ جدًا حيث اليوم أنظمة التشغيل متعدّدة المهام. بهذا يكون لمرة القياس الأولى موارد معالجة أقل من المرة الثانية، ما قد يؤدّي إلى نتائج قياس خطأ.

إن أردنا التعويل على قياس الأداء، علينا إعادة تشغيل كل قياسات الأداء الموجودة أكثر من مرّة. هكذا مثلًا:

function diffSubtract(date1, date2) {
  return date2 - date1;
}

function diffGetTime(date1, date2) {
  return date2.getTime() - date1.getTime();
}

function bench(f) {
  let date1 = new Date(0);
  let date2 = new Date();

  let start = Date.now();
  for (let i = 0; i < 100000; i++) f(date1, date2);
  return Date.now() - start;
}

let time1 = 0;
let time2 = 0;

// ‫نشغّل bench(upperSlice)‎ وbench(upperLoop)‎ عشر مرات مرّة بمرّة
for (let i = 0; i < 10; i++) {
  time1 += bench(diffSubtract);
  time2 += bench(diffGetTime);
}

alert( 'Total time for diffSubtract: ' + time1 );
alert( 'Total time for diffGetTime: ' + time2 );

لا تبدأ محرّكات جافا سكريبت الحديثة بتطبيق التحسينات المتقدّمة إلّا على ”الشيفرات الحرجة“ التي تتنفّذ أكثر من مرّة (لا داعٍ بتحسين شيفرة نادرة التنفيذ). بهذا في المثال الأول، قد لا تكون مرات التنفيذ الأولى محسّنة كما يجب، وربما علينا إضافة تحمية سريعة:

// أضفناه لـ”تحمية“ المحرّك قبل الحلقة الأساس
bench(diffSubtract);
bench(diffGetTime);

// الآن نقيس
for (let i = 0; i < 10; i++) {
  time1 += bench(diffSubtract);
  time2 += bench(diffGetTime);
}

الزم الحذر متى ما أجريت قياسات أداء على المستوى الذرّي.

تُنفّذ محرّكات جافا سكريبت الحديثة عددًا كبيرًا من التحسينات، وقد تُغيّر نتائج ”الاختبارات الصناعية“ موازنةً ”بالاستعمال الطبيعي لها“، خصوصًا حين نقيس أداء ما هو صغير للغاية مثل طريقة عمل مُعامل رياضي، أو دالة مضمّنة في اللغة نفسها. لهذا، لو كنت تريد حقًا فهم الأداء كما يجب، فمن فضلك تعلّم طريقة عمل محرّك جافا سكريبت. حينها ربّما لن تحتاج هذه القياسات على المستوى الذرّي، أبدًا.

يمكنك أن تقرأ بعض المقالات الرائعة حول V8 هنا http://mrale.ph.

تحليل سلسلة نصية باستعمال Date.parse

يمكن أن يقرأ التابِع Date.parse(str) تاريخًا من سلسلة نصية. يجب أن يكون تنسيق تلك السلسلة هكذا: YYYY-MM-DDTHH:mm:ss.sssZ، إذ تعني:

  • YYYY-MM-DD – is the date: year-month-day.
  • The character "T" is used as the delimiter.
  • HH:mm:ss.sss – is the time: hours, minutes, seconds and milliseconds.
  • The optional 'Z' part denotes the time zone in the format +-hh:mm. A single letter Z would mean UTC+0.

يمكنك أيضًا استعمال تنسيقات أقصر مثل YYYY-MM-DD أو YYYY-MM أو حتّى YYYY.

باستدعاء Date.parse(str) فالسلسلة النصية تُحلّل حسب التنسيق فيها ويُعيد التابِع الختم الزمني (رقم المليثوان منذ الأول من يناير 1970 بتوقيت UTC+0). لو كان التنسيق غير صحيح فيُعيد NaN.

إليك مثالًا:

let ms = Date.parse('2012-01-26T13:51:50.417-07:00');

alert(ms); // ‫1327611110417 (ختم زمني)

يمكننا إنشاء كائن new Date مباشرةً من الختم الزمني:

let date = new Date( Date.parse('2012-01-26T13:51:50.417-07:00') );

alert(date);

ملخص

  • يُمثّل التاريخ والوقت في جافا سكريبت بكائن Date. لا يمكننا إنشاء ”تاريخ فقط“ أو ”وقتًا فقط“، فعلى كائنات التاريخ Date احتواء الاثنين معًا.
  • تُعدّ الأشهر بدءًا بالصفر (يناير هو الشهر صفر، نعم).
  • يُعدّ رقم اليوم من الأسبوع في getDay()‎ من الصفر أيضًا (وهو يوم الأحد).
  • يصحّح كائن التاريخ نفسه تلقائيًا حين تُضبط مكوّناته بقيم لا منطقية. يفيدنا لجمع/طرح الأيام والأشهر والأعوام.
  • يمكن طرح التواريخ ومعرفة الفرق بينها بالمليثانية، ذلك لأنّ كائن التاريخ يتحوّل إلى ختم زمني حين يتحوّل إلى عدد.
  • استعمل Date.now() لو أردت جلب الختم الزمني الحالي بسرعة.

لاحظ بأنّ الأختام الزمنية في جافا سكريبت هي بالمليثانية، على العكس من أنظمة عديدة أخرى.

نجد نفسنا بين الحين والآخر قياسات وقت دقيقة. للأسف فلا توفّر جافا سكريبت نفسها طريقة لحساب الوقت بالنانوثانية (1 على مليون من الثانية)، ولكن أغلب بيئاتها توفّر ذلك. فمثلًا تملك المتصفّحات التابِع performance.now()‎ إذ يُعيد عدد المليثوان منذ بدأ تحميل الصفحة بقدّة تصل إلى المايكروثانية (ثلاث خانات بعد الفاصلة):

alert(`Loading started ${performance.now()}ms ago`);
// Something like: "Loading started 34731.26000000001ms ago"
// .26 is microseconds (260 microseconds)
// more than 3 digits after the decimal point are precision errors, only the first 3 are correct

ما هو التاريخ الذي كان قبل كذا يوم؟

الأهمية: 4

أنشِئ دالة getDateAgo(date, days) تُعيد بتمرير التاريخ date اسم اليوم من الشهر قبل فترة days يوم.

مثال: لو كان اليوم العشرون من الشهر، فتُعيد getDateAgo(new Date(),1 )‎ التاسع عشر وgetDateAgo(new Date(), 2)‎ الثامن عشر.

يجب أن نعوّل بأن تعمل الدالة في حال days=356 وأكثر حتّى:

let date = new Date(2015, 0, 2);

alert( getDateAgo(date, 1) ); // ‫1، (1 يناير 2015)
alert( getDateAgo(date, 2) ); // ‫31، (31 ديسمبر 2014)
alert( getDateAgo(date, 365) ); // ‫2، (2 يناير 2014)

ملاحظة: يجب ألّا تُعدّل الدالة التاريخ date المُمرّر.

الحل

الفكرة بسيطة، أن نطرح عدد الأيام من التاريخ date:

function getDateAgo(date, days) {
  date.setDate(date.getDate() - days);
  return date.getDate();
}

ولكن… يجب ألّا تُعدّل الدالة على date. هذا مهم إذ أنّ الشيفرة خارج الدالة التي تُعطينا التاريخ لا تريد منّا تغييره. لننفّذ ذلك، علينا نسخ التاريخ هكذا أولًا:

function getDateAgo(date, days) {
  let dateCopy = new Date(date);

  dateCopy.setDate(date.getDate() - days);
  return dateCopy.getDate();
}

let date = new Date(2015, 0, 2);

alert( getDateAgo(date, 1) ); // ‫1، (1 يناير 2015)
alert( getDateAgo(date, 2) ); // ‫31، (31 ديسمبر 2014)
alert( getDateAgo(date, 365) ); // ‫2، (2 يناير 2014)

آخر يوم من الشهر كذا؟

الأهمية: 5

اكتب دالة getLastDayOfMonth(year, month) تُعيد آخر يوم من الشهر. أحيانًا يكون الثلاثين، أو الحادي والثلاثين أو الثامن/التاسع عشر من فبراير.

المُعاملات:

  • year – العام بأربع خانات، مثلًا 2012.
  • month – الشهر من 0 إلى 11.

مثال: getLastDayOfMonth(2012, 1) = 29 (سنة كبيسة، فبراير).

الحل

فلنصنع تاريخًا باستعمال الشهر التالي، ولكنّ نمرّر الصفر ليكون رقم اليوم:

function getLastDayOfMonth(year, month) {
  let date = new Date(year, month + 1, 0);
  return date.getDate();
}

alert( getLastDayOfMonth(2012, 0) ); // 31
alert( getLastDayOfMonth(2012, 1) ); // 29
alert( getLastDayOfMonth(2013, 1) ); // 28

عادةً ما تبدأ التواريخ بالواحد، لكن يمكننا (تقنيًا) تمرير أيّ عدد وسيُعدّل التاريخ نفسه. لذا حين نمرّر 0 نعني بذلك ”يومًا واحد قبل الأول من الشهر“، أي ”اليوم الأخير من الشهر الماضي“.

كم من ثانية مضت اليوم؟

الأهمية: 5

اكتب دالة getSecondsToday()‎ تُعيد عدد الثواني منذ بداية هذا اليوم. فمثلًا لو كانت الساعة الآن 10:00 am، وبدون التوقيت الصيفي، فستعطينا الدالة:

getSecondsToday() == 36000 // (3600 * 10)

يجب أن تعمل الدالة مهما كان اليوم. أيّ ألا تحتوي على قيمة داخلها بتاريخ ”اليوم“… اليوم.

الحل

لنعرف عدد الثواني يمكننا توليد تاريخًا باستعمال اليوم الحالي والساعة 00:00:00، وثمّ نطرح منها ”الوقت والتاريخ الآن“. سيكون الفرق حينها بعدد المليثوان منذ بداية هذا اليوم، فنقسمه على 1000 لنعرف الثواني فقط:

function getSecondsToday() {
  let now = new Date();

  // أنشِئ كائنًا باستعمال اليوم والشهر والسنة حاليًا
  let today = new Date(now.getFullYear(), now.getMonth(), now.getDate());

  let diff = now - today; // الفرق بالمليثانية
  return Math.round(diff / 1000); // نحوّله إلى ثوان
}

alert( getSecondsToday() );

الحل الآخر هو جلب الساعة والدقيقة والثانية وتحويلها إلى عدد الثواني:

function getSecondsToday() {
  let d = new Date();
  return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
}

كم من ثانية بقت حتّى الغد؟

الأهمية: 5

أنشِئ دالة getSecondsToTomorrow()‎ تُعيد عدد الثواني حتّى يحلّ الغد. فمثلًا لو كان الوقت الآن 23:00، تُعيد لنا:

getSecondsToTomorrow() == 3600

ملاحظة: يجب أن تعمل الدالة مهما كان اليوم، وألا تعتبر ”اليوم“ هذا اليوم.

الحل

لنعرف عدد المليثوان حتّى قدوم الغد، يمكننا أن نطرح من ”الغد 00:00:00“ التاريخ اليوم. أوّلًا، نولّد هذا ”الغد“ وثمّ ننفّذ الطرح:

function getSecondsToTomorrow() {
  let now = new Date();

  // تاريخ الغد
  let tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1);

  let diff = tomorrow - now; // الفرق بالمليثانية
  return Math.round(diff / 1000); // نحوّله إلى ثوان
}

حل بديل:

function getSecondsToTomorrow() {
  let now = new Date();
  let hour = now.getHours();
  let minutes = now.getMinutes();
  let seconds = now.getSeconds();
  let totalSecondsToday = (hour * 60 + minutes) * 60 + seconds;
  let totalSecondsInADay = 86400;

  return totalSecondsInADay - totalSecondsToday;
}

لاحظ أنّ هناك دولًا كثيرة تستعمل التوقيت الصيفي، لذا ستجد هناك أيام فيها 23 أو 25 ساعة. يمكن أن نتعامل مع هذه الأيام بنحوٍ منفصل.

تنسيق التاريخ نسبيًا

الأهمية: 4

اكتب دالة formatDate(date) تُنسّق التاريخ date حسب الآتي:

  • لو مرّت أقلّ من ثانية من date، فتُعيد "right now".
  • وإلّا، لو مرّت أقلّ من دقيقة من date، فتُعيد "n sec. ago".
  • وإلّا، لو أقل من ساعة، فتُعيد "m min. ago".
  • وإلّا، فتُعيد التاريخ كاملًا بالتنسيق "DD.MM.YY HH:mm"، أي (شَكلًا): الدقيقة:الساعة العام:الشهر:اليوم (كلها بخانتين). مثل: 31.12.16 10:00.

أمثلة:

alert( formatDate(new Date(new Date - 1)) ); // "right now"

alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago"

alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago"

// ‫تاريخ الأمس، مثلًا ‎31.12.16, 20:00
alert( formatDate(new Date(new Date - 86400 * 1000)) );

الحل

لنجلب الوقت المنقضي منذ date وحتّى الآن، سنطرح التاريخين.

function formatDate(date) {
  let diff = new Date() - date; // الفرق بالمليثانية

  if (diff < 1000) { // أقل من ثانية واحدة
    return 'right now';
  }

  let sec = Math.floor(diff / 1000); // نحوّل الفرق إلى ثوان

  if (sec < 60) {
    return sec + ' sec. ago';
  }

  let min = Math.floor(diff / 60000); // نحوّل الفرق إلى دقائق
  if (min < 60) {
    return min + ' min. ago';
  }

  // ننسّق التاريخ
  // ونُضيف أصفارًا لو كان اليوم/الشهر/الساعة/الدقيقة بخانة واحدة
  let d = date;
  d = [
    '0' + d.getDate(),
    '0' + (d.getMonth() + 1),
    '' + d.getFullYear(),
    '0' + d.getHours(),
    '0' + d.getMinutes()
  ].map(component => component.slice(-2)); // نأخذ الخانتين الأخيرتين من كلّ مكوّن

  // ندمج المكوّنات في تاريخ
  return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':');
}

alert( formatDate(new Date(new Date - 1)) ); // "right now"

alert( formatDate(new Date(new Date - 30 * 1000)) ); // "30 sec. ago"

alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "5 min. ago"

// ‫تاريخ الأمس، مثلًا ‎31.12.16, 20:00
alert( formatDate(new Date(new Date - 86400 * 1000)) );

حل بديل:

function formatDate(date) {
  let dayOfMonth = date.getDate();
  let month = date.getMonth() + 1;
  let year = date.getFullYear();
  let hour = date.getHours();
  let minutes = date.getMinutes();
  let diffMs = new Date() - date;
  let diffSec = Math.round(diffMs / 1000);
  let diffMin = diffSec / 60;
  let diffHour = diffMin / 60;

  // التنسيق
  year = year.toString().slice(-2);
  month = month < 10 ? '0' + month : month;
  dayOfMonth = dayOfMonth < 10 ? '0' + dayOfMonth : dayOfMonth;

  if (diffSec < 1) {
    return 'right now';
  } else if (diffMin < 1) {
    return `${diffSec} sec. ago`
  } else if (diffHour < 1) {
    return `${diffMin} min. ago`
  } else {
    return `${dayOfMonth}.${month}.${year} ${hour}:${minutes}`
  }
}

لاحظ بأنّ هذه الطريقة سيّئة لو أردت دعم اللغات دعمًا صحيحًا (في العربية هناك ثانية واحدة وثانيتين وثلاث ثوان وخمسون ثانية وهكذا).

ترجمة -وبتصرف- للفصل Date and time من كتاب The JavaScript language.

مهمه

قم بإنشاء غرض Date للتاريخ: Feb 20, 2012, 3:12am. باعتبار المنطقة الزمنية هي المنطقة المحلية.

قم بإظهار الغرض المنشأ باستخدام alert.

يقوم الباني new Date باستخدام المنطقة الزمنية المحلية. لذا فإن الشيء المهم الوحيد الذي يجب تذكره الآن هو أن الأشهر تبدأ من الصفر وليس من الواحد.

لذلك يتم تمثيل شهر شباط (فبراير) بالرقم 1.

Here’s an example with numbers as date components:

//new Date(year, month, date, hour, minute, second, millisecond)
let d1 = new Date(2012, 1, 20, 3, 12);
alert( d1 );

We could also create a date from a string, like this:

//new Date(datastring)
let d2 = new Date("February 20, 2012 03:12:00");
alert( d2 );

قم بكتابة الدالة getWeekDay(date) التي تقوم بإظهار يوم الأسبوع بتنسيق قصير: ‘MO’، ‘TU’، ‘WE’، ‘TH’، ‘FR’، ‘SA’، ‘SU’.

على سبيل المثال:

let date = new Date(2012, 0, 3);  // 3 Jan 2012
alert( getWeekDay(date) );        // يجب أن يظهر "TU"

افتح sandbox بالإختبارات.

تقوم الطريقة ()date.getDay بإرجاع رقم يوم الأسبوع المقابل، بدءاً من يوم الأحد.

لنقم بإنشاء مصفوفة من أيام الأسبوع، حتى نتمكن من الحصول على اسم اليوم المقابل من خلال رقمه:

function getWeekDay(date) {
  let days = ['SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA'];

  return days[date.getDay()];
}

let date = new Date(2014, 0, 3); // 3 Jan 2014
alert( getWeekDay(date) ); // FR

افتح الحل الإختبارات في sandbox.

تبدأ أيام الأسبوع في البلدان الأوروبية من يوم الإثنين (الرقم 1) ثم الثلاثاء (الرقم 2) وحتى يوم الأحد (رقم 7). اكتب دالة getLocalDay(date) تقوم بإرجاع يوم الأسبوع الأوروبي لتاريخ معين date.

let date = new Date(2012, 0, 3);  // 3 Jan 2012
alert( getLocalDay(date) );       // الثلاثاء، يجب إظهار 2

افتح sandbox بالإختبارات.

function getLocalDay(date) {

  let day = date.getDay();

  if (day == 0) { // اليوم 0 (الأحد) هو اليوم رقم 7 في أوروبا
    day = 7;
  }

  return day;
}

افتح الحل الإختبارات في sandbox.

قم بإنشاء دالة getDateAgo(date, days) تقوم بإرجاع يوم الشهر days المنصرم من التاريخ date.

على سبيل المثال، إذا كان اليوم الحالي هو 20، فيجب أن يرد الاستدعاء getDateAgo(new Date(), 1) القيمة 19 ويرد الاستدعاء getDateAgo(new Date(), 2) القيمة 18.

يجب أن يعمل التابع بشكل فعال في حال كانت القيمة days=365 أو أكثر:

let date = new Date(2015, 0, 2);

alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015)
alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014)
alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014)

ملاحظة: يجب أن لا يقوم التابع بتعديل قيمة الغرض date الممرر.

افتح sandbox بالإختبارات.

الفكرة بسيطة: نقوم بطرح عدد معين من الأيام من التاريخ date:

function getDateAgo(date, days) {
  date.setDate(date.getDate() - days);
  return date.getDate();
}

…ولكن يجب أن لا يغير التابع قيمة غرض التاريخ الممرر date. وهذه ملاحظة مهمة، لأن الكود الخارجي الذي يقوم بتمرير قيمة الغرض لا يتوقع أن يقوم التابع الخاص بنا بتعديل هذه القيمة.

لتنفيذ الطلب السابق، دعنا نقم باستنساخ التاريخ، كالتالي:

function getDateAgo(date, days) {
  let dateCopy = new Date(date);

  dateCopy.setDate(date.getDate() - days);
  return dateCopy.getDate();
}

let date = new Date(2015, 0, 2);

alert( getDateAgo(date, 1) ); // 1, (1 Jan 2015)
alert( getDateAgo(date, 2) ); // 31, (31 Dec 2014)
alert( getDateAgo(date, 365) ); // 2, (2 Jan 2014)

افتح الحل الإختبارات في sandbox.

قم بكتابة دالة getLastDayOfMonth(year, month) تقوم برد اليوم الأخير من الشهر. أحياناً تكون القيمة 30، 31 أو حتى 28/29 في حال كان الشهر هو شباط.

الوسطاء:

  • year – أربعة أرقام لتمثيل السنة، على سبيل المثال 2012.
  • month – الشهر، من 0 وحتى 11.

على سبيل المثال، getLastDayOfMonth(2012, 1) = 29 (سنة كبيسة، شباط/فبراير).

افتح sandbox بالإختبارات.

لنقم بإنشاء غرض من التاريخ باستخدام الشهر التالي، ولكن لنقم بتمرير 0 الممثل لليوم:

function getLastDayOfMonth(year, month) {
  let date = new Date(year, month + 1, 0);
  return date.getDate();
}

alert( getLastDayOfMonth(2012, 0) ); // 31
alert( getLastDayOfMonth(2012, 1) ); // 29
alert( getLastDayOfMonth(2013, 1) ); // 28

تبدأ التواريخ عادة من 1، ولكن من الناحية الفنية يمكننا تمرير أية رقم، وسيقوم غرض التاريخ بتصحيح نفسه بشكل تلقائي، لذلك وعند تمرير القيمة 0، فهي تعني “يوم واحد قبل اليوم الأول من الشهر”، بمعنى آخر: “اليوم الأخير من الشهر السابق”.

افتح الحل الإختبارات في sandbox.

قم بكتابة دالة ()getSecondsToday والتي تُرجع عدد الثواني المنصرمة منذ بداية اليوم الحالي.

على سبيل المثال، إذا كانت الساعة الحالية هي am 10:00، ولم يكن هناك تحول في الوقت الصيفي، فعندئذٍ:

getSecondsToday() == 36000 // (3600 * 10)

يجب أن يعمل التابع في أي يوم. أي أنه لا يجب أن يكون هناك قيمة ثابتة لليوم “today”.

للحصول على عدد الثواني، بإمكاننا توليد غرض من التاريخ باستخدام اليوم بحيث يكون الوقت 00:00:00، ثم نقوم بطرح هذا التاريخ من قيمة التاريخ “الآن”.

الفرق هو عدد المللي ثانية منذ بداية اليوم، والذي يجب أن نقسمه على 1000 للحصول على الثواني المطلوبة:

function getSecondsToday() {
  let now = new Date();

  // إنشاء غرض من التاريخ باستخدام السنة/الشهر/اليوم الحاليين
  let today = new Date(now.getFullYear(), now.getMonth(), now.getDate());

  let diff = now - today; // الفرق من مرتبة المللي ثانية
  return Math.round(diff / 1000); // التحويل للثواني
}

alert( getSecondsToday() );

يمكن استخدام الحل البديل، وهو الحصول على الساعات/الدقائق/الثواني الحالية وتحويلها جميعاً إلى فئة الثانية:

function getSecondsToday() {
  let d = new Date();
  return d.getHours() * 3600 + d.getMinutes() * 60 + d.getSeconds();
}

قم بإنشاء دالة ()getSecondsToTomorrow والتي تقوم بإرجاع عدد الثواني المتبقية حتى الغد.

على سبيل المثال، إذا كانت الوقت الحالي هو 23:00، فعندئذٍ:

getSecondsToTomorrow() == 3600

ملاحظة: يجب أن تعمل الدالة عند أي يوم، أي أن اليوم “today” لا يحمل أي قيمة ثابتة.

للحصول على عدد المللي ثانية حتى الغد، يمكننا من “الغد 00:00:00” طرح التاريخ الحالي.

أولاً، لنقم بإنشاء غرض الغد من التاريخ، ثم نقوم بعملية الطرح السابقة:

function getSecondsToTomorrow() {
  let now = new Date();

  // غرض تاريخ الغد
  let tomorrow = new Date(now.getFullYear(), now.getMonth(), now.getDate()+1);

  let diff = tomorrow - now; // الفرق من مرتبة المللي ثانية
  return Math.round(diff / 1000); // التحويل لمرتبة الثواني
}

حل بديل:

function getSecondsToTomorrow() {
  let now = new Date();
  let hour = now.getHours();
  let minutes = now.getMinutes();
  let seconds = now.getSeconds();
  let totalSecondsToday = (hour * 60 + minutes) * 60 + seconds;
  let totalSecondsInADay = 86400;

  return totalSecondsInADay - totalSecondsToday;
}

يرجى ملاحظة أن العديد من البلدان لديها التوقيت الصيفي (DST)، لذلك قد يكون هناك أيام بِ 23 أو 25 ساعة. لذلك، يجب معاملة هذه الأيام بشكل منفصل.

قم بكتابة دالة formateDate(date) والتي تقوم بتنسيق تاريخ معين date على النحو التالي:

  • إذا لم يمر من التاريخ date أكثر من ثانية، يظهر لنا "الآن".
  • وإلا، إذا لم يمر من التاريخ date أكثر من دقيقة، يظهر لنا "منذ n ثانية".
  • وإلا، إذا لم يمر من التاريخ date أكثر من ساعة، يظهر لنا "منذ m دقيقة".
  • وإلا، يظهر لنا التاريخ بشكل كامل في التنسيق "DD.MM.YY HH:mm". والذي هو: "day.month.year hours:minutes"، جميعها بتنسيق رقمين، مثلاً 31.12.16 10:00.

على سبيل المثال:

alert( formatDate(new Date(new Date - 1)) ); // "الآن"

alert( formatDate(new Date(new Date - 30 * 1000)) ); // "منذ 30 ثانية"

alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "منذ 5 دقيقة"

// تاريخ البارحة كـ 31.12.16 20:00
alert( formatDate(new Date(new Date - 86400 * 1000)) );

افتح sandbox بالإختبارات.

للحصول على الوقت المنصرم منذ التاريخ date حتى الآن – لنقم بطرح التاريخين.

function formatDate(date) {
  let diff = new Date() - date; // الفرق من مرتبة المللي ثانية

  if (diff < 1000) { // أقل من ثانية
    return 'الآن';
  }

  let sec = Math.floor(diff / 1000); // تحويل الفرق لمرتبة الثواني

  if (sec < 60) {
    return 'منذ ' + sec + ' ثانية';
  }

  let min = Math.floor(diff / 60000); // تحويل الفرق لمرتبة الدقائق
  if (min < 60) {
    return 'منذ ' + min + ' دقيقة';
  }

  // تنسيق التاريخ
  // إضافة الأصفار بادئة إلى اليوم/الشهر/الساعات/الدقائق المكونة من رقم واحد
  let d = date;
  d = [
    '0' + d.getDate(),
    '0' + (d.getMonth() + 1),
    '' + d.getFullYear(),
    '0' + d.getHours(),
    '0' + d.getMinutes()
  ].map(component => component.slice(-2)); // أخذ آخر رقمين من كل مكوّن

  // جمع المكونات في تاريخ واحد
  return d.slice(0, 3).join('.') + ' ' + d.slice(3).join(':');
}

alert( formatDate(new Date(new Date - 1)) ); // "الآن"

alert( formatDate(new Date(new Date - 30 * 1000)) ); // "منذ 30 ثانية"

alert( formatDate(new Date(new Date - 5 * 60 * 1000)) ); // "منذ 5 دقيقة"

// تاريخ الغد كـ 20:00 31.12.2016
alert( formatDate(new Date(new Date - 86400 * 1000)) );

حل بديل:

function formatDate(date) {
  let dayOfMonth = date.getDate();
  let month = date.getMonth() + 1;
  let year = date.getFullYear();
  let hour = date.getHours();
  let minutes = date.getMinutes();
  let diffMs = new Date() - date;
  let diffSec = Math.round(diffMs / 1000);
  let diffMin = diffSec / 60;
  let diffHour = diffMin / 60;

  // التنسيق
  year = year.toString().slice(-2);
  month = month < 10 ? '0' + month : month;
  dayOfMonth = dayOfMonth < 10 ? '0' + dayOfMonth : dayOfMonth;
  hour = hour < 10 ? '0' + hour : hour;
  minutes = minutes < 10 ? '0' + minutes : minutes;

  if (diffSec < 1) {
    return 'الآن';
  } else if (diffMin < 1) {
    return `منذ ${diffSec} ثواني`;
  } else if (diffHour < 1) {
    return `منذ ${diffMin} دقيقة`;
  } else {
    return `${dayOfMonth}.${month}.${year} ${hour}:${minutes}`
  }
}

افتح الحل الإختبارات في sandbox.

خريطة الدورة التعليمية

التعليقات

إقرأ هذا قبل أن تضع تعليقًا…
  • إذا كان لديك اقتراحات أو تريد تحسينًا - من فضلك من فضلك إفتح موضوعًا فى جيتهاب أو شارك بنفسك بدلًا من التعليقات.
  • إذا لم تستطع أن تفهم شيئّا فى المقال - وضّح ماهو.
  • إذا كنت تريد عرض كود استخدم عنصر <code> ، وللكثير من السطور استخدم <pre>، ولأكثر من 10 سطور استخدم (plnkr, JSBin, codepen…)