اذهب إلى المحتوى

استخدام التاريخ والوقت في لغة جو


هدى جبور

صُممت البرمجيات لتسهيل إنجاز الأعمال، وفي بعض الأحيان يتطلب الأمر التعامل مع التاريخ والوقت، إذ يمكن رؤية قيم التاريخ والوقت في معظم البرامج الحديثة، فمثلًا لو أردنا تطوير تطبيق يعطينا تنبيهات عن أوقات الصلاة، سيتوجب على البرنامج تشغيل تنبيه عند وقت وتاريخ محدد. مثال آخر هو تتبع الوقت في السيارات الحديثة، إذ نحتاج إلى إرسال تنبيهات إلى مالك السيارة لإبلاغه عن وجود مشكلة أو حاجة معينة للسيارة، أو تتبع التغييرات في قاعدة بيانات لإنشاء سجل تدقيق، أو الوقت الذي يتطلبه إنجاز عملية ما مثل عبور شارع معين للوصول إلى الوجهة …إلخ. يشير هذا إلى ضرورة التعامل مع التاريخ والوقت في البرامج والتفاعل معها وعرضها على المستخدمين بتنسيق واضح وسهل الفهم، فهذه خاصية أساسية لهكذا تطبيقات.

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

المتطلبات

لتتابع هذا المقال، ستحتاج إلى:

الحصول على التاريخ والوقت الحالي

سنستخدم خلال هذا القسم حزمة لغة جو time للوصول إلى قيم التاريخ والوقت الحالي. تُقدّم لنا هذه الحزمة القياسية من مكتبة لغة جو القياسية -العديد من الدوال المتعلقة بالتاريخ والوقت، والتي يمكن من خلالها تمثيل نقاط معينة من الوقت باستخدام النوع time.Time. يمكننا أيضًا من خلال هذه الدوال التقاط بعض المعلومات عن المنطقة الزمنية التي تمثِّل التاريخ والوقت المقابل.

كما هي العادة، سنحتاج لبدء إنشاء برامجنا إلى إنشاء مجلد للعمل ووضع الملفات فيه. يمكن أن نضع المجلد في أي مكان على الحاسب، إذ يكون للعديد من المبرمجين عادةً مجلدٌ يضعون داخله كافة مشاريعهم. سنستخدم في هذا المقال مجلدًا باسم "projects"، لذا فلننشئ هذا المجلد وننتقل إليه:

$ mkdir projects
$ cd projects

الآن، من داخل هذا المجلد، سنشغّل الأمر mkdir لإنشاء مجلد "datetime" ثم سنستخدم cd للانتقال إليه:

$ mkdir datetime
$ cd datetime

يمكننا الآن فتح ملف main.go باستخدام محرر نانو nano أو أي محرر آخر تريده:

$ nano main.go

نضيف الدالة main التي سنكتب فيها تعليمات الحصول على التاريخ والوقت وعرضهما:

package main

import (
    "fmt"
    "time"
)

func main() {
    currentTime := time.Now()
    fmt.Println("The time is", currentTime)
}

استخدمنا الدالة time.Now من الحزمة time للحصول على الوقت الحالي مثل قيمة من النوع time.Time وتخزينها في المتغير currentTime، ثم طبعنا قيمة هذا المتغير باستخدام الدالة fmt.Println، إذ سيُطبع وفقًا لتنسيق سلسلة نصية افتراضي خاص بالنوع time.Time.

شغّل الآن ملف البرنامج "main.go" باستخدام الأمر go run:

$ go run main.go

سيبدو الخرج الذي يعرض التاريخ والوقت الحاليين مشابهًا لما يلي:

The time is 2021-08-15 14:30:45.0000001 -0500 CDT m=+0.000066626

طبعًا في كل مرة نُشغّل فيها هذا البرنامج سيكون التاريخ والوقت مختلفين، كما أن المنطقة الزمنية ‎0500 CDT- مُتغيرة تبعًا للمنطقة الزمنية التي ضُبط عليها الحاسب كما سبق وأشرنا. نلاحظ وجود القيمة =m، التي تُشير إلى ساعة رتيبة monotonic clock، وتُستخدم ضمنيًّا في جو عند قياس الاختلافات في الوقت، وقد صُممت لتعويض التغييرات المحتملة في تاريخ ووقت ساعة نظام الحاسب أثناء تشغيل البرنامج. من خلال هذه الساعة، ستبقى القيمة المُعادة من الدالة time.Now صحيحة حتى لو جرى تغيير ساعة نظام الحاسب لاحقًا. مثلًا لو استدعينا الدالة time.Now الآن وكان الوقت 10:50، ثم بعد دقيقتين جرى تأخير لساعة الحاسب بمقدار 60 دقيقة، ثم بعد 5 دقائق (من الاستدعاء الأول للدالة time.Now) استدعينا الدالة time.Now مرةً أخرى (في نفس البرنامج)، سيكون الخرج 10:55 وليس 9:55. لست بحاجة إلى فهم أكثر من ذلك حول آلية عمل هذه الساعة خلال المقال، لكن إذا كنت ترغب في معرفة المزيد حول الساعات الرتيبة وكيفية استخدامها، لكن يمكنك الذهاب إلى التوثيق الرسمي ورؤية المزيد من التفاصيل لو أحببت.

قد يكون التنسيق الذي يظهر به التاريخ والوقت على شاشة الخرج غير مناسب وقد ترغب بتغييره أو أنه يتضمن أجزاء من التاريخ أو الوقت أكثر مما تريد عرضه. لحسن الحظ، يوفر النوع time.Time العديد من الدوال لتنسيق عرض التاريخ والوقت وعرض أجزاء محددة منهما؛ فمثلًا لو أردنا معرفة السنة فقط من المتغير currentTime يمكننا استخدام التابع Year أو يمكننا عرض الساعة فقط من خلال التابع Hour.

لنفتح ملف "main.go" مرةً أخرى ونضيف التعديلات التالية لرؤية ذلك:

...

func main() {
    currentTime := time.Now()
    fmt.Println("The time is", currentTime)

    fmt.Println("The year is", currentTime.Year())
    fmt.Println("The month is", currentTime.Month())
    fmt.Println("The day is", currentTime.Day())
    fmt.Println("The hour is", currentTime.Hour())
    fmt.Println("The minute is", currentTime.Hour())
    fmt.Println("The second is", currentTime.Second())
}

شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج:

The time is 2021-08-15 14:30:45.0000001 -0500 CDT m=+0.000066626
The year is 2021
The month is August
The day is 15
The hour is 14
The minute is 14
The second is 45

كما ذكرنا منذ قليل: في كل مرة نُشغّل فيها هذا البرنامج سيكون التاريخ أو الوقت مختلفين، لكن التنسيق يجب أن يكون متشابهًا. طبعنا في هذا المثال التاريخ كاملًا (السطر الأول)، ثم استخدمنا توابع النوع time.Time لعرض تفاصيل محددة من التاريخ كلٌ منها بسطر منفرد؛ بحيث عرضنا السنة ثم الشهر ثم اليوم ثم الساعة وأخيرًا الثواني. ربما نلاحظ أن الشهر طُبع اسمه August وليس رقمه كما في التاريخ الكامل، وذلك لأن التابع Month يعيد الشهر على أنه قيمة من النوع time.Month بدلًا من مجرد رقم، ويكون التنسيق عند طباعته سلسلة نصية string.

لنحدّث ملف "main.go" مرةً أخرى ونضع استدعاءات التوابع السابقة كلها ضمن دالة fmt.Printf لنتمكن من طباعة التاريخ والوقت الحاليين بتنسيق أقرب إلى ما قد نرغب في عرضه على المستخدم:

...

func main() {
    currentTime := time.Now()
    fmt.Println("The time is", currentTime)

    fmt.Printf("%d-%d-%d %d:%d:%d\n",
        currentTime.Year(),
        currentTime.Month(),
        currentTime.Day(),
        currentTime.Hour(),
        currentTime.Hour(),
        currentTime.Second())
}

شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج على النحو التالي:

The time is 2021-08-15 14:30:45.0000001 -0500 CDT m=+0.000066626
2021-8-15 14:14:45

الخرج الآن أقرب بكثير إلى ما نريد، ولكن لا تزال هناك بعض الأشياء التي يمكن تعديلها في الخرج، فمثلًا عُرض الشهر رقمًا هذه المرة، لأننا استخدمنا العنصر النائب d% الذي سيُجبر النوع time.Month على استخدام رقم وليس سلسلة نصية. هنا قد نفضل عرض رقمين 08 بدلًا من رقم واحد 8، وبالتالي يجب أن نغيّر تنسيق fmt.Printf وفقًا لذلك، ولكن ماذا لو أردنا أيضًا إظهار الوقت بتنسيق 12 ساعة بدلًا من 24 ساعة كما هو موضح في الخرج أعلاه؟ سيتطلب هذا بعض العمليات الحسابية الخاصة ضمن الدالة fmt.Printf.

بالرغم من إمكانية طباعة التواريخ والأوقات باستخدام fmt.Printf، ولكن يمكن أن يُصبح اﻷمر مرهقًا إذا أردنا إجراء تنسيقات أعقد، فقد ينتهي بنا الأمر بكتابة عدد كبير من الأسطر البرمجية لكل جزء نريد عرضه، أو قد نحتاج إلى إجراء عدد من العمليات الحسابية الخاصة لتحديد ما نريد عرضه.

أنشأنا في هذا القسم برنامجًا يستخدم الدالة time.Now للحصول على الوقت الحالي واستخدمنا دوال مثل Year و Hour على النوع time.Time لعرض معلومات عن التاريخ والوقت الحاليين، كما تعرّفنا على كيفية إجراء تنسيقات بسيطة على آلية عرضهما، مثل عرض أجزاء محددة من التاريخ والوقت. رأينا أيضًا أن عملية الحصول على تنسيقات أعقد تصبح أصعب باستخدام الدالة fmt.Printf. لتسهيل الأمر توفر لغة جو تابع خاص لتنسيق التواريخ والأوقات ويعمل بطريقة مشابهة لعمل الدالة fmt.Printf التي استخدمناها في البداية.

طباعة وتنسيق تواريخ محددة

إضافة إلى التوابع التي رأيناها في القسم السابق، يوفر النوع time.Time تابعًا يُدعى Format، يسمح لك بتقديم نسق layout على هيئة سلسلة نصية string (بطريقة مشابهة لتنسيق السلاسل في دالتي fmt.Printf و fmt.Sprintf). يُخبر هذا النسق التابع Format بالشكل الذي نريده لطباعة التاريخ والوقت.

نستخدم خلال هذا القسم نفس الشيفرة السابقة، ولكن بطريقة أكثر إيجازًا وسهولةً من خلال التابع Format. بدايةً ربما يكون من الأفضل معرفة كيف يؤثر تابع Format على خرج التاريخ والوقت في حال لم يتغير في كل مرة نُشغّل البرنامج، أي عندما نُثبّت التاريخ والوقت. نحصل حاليًا على الوقت الحالي باستخدام time.Now، لذلك في كل مرة نُشغّل البرنامج سيظهر رقم مختلف. هناك دالةٌ مفيدة أخرى توفرها الحزمة time، وهي الدالة time.Date التي تسمح بتحديد تاريخ ووقت محددين لتمثيلهم مثل قيم من النوع time.Time.

لنبدأ باستخدام الدالة time.Date بدلًا من time.Now في البرنامج. افتح ملف "main.go" مرةً أخرى واستبدل time.Now بدالة time.Date:

...

func main() {
    theTime := time.Date(2021, 8, 15, 14, 30, 45, 100, time.Local)
    fmt.Println("The time is", theTime)
}

تتضمن معاملات الدالة time.Date السنة والشهر واليوم والساعة والدقيقة والثواني من التاريخ والوقت اللذين نريد تمثيلهما للنوع time.Time. يمثل المعامل قبل الأخير النانو ثانية، والمعامل الأخير هو المنطقة الزمنية المطلوب إنشاء الوقت لها (نُغطي موضوع المناطق الزمنية لاحقًا). شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج على النحو التالي:

The time is 2021-08-15 14:30:45.0000001 -0500 CDT

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

تنسيق عرض التاريخ والوقت باستخدام تابع Format

تتضمن العديد من لغات البرمجة طريقة مشابهة لتنسيق التواريخ والأوقات التي تُعرض، ولكن الطريقة التي تُنشئ بها لغة جو تصميمًا لتلك التنسيقات قد تكون مختلفةً قليلًا عن باقي لغات البرمجة. يستخدم تنسيق التاريخ في اللغات الأخرى نمطًا مشابهًا لكيفية عمل Printf في لغة جو، إذ يُستخدم محرف % متبوعًا بحرف يمثل جزءًا من التاريخ أو الوقت المطلوب إدراجه. مثلًا قد تُمثّل السنة المكونة من 4 أرقام بواسطة العنصر النائب Y% موضوعًا ضمن السلسلة؛ أما في لغة جو تُمثّل هذه الأجزاء من التاريخ أو الوقت بمحارف تمثل تاريخًا محددًا. مثلًا، لتمثيل سنة معينة من 4 أرقام نكتب 2006 ضمن السلسلة. تتمثل فائدة هذا النوع من التصميم في أن ما نراه في الشيفرة يمثل ما سنراه في الخرج، وبالتالي عندما نكون قادرين على رؤية شكل الخرح، فإنه يسهل علينا التحقق من أن التنسيق الذي كتبناه يُطابق ما نبحث عنه، كما أنه يسهل على المطوّرين الآخرين فهم خرج البرنامج دون تشغيل البرنامج أصلًا.

يعتمد جو التصميم الافتراضي التالي، لعرض التاريخ والوقت:

01/02 03:04:05PM '06 -0700

إذا نظرت إلى كل جزء من التاريخ والوقت في هذا التصميم الافتراضي، فسترى أنها تزيد بمقدار واحد لكل جزء. يأتي الشهر أولًا 01 ثم يليه يوم الشهر 02 ثم الساعة 03 ثم الدقيقة 04 ثم الثواني 05 ثم السنة 06 (أي 2006)، وأخيرًا المنطقة الزمنية 07. يسهل هذا الأمر إنشاء تنسيقات التاريخ والوقت مستقبلًا. يمكن أيضًا العثور على أمثلة للخيارات المتاحة للتنسيق في توثيق لغة جو الخاصة بحزمة time.

سنستخدم الآن التابع الجديد Format لاستبدال وتنظيف تنسيق التاريخ الذي طبعناه في القسم الأخير. يتطلب الأمر -بدون Format- عدة أسطر واستدعاءات لدوال من أجل عرض ما نريده، لكن باستخدام هذه الدالة يُصبح الأمر أسهل وأنظف.

لنفتح ملف "main.go" ونضيف استدعاء جديد للدالة fmt.Println ونمرر لها القيمة المُعادة من استدعاء التابع Format على المتغير theTime:

...

func main() {
    theTime := time.Date(2021, 8, 15, 14, 30, 45, 100, time.Local)
    fmt.Println("The time is", theTime)

    fmt.Println(theTime.Format("2006-1-2 15:4:5"))
}

إذا نظرنا إلى التصميم المستخدم للتنسيق، فسنرى أنه يستخدم نفس التصميم الافتراضي في تحديد كيفية تنسيق التاريخ (January 2, 2006). شيء واحد يجب ملاحظته هو أن الساعة تستخدم 15 بدلًا من 03. يوضح هذا أننا نرغب في عرض الساعة بتنسيق 24 ساعة بدلًا من تنسيق 12 ساعة. شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج على النحو التالي:

The time is 2021-08-15 14:30:45.0000001 -0500 CDT
2021-8-15 14:30:45

نلاحظ أننا حصلنا على خرج مماثل للقسم الأخير لكن بطريقة أبسط. كل ما نحتاج إليه هو سطر واحد من التعليمات البرمجية وسلسلة تمثّل التصميم، ونترك الباقي للدالة Format. اعتمادًا على التاريخ أو الوقت الذي نعرضه، من المحتمل أن يكون استخدام تنسيق متغير الطول مثل التنسيق السابق عند طباعة الأرقام مباشرةً صعب القراءة لنا أو للمستخدمين أو لأي شيفرة أخرى تحاول قراءة القيمة. يؤدي استخدام 1 لتنسيق الشهر إلى عرض شهر آذار (مارس) بالرقم 3، بينما يحتاج شهر تشرين الأول (أكتوبر) محرفين ويظهر بالرقم 10.

افتح الملف "main.go" وأضِف سطرًا إضافيًا إلى البرنامج لتجربة نسق آخر أكثر تنظيمًا. هذه المرة سنضع بادئة 0 قبل أجزاء التاريخ والوقت الفردية، ونجعل الساعة تستخدم تنسيق 12 ساعة.

func main() {
    theTime := time.Date(2021, 8, 15, 14, 30, 45, 100, time.Local)
    fmt.Println("The time is", theTime)

    fmt.Println(theTime.Format("2006-1-2 15:4:5"))
    fmt.Println(theTime.Format("2006-01-02 03:04:05 pm"))
}

شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج:

The time is 2021-08-15 14:30:45.0000001 -0500 CDT
2021-8-15 14:30:45
2021-08-15 02:30:45 pm

بإضافة بادئة 0 إلى أجزاء سلسلة النسق، يصبح الرقم 8 للشهر في الخرج الجديد 08، ونفس الأمر بالنسبة للساعة التي أصبحت الآن بتنسيق 12 ساعة. لاحظ أن النسق الذي نكتبه يوافق ما نراه في الخرج، وهذا يجعل الأمور أبسط.

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

استخدام تنسيقات معرفة مسبقا

هناك العديد من التنسيقات جاهزة الاستخدام والشائعة للتاريخ، مثل العلامات الزمنية Timestamps لرسائل التسجيل log messages، وفي حال أردت إنشاء هكذا تنسيقات في كل مرة تحتاجها، سيكون أمرًا مملًا ومتعبًا. تتضمن حزمة time تنسيقات جاهزة يمكنك استخدامها لجعل الأمور أسهل واختصار الجهد المكرر. أحد هذه التنسيقات هو RFC 3339، وهو مستند يُستخدم لتحديد كيفية عمل المعايير على الإنترنت، ويمكن بعد ذلك بناء تنسيقات RFC على بعضها. تحدد بعض تنسيقات RFC مثل RFC 2616 كيفية عمل بروتوكول HTTP، وهناك تنسيقات تُبنى على هذا التنسيق لتوسيع تعريف بروتوكول HTTP. لذا في حالة RFC 3339، يحدد RFC تنسيقًا قياسيًا لاستخدامه في الطوابع الزمنية على الإنترنت. التنسيق معروف ومدعوم جيدًا عبر الإنترنت، لذا فإن فرص رؤيته في مكان آخر عالية.

تُمثّل جميع تنسيقات الوقت المُعرّفة مسبقًا في الحزمة الزمنية time بسلسلة ثابتة const string تُسمى بالتنسيق الذي تمثله. هناك اثنين من التنسيقات المتاحة للتنسيق RFC 3339، هما: time.RFC3339 و time.RFC3339Nano، والفرق بينهما أن التنسيق الثاني يُمثّل الوقت بالنانو ثانية.

لنفتح الآن ملف "main.go" ونعدّل البرنامج لاستخدام تنسيق time.RFC3339Nano للخرج:

...

func main() {
    theTime := time.Date(2021, 8, 15, 14, 30, 45, 100, time.Local)
    fmt.Println("The time is", theTime)

    fmt.Println(theTime.Format(time.RFC3339Nano))
}

نظرًا لأن التنسيقات المعرّفة مسبقًا هي قيم من النوع string للتنسيق المطلوب، نحتاج فقط إلى استبدال التنسيق الذي كُنا تستخدمه عادةً بهذا التنسيق (أو أي تنسيق جاهز آخر نريده). شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج على النحو التالي:

The time is 2021-08-15 14:30:45.0000001 -0500 CDT
2021-08-15T14:30:45.0000001-05:00

يُعد تنسيق RFC 3339 جيدًا للاستخدام إذا كنا بحاجة إلى حفظ قيمة زمنية مثل سلسلة نصية string في مكان ما، إذ يمكن قراءتها بسهولة من قبل العديد من لغات البرمجة والتطبيقات الأخرى، كما أنها مختصرة قدر الإمكان.

عدّلنا خلال هذا القسم البرنامج بحيث استخدمنا التابع Format لطباعة التاريخ والوقت، إذ يمنحنا هذا النسق المرن إمكانية الحصول على الخرج الأخير. أخيرًا، تعرّفنا واستخدمنا واحدًا من السلاسل النصية المعرفة مسبقًا لطباعة الوقت والتاريخ اعتمادًا على تنسيقات جاهزة ومدعومة يمكن استخدامها مباشرةً دون تكلّف عناء كتابة تصميمات تنسيق يدويًا.

سنعدّل في القسم التالي برنامجنا لتحويل قيمة السلسلة string إلى النوع time.Time لنتمكن من معالجتها من خلال تحليلها Parsing. يمكنك الاطلاع على مقال تحليل التاريخ والوقت في dot NET على أكاديمية حسوب لمزيدٍ من المعلومات حول مفهوم التحليل.

تحويل السلاسل النصية إلى قيم زمنية عبر تحليلها

قد نصادف في العديد من التطبيقات تواريخ ممثلة بسلاسل نصية من النوع string، وقد نرغب بإجراء بعض التعديلات أو بعض العمليات عليها؛ فمثلًا قد تحتاج إلى استخراج جزء التاريخ من القيمة، أو جزء الوقت، أو كامل القيمة للتعامل معها ..إلخ، إضافةً إلى إمكانية استخدام التابع Format لإنشاء قيم string من قيم النوع time.Time. تتيح لغة جو أيضًا إمكانية التحويل بالعكس، أي من سلسلة إلى time.Time من خلال الدالة time.Parse، إذ تعمل هذه الدالة بآلية مشابهة للتابع Format، بحيث تأخذ نسق التاريخ والوقت المتوقع إضافةً إلى قيمة السلسلة مثل معاملات.

لنفتح ملف "main.go" ونحدّثه لاستخدام دالة time.Parse لتحويل timeString إلى متغير time.Time:

...

func main() {
    timeString := "2021-08-15 02:30:45"
    theTime, err := time.Parse("2006-01-02 03:04:05", timeString)
    if err != nil {
        fmt.Println("Could not parse time:", err)
    }
    fmt.Println("The time is", theTime)

    fmt.Println(theTime.Format(time.RFC3339Nano))
}

على عكس التابع Format، تُعيد الدالة time.Parse قيمة خطأ محتملة في حالة عدم تطابق قيمة السلسلة المُمرّرة مع التنسيق المُمرّر. إذا نظرنا إلى النسق المستخدم، سنرى أن النسق المُعطى لدالة time.Parse يستخدم 1 للشهر 2 لليوم من الشهر ..إلخ، وهذا هو نفس النسق المُستخدم في تابع Format.

شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج كما يلي:

The time is 2021-08-15 02:30:45 +0000 UTC
2021-08-15T02:30:45Z

هناك بعض الأشياء التي يجب ملاحظتها في هذا الخرج، أولها هو أن المنطقة الزمنية الناتجة عن تحليل المتغير timeString هي المنطقة الزمنية الافتراضية، وهي إزاحة 0+، والتي تُعرف بالتوقيت العالمي المُنسّق Coordinated Universal Time -أو اختصارًا UTC أو توقيت غرينتش، فنظرًا لعدم احتواء القيمة الزمنية أو التصميم على المنطقة الزمنية، لا تعرف الدالة time.Parse المنطقة الزمنية التي تريد ربطها بها، وبالتالي تعدّه غرينتش. إذا كنت بحاجة لتحديد المنطقة الزمنية، يمكنك استخدام الدالة time.ParseInLocation لذلك. يمكن ملاحظة استخدام نسق time.RFC3339Nano، لكن الخرج لا يتضمن قيم بالنانو ثانية، وسبب ذلك هو أن القيم التي تحللها دالة time.Parse ليست نانو ثانية، وبالتالي تُضبط القيمة لتكون 0 افتراضيًا، وعندما تكون النانو ثانية 0، لن يتضمّن استخدام التنسيق time.RFC3339Nano قيمًا بالنانو ثانية في الخرج. يمكن للتابع time.Parse أيضًا استخدام أيًّا من تنسيقات الوقت المعرفة مسبقًا والمتوفرة في حزمة الوقت time عند تحليل قيمة سلسلة.

لنفتح ملف "main.go" ونعدّل قيمة timeString، يحبث نحل مشكلة عدم ظهور قيم النانو ثانية عند استخدام تنسيق time.RFC3339Nano ولنحدّث معاملات time.Parse بطريقة توافق التعديلات الجديدة:

...

func main() {
    timeString := "2021-08-15T14:30:45.0000001-05:00"
    theTime, err := time.Parse(time.RFC3339Nano, timeString)
    if err != nil {
        fmt.Println("Could not parse time:", err)
    }
    fmt.Println("The time is", theTime)

    fmt.Println(theTime.Format(time.RFC3339Nano))
}

شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج على النحو التالي:

The time is 2021-08-15 14:30:45.0000001 -0500 CDT
2021-08-15T14:30:45.0000001-05:00

يُظهر خرج Format هذه المرة بأن time.Parse كانت قادرة على تحليل كلٍ من المنطقة الزمنية وقيم النانو ثانية من متغير timeString.

تعلمنا في هذا القسم كيفية استخدام دالة time.Parse لتحليل سلسلة string تُمثّل تاريخًا معينًا وفقًا لتصميم معين، وتعلمنا كيفية تحديد المنطقة الزمنية للسلسلة المُحللة. سنتعرّف في القسم التالي على كيفية التعامل مع المناطق الزمنية بطريقة أوسع وكيفية التبديل بين المناطق الزمنية المختلفة من خلال الميزات التي يوفرها النوع time.Time.

التعامل مع المناطق الزمنية

يُشاع في التطبيقات التي يستخدمها مُستخدمين من أنحاء مختلفة من العالم -تخزّين التواريخ والأوقات باستخدام توقيت غرينيتش UTC، ثم التحويل إلى التوقيت المحلي للمستخدم عند الحاجة. يسمح ذلك بتخزين البيانات بتنسيق موحّد ويجعل الحسابات بينها أسهل، نظرًا لأن التحويل مطلوب فقط عند عرض التاريخ والوقت للمستخدم.

أنشأنا خلال المقال برنامجًا يعمل وفقًا للمنطقة الزمنية المحلية المتواجدين بها. لحفظ قيم البيانات من النوع time.Time على أنها قيم UTC، نحتاج أولًا إلى تحويلها إلى قيم UTC باستخدام التابع UTC الذي يُعيد القيمة الزمنية الموافقة لنظام UTC من متغير التاريخ الذي يستدعيها.

ملاحظة: يجري التحويل هنا من المنطقة المحلية التي يوجد بها الحاسب الذي نستخدمه إلى UTC، وبالتالي إذا كان حاسبنا موجود ضمن منطقة UTC لن نرى فرقًا.

لنفتح ملف "main.go" ونُطبق هذا الكلام:

...

func main() {
    theTime := time.Date(2021, 8, 15, 14, 30, 45, 100, time.Local)
    fmt.Println("The time is", theTime)
    fmt.Println(theTime.Format(time.RFC3339Nano))

    utcTime := theTime.UTC()
    fmt.Println("The UTC time is", utcTime)
    fmt.Println(utcTime.Format(time.RFC3339Nano))
}

هذه المرة يُنشئ البرنامج متغير theTime على أنه فيمة من النوع time.Time وفقًا لمنطقتنا الزمنية، ثم يطبعه بتنسيقين مختلفين، ثم يستخدم تابع UTC للتحويل من المنطقة الزمنية المحلية الحالية إلى المنطقة الزمنية UTC.

شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج على النحو التالي:

The time is 2021-08-15 14:30:45.0000001 -0500 CDT
2021-08-15T14:30:45.0000001-05:00
The UTC time is 2021-08-15 19:30:45.0000001 +0000 UTC
2021-08-15T19:30:45.0000001Z

سيختلف الخرج اعتمادًا على المنطقة الزمنية المحلية، ولكن سنرى في الخرج السابق أن المنطقة الزمنية في المرة الأولى كانت بتوقيت CDT (التوقيت الصيفي المركزي لأمريكا الشمالية)، وهي "5-" ساعات من UTC. بعد استدعاء تابع UTC وطباعة الوقت وفق نظام UTC، سنرى أن الوقت تغير من 14 إلى 19، لأن تحويل الوقت إلى UTC أضاف خمس ساعات. من الممكن أيضًا تحويل UTC إلى التوقيت المحلي باستخدام التابع Local على متغير النوع time.Time بنفس الأسلوب.

افتح ملف "main.go" مرةً أخرى، وضِف استدعاءً للتابع Local على المتغير utcTime لتحويله مرةً أخرى إلى المنطقة الزمنية المحلية:

...

func main() {
    theTime := time.Date(2021, 8, 15, 14, 30, 45, 100, time.Local)
    fmt.Println("The time is", theTime)
    fmt.Println(theTime.Format(time.RFC3339Nano))

    utcTime := theTime.UTC()
    fmt.Println("The UTC time is", utcTime)
    fmt.Println(utcTime.Format(time.RFC3339Nano))

    localTime := utcTime.Local()
    fmt.Println("The Local time is", localTime)
    fmt.Println(localTime.Format(time.RFC3339Nano))
}

شغّل البرنامج باستخدام go run:

$ go run main.go

ليكون الخرج:

The time is 2021-08-15 14:30:45.0000001 -0500 CDT
2021-08-15T14:30:45.0000001-05:00
The UTC time is 2021-08-15 19:30:45.0000001 +0000 UTC
2021-08-15T19:30:45.0000001Z
The Local time is 2021-08-15 14:30:45.0000001 -0500 CDT
2021-08-15T14:30:45.0000001-05:00

نلاحظ أنه جرى التحويل من UTC إلى المنطقة الزمنية المحلية، وهذا يعني طرح خمس ساعات من UTC، وتغيير الساعة من 19 إلى 14.

عدّلنا خلال هذا القسم البرنامج لتحويل البيانات الزمنية من منطقة زمنية محلية إلى توقيت غرينتش UTC باستخدام التابع UTC، كما أجرينا تحويلًا معاكسًا باستخدام التابع Local.

تتمثل إحدى الميزات الإضافية التي تقدمها حزمة time والتي يمكن أن تكون مفيدة في تطبيقاتك -في تحديد ما إذا كان وقت معين قبل أو بعد وقت آخر.

مقارنة الأوقات الزمنية

قد تكون مقارنة تاريخين مع بعضهما صعبة أحيانًا، بسبب المتغيرات التي يجب أخذها بالحسبان عند المقارنة، مثل الحاجة إلى الانتباه إلى المناطق الزمنية أو حقيقة أن الأشهر لها عدد مختلف من الأيام عن بعضها. توفر الحزمة الزمنية time تابعين لتسهيل ذلك، هما: Before و After اللذان يمكن تطبيقهما على متغيرات النوع time.Time. تقبل هذه التوابع قيمةً زمنيةً واحدة وتعيدان إما true أو false اعتمادًا على ما إذا كان الوقت المُمثّل بالمتغير الذي يستدعيهما قبل أو بعد الوقت المقدم.

لنفتح ملف "main.go" ونرى كيف تجري الأمور:

...

func main() {
    firstTime := time.Date(2021, 8, 15, 14, 30, 45, 100, time.UTC)
    fmt.Println("The first time is", firstTime)

    secondTime := time.Date(2021, 12, 25, 16, 40, 55, 200, time.UTC)
    fmt.Println("The second time is", secondTime)

    fmt.Println("First time before second?", firstTime.Before(secondTime))
    fmt.Println("First time after second?", firstTime.After(secondTime))

    fmt.Println("Second time before first?", secondTime.Before(firstTime))
    fmt.Println("Second time after first?", secondTime.After(firstTime))
}

شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج على النحو التالي:

The first time is 2021-08-15 14:30:45.0000001 +0000 UTC
The second time is 2021-12-25 16:40:55.0000002 +0000 UTC
First time before second? true
First time after second? false
Second time before first? false
Second time after first? true

بما أننا نستخدم في الشيفرة أعلاه نظام الوقت UTC، فيجب أن يكون الخرج متشابهًا بغض النظر عن المنطقة الزمنية. نلاحظ أنه عند استخدام تابع Before مع المتغير firstTime وتمرير الوقت secondTime الذي نريد المقارنة به، ستكون النتيجة true أي أن ‎2021-08-15 قبل ‎2021-12-25. عند استخدام After مع المتغير firstTime وتمرير secondTime، تكون النتيجة false لأن ‎2021-08-15 ليس بعد 2021-12-25. يؤدي تغيير ترتيب استدعاء التوابع على secondTime إلى إظهار نتائج معاكسة.

هناك طريقة أخرى لمقارنة القيم الزمنية في حزمة الوقت وهي تابع Sub، الذي يطرح تاريخًا من تاريخ آخر ويُعيد قيمةً من نوع جديد هو time.Duration. على عكس قيم النوع time.Time التي تمثل نقاط زمنية مطلقة، تمثل قيمة time.Duration فرقًا في الوقت. مثلًا، قد تعني عبارة "في ساعة واحدة" مدة duration لأنها تعني شيئًا مختلفًا بناءً على الوقت الحالي من اليوم، لكنها تمثّل "عند الظهر" وقتًا محددًا ومطلقًا. تستخدم لغة جو النوع time.Duration في بعض الحالات، مثل الوقت الذي نريد فيه تحديد المدة التي يجب أن تنتظرها الدالة قبل إعادة خطأ أو كما هو الحال هنا، إذ نحتاج إلى معرفة مقدار الزمن بين وقت وآخر.

لنفتح الآن ملف "main.go" ونستخدم التابع Sub على المتغير firstTime و secondTime ونطبع النتائج:

...

func main() {
    firstTime := time.Date(2021, 8, 15, 14, 30, 45, 100, time.UTC)
    fmt.Println("The first time is", firstTime)

    secondTime := time.Date(2021, 12, 25, 16, 40, 55, 200, time.UTC)
    fmt.Println("The second time is", secondTime)

    fmt.Println("Duration between first and second time is", firstTime.Sub(secondTime))
    fmt.Println("Duration between second and first time is", secondTime.Sub(firstTime))

شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج على النحو التالي:

The first time is 2021-08-15 14:30:45.0000001 +0000 UTC
The second time is 2021-12-25 16:40:55.0000002 +0000 UTC
Duration between first and second time is -3170h10m10.0000001s
Duration between second and first time is 3170h10m10.0000001s

يوضح الناتج أعلاه أن هناك 3170 ساعة و 10 دقائق و 10 ثوان و 100 نانو ثانية بين التاريخين، وهناك بعض الأشياء التي يجب ملاحظتها في الخرج، أولها أن المدة بين المرة الأولى first time والثانية second time قيمة سالبة، وهذا يشير إلى أن الوقت الثاني بعد الأول، وسيكون الأمر مماثلًا إذا طرحنا 5 من 0 وحصلنا على 5-. نلاحظ أيضًا أن أكبر وحدة قياس للمدة هي ساعة، لذلك فهي لا تُقسم إلى أيام أو شهور. بما أن عدد الأيام في الشهر غير متسق وقد يكون لكلمة "اليوم" معنى مختلف عند التبديل للتوقيت الصيفي، فإن قياس الساعة هو أدق مقياس، إذ أنه لا يتقلب.

عدّلنا البرنامج خلال هذا القسم مرتين للمقارنة بين الأوقات الزمنية باستخدام ثلاث توابع مختلفة؛ إذ استخدمنا أولًا التابعين Before و After لتحديد ما إذا كان الوقت قبل أو بعد وقت آخر؛ ثم استخدمنا Sub لمعرفة المدة بين وقتين.

ليس الحصول على المدة بين وقتين الطريقة الوحيدة التي تستخدم بها الحزمة الزمنية الدالة time.Duration، إذ يمكننا أيضًا استخدامها لإضافة وقت أو إزالته من قيمة من النوع time.Time.

إضافة وطرح الأوقات الزمنية

إحدى العمليات الشائعة عند كتابة التطبيقات التي تستخدم التواريخ والأوقات هي تحديد وقت الماضي أو المستقبل بناءً على وقت آخر مرجعي، فمثلًا يمكن استخدام هذا الوقت المرجعي لتحديد موعد تجديد الاشتراك بخدمة، أو ما إذا كان قد مر قدرٌ معينٌ من الوقت منذ آخر مرة جرى فيها التحقق من أمر معين. توفر حزمة الوقت time طريقةً لمعالجة الأمر، وذلك من خلال تحديد المدد الزمنية الخاصة بنا باستخدام متغيرات من النوع time.Duration.

إنشاء قيمة من النوع time.Duration بسيط جدًا والعمليات الرياضية عليه متاحة كما لو أنه متغير عددي عادي؛ فمثلًا لإنشاء مدة زمنية time.Duration تُمثّل ساعة أو ساعتين أو ثلاثة ..إلخ، يمكننا استخدام time.Hour مضروبةً بعدد الساعات التي نريدها:

oneHour := 1 * time.Hour
twoHours := 2 * time.Hour
tenHours := 10 * time.Hour

نستخدم time.Minute و time.Second في حال الدقائق والثواني:

tenMinutes := 10 * time.Minute
fiveSeconds := 5 * time.Second

يمكن أيضًا إضافة مدة زمنية إلى مدة أخرى للحصول على مجموعهما. لنفتح ملف main.go ونطبق ذلك:

...

func main() {
    toAdd := 1 * time.Hour
    fmt.Println("1:", toAdd)

    toAdd += 1 * time.Minute
    fmt.Println("2:", toAdd)

    toAdd += 1 * time.Second
    fmt.Println("3:", toAdd)
}

شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج على النحو التالي:

1: 1h0m0s
2: 1h1m0s
3: 1h1m1s

نلاحظ من الخرج أن المدة الأولى المطبوعة هي ساعة واحدة، وهذا يتوافق مع ‎1 * time.Hour في الشيفرة. أضفنا بعد ذلك ‎1 * time.Minute إلى القيمة toAdd أي ساعة ودقيقة واحدة. أخيرًا أضفنا ‎1 * time.Second إلى قيمة toAdd، لينتج لدينا ساعة واحدة ودقيقة واحدة وثانية في المدة الزمنية الممثلة بالنوع time.Duration.

يمكن أيضًا دمج عمليات إضافة المُدد معًا في عبارة واحدة، أو طرح مدة من أخرى:

oneHourOneMinute := 1 * time.Hour + 1 * time.Minute
tenMinutes := 1 * time.Hour - 50 * time.Minute

لنفتح ملف "main.go" ونعدله بحيث نستخدم توليفة من هذه العمليات لطرح دقيقة واحدة وثانية واحدة من toAdd:

...

func main() {

    ...

    toAdd += 1 * time.Second
    fmt.Println("3:", toAdd)

    toAdd -= 1*time.Minute + 1*time.Second
    fmt.Println("4:", toAdd)
}

شغّل البرنامج باستخدام go run:

$ go run main.go

سيعطي الخرج التالي:

1: 1h0m0s
2: 1h1m0s
3: 1h1m1s
4: 1h0m0s

يظهر السطر الرابع من الخرج، والذي يُمثّل ناتج طرح المجموع ‎1*time.Minute + 1*time.Second من toAdd أن العملية ناجحة، إذ حصلنا على قيمة ساعة واحدة (طُرحت الثانية والدقيقة).

يتيح لنا استخدام هذه المُدد المقترنة بالتابع Add للنوع time.Time حساب المدد الزمنية بين نقطتين زمنيتين إحداهما نقطة مرجعية، مثل حساب المدة منذ أول يوم اشتراك في خدمة معينة حتى اللحظة. لرؤية مثال آخر نفتح ملف "main.go" ونجعل قيمة toAdd تساوي 24 ساعة أي ‎24 * time.Hour، ثم نستخدم التابع Add على قيمة متغير من النوع time.Time لمعرفة الوقت الذي سيكون بعد 24 ساعة من تلك النقطة:

...

func main() {
    theTime := time.Date(2021, 8, 15, 14, 30, 45, 100, time.UTC)
    fmt.Println("The time is", theTime)

    toAdd := 24 * time.Hour
    fmt.Println("Adding", toAdd)

    newTime := theTime.Add(toAdd)
    fmt.Println("The new time is", newTime)
}

شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج على النحو التالي:

The time is 2021-08-15 14:30:45.0000001 +0000 UTC
Adding 24h0m0s
The new time is 2021-08-16 14:30:45.0000001 +0000 UTC

نلاحظ أن إضافة 24 ساعة إلى التاريخ ‎2021-08-15 سينتج عنه التاريخ الجديد ‎2021-08-16. يمكننا أيضًا استخدام التابع Add لطرح الوقت، إذ سنستخدم قيمة سالبة ببساطة، فيصبح الأمر كما لو أننا نستخدم التابع Sub. لنفتح ملف "main.go" ونطبق هذا الكلام، fحيث سنطرح هذه المرة 24 ساعة، أي يجب أن نستخدم قيمة "24-".

...

func main() {
    theTime := time.Date(2021, 8, 15, 14, 30, 45, 100, time.UTC)
    fmt.Println("The time is", theTime)

    toAdd := -24 * time.Hour
    fmt.Println("Adding", toAdd)

    newTime := theTime.Add(toAdd)
    fmt.Println("The new time is", newTime)
}

`

شغّل البرنامج باستخدام go run:

$ go run main.go

سيكون الخرج كما يلي:

The time is 2021-08-15 14:30:45.0000001 +0000 UTC
Adding -24h0m0s
The new time is 2021-08-14 14:30:45.0000001 +0000 UTC

نلاحظ من الخرج أنه قد طُرح 24 ساعة من الوقت الأصلي.

استخدمنا خلال هذا القسم التوابع time.Hour و time.Minute و time.Second لإنشاء قيم من النوع time.Duration. استخدمنا أيضًا قيم النوع time.Duration مع التابع Add للحصول على قيمة جديدة لمتغير من النوع time.Time. سيكون لدينا -من خلال التوابع time.Now و Add و Before و After- القدرة الكافية على التعامل مع التاريخ والوقت في التطبيقات.

الخاتمة

استخدمنا خلال هذا المقال الدالة time.Now للحصول على التاريخ والوقت المحلي الحالي على شكل قيم من النوع time.Time، ثم استخدمنا التوابع Year و Month و Hour ..إلخ، للحصول على معلومات محددة من التاريخ والوقت. استخدمنا بعد ذلك التابع Format لطباعة الوقت بالطريقة التي نريدها وفقًا لتنسيق مخصص نقدمه أو وفقًا لتنسيق جاهز مُعرّف مسبقًا. استخدمنا الدالة time.Parse لتحويل قيمة سلسلة نصية string تمثّل بيانات زمنية إلى قيمة من النوع time.Time لنتمكن من التعامل معها. تعلمنا أيضًا كيفية التبديل من المنطقة الزمنية المحلية إلى منطقة غرينتش UTC باستخدام التابع Local والعكس. تعلمنا استخدام توابع Sub و Add لإجراء عمليات جمع وطرح على البيانات الزمنية، لإيجاد الفرق بين مدتين زمنيتين أو لإضافة مدة زمنية إلى بيانات زمنية محددة.

ترجمة -وبتصرف- للمقال How To Use Dates and Times in Go لصاحبه Kristin Davidson.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...