จัดการ DateTime ใน Python ได้ง่าย ๆ ด้วย Pendulum

เวลาเราเขียน Python หนึ่งในเรื่องที่เราปวดหัวที่สุดแล้ว ก็คือการจัดการกับพวก Datetime ตั้งแต่เรื่องการ Parsing จนไปถึงการ Manipulate ต่าง ๆ และสุดท้ายการ Transform ออกมาให้อยู่ในรูปที่เราต้องการ โดยเฉพาะเมื่อเราทำงานกับ Dataset ขนาดใหญ่ ๆ ก็คือ ไม่สนุกเลย เช็คกันรัว ๆ แตกกันกระจายแน่นอน ทำให้วันนี้เรามี Library ตัวนึงที่จะทำให้เรื่องเหล่านี้ง่ายขึ้นนั่นคือ Pendulum

ติดตั้ง Pendulum

Pendulum ไม่ได้เป็น Built-in Module ใน Python ทำให้เราจะต้องทำการติดตั้งก่อนถึงจะใช้งานได้ ซึ่งแน่นอนว่าเดี๋ยวนี้เรามี Package Manager อย่าง pip และ conda อยู่แล้ว ทำให้การติดตั้งไม่ใช่เรื่องยากเลย

conda install pendulum

# or

pip install pendulum

เท่านี้เราก็ได้ Pendulum เข้ามาใช้งานเรียบร้อยแล้ว ส่วนใครที่ใช้พวก Apple Silicon อยู่ สามารถใช้งานได้ปกติเลยนะ ไม่ต้องใส่ท่ายากอะไร ไม่เหมือนกับบาง Library ที่บอกเลยว่า ปวดหัวมาก ๆ บางอันลงได้ ใช้แล้วพัง บางอันลงไม่ได้เลย ต้องเอา Source มา Compile เอง แล้วค่อยติดตั้งอีกที (พวกนั้นมันเขียนด้วย C ประกอบด้วย ทำให้บางทีเปลี่ยน Arch แล้วจะแตกได้ ต้อง Compile Source Code ส่วนนั้นใหม่เอง)

Parsing

เริ่มจากการ Parse DateTime เข้ามาก่อนละกัน โดยปกติ การ Parse เราก็อาจจะได้ข้อมูลเข้ามาเป็น String อาจจะมาจาก Database หรือ API ต่าง ๆ โดยปัญหาที่เรามักจะเจอคือ Format ที่ไม่เหมือนกันเลย บางที่อาจจะเอา เดือน วัน ปี หรืออีกที่ วัน เดือน ปี สารพัดมาก ๆ พีคกว่านั้นอีก คือ วัน กับ เดือน นี่แหละ โอเคแหละ ถ้า Row ไหน วันที่มันเลย 12 ไปแล้ว เช่น 13 และ 30 พวกนี้เราจัดการไม่ยากเลย เพราะทั้งปีมี 12 เดือน แล้วถ้าเป็นวันที่ 2 งี้ บางทีก็ งง ว่าสรุปมันเดือน 2 หรือวันที่ 2 กันแน่ ปัญหานี้เราเจอบ่อยมาก ๆ เพราะคนที่ให้ข้อมูล โยนไฟล์มาให้อย่างเดียวเลย ไม่มี Data Dictionary อะไรมาให้เลย (ทำไมร้อนจังคะ ฮ่า ๆ)

แต่ปัญหาที่เราว่ามา Pendulum ไม่ได้ช่วยอะไร ฮ่า ๆ ร้อนล้วน ๆ แต่ถ้าเราบอกว่า ข้อมูลของเราที่ได้มามันเป็นไปตาม Standard อยู่แล้ว เรื่องนี้ไม่ต้องกลัว โดยใน Pendulum มันจะจัดการให้ ถ้าเราจัด Format ตาม RFC3399 หรือ ISO8601 เราสามารถรันผ่านคำสั่ง parse() ได้เลย ตัวอย่างเช่น

import pendulum

sampled_datetime = pendulum.parse('2021-11-29T13:30:14.039185+07:00')

จากตัวอย่างด้านบน เราเลือกใช้เป็น RFC3399 เราก็รันได้ตรง ๆ เลย โดยที่เราไม่ต้องบอกอะไรมันเพิ่มเลย มันทำให้เราทำงานได้ง่ายมาก ๆ จริง ๆ แล้วในตัวของ Python เองมันก็มีคำสั่งสำหรับ Parse แบบนี้แหละ ง่ายไม่ต่างกัน แต่ที่เราว่า Pendulum ทำให้คุณภาพชีวิตในการทำงานตรงส่วนนี้ดีขึ้นคือ มันอ่านแล้วเข้าใจง่ายว่า อ่อ มันคือการ Parse นะอะไรแบบนั้น

pendulum.from_format('29-11-2021 22 21', 'DD-MM-YYYY HH mm')

หรือถ้าเราบอกว่า ข้อมูลของเรามันล่อมาไม่ตรงกับ Standard อะไรเลย เป็น Standard แบบเอาแต่ใจไปเรื่อยใจ เ ก เ ร จัด ๆ เราก็สามารถบอกลักษณะของ Format ให้ Pendulum จัดการได้เช่นกัน ผ่านคำสั่งที่ชื่อว่า from_format()

sampled_datetime.in_timezone('Europe/London')

บางครั้งโปรแกรมของเราทำงานกับหลากหลาย Timezone ด้วยกัน การแปลงให้มันอยู่ใน Timezone เดียวกัน ทำให้เราทำงานได้ง่ายขึ้น และเลี่ยงปัญหาว่าถ้าเกิดบางที เราลืม Output Timezone ออกมาด้วยตอนเรา Export Data เราจะได้ระบุมันง่าย ๆ Normalise ให้มันเป็น Timezone เดียวกันไปเลยดีกว่า โดยที่เราสามารถทำได้ผ่านคำสั่ง in_timezone() พร้อมกับใส่ Timezone ที่เราต้องการลงไปได้เลย

โดยปกติแล้ว เวลาเราทำงาน ถ้าเราทำงานกับเครื่อง Server เครื่องเดียวเลย และเป็นข้อมูลพวก Timestamp เราจะไม่มีปัญหาเลย เพราะ Datetime มันจะถูก Generate ออกจากเครื่องเดียวกันทั้งหมดแน่ ๆ ทำให้เรามั่นใจได้ว่า โอเค เราจะได้ Timezone เดียวกันทั้งหมดจริง ๆ แต่ถ้ามันไม่ใช่นั่นแหละ สภาพ..... เพื่อความปลอดภัยในเคสนั้นเราก็จะแปลงให้เป็น Timezone ที่ +0 เสมอ หรือก็คือเวลาที่ London

ปล. ก่อนที่เราจะแปลง Timezone ให้เราเช็คก่อนว่า ตัว Datetime Object มี Timezone เข้ามา และ ข้อมูลถูกต้อง ไม่งั้น Default มันจะให้มาเป็น +0 แล้วถ้าเราแปลงมันอาจจะมีปัญหาได้นะ โดนกันมาเยอะ ฮ่า ๆ

Getting Current & Relative DateTime

หรือถ้าส่วนที่เราทำงาน มันไม่ได้เป็นการดึงข้อมูลเข้ามา แต่เราอ้างอิงจากเวลาปัจจุบัน เราก็สามารถเรียกได้จากใน Pendulum เช่นกัน

pendulum.now()

โดยเราสามารถใช้คำสั่งชื่อว่า now() ในการเอาวัน และ เวลาที่ปัจจุบันเข้ามาได้เลย อันนี้เราว่ามันก็ไปเหมือนกับ datetime.now() ที่อยู่ใน Built-in Module ของ Python เองเลย แต่ ๆๆๆ สิ่งที่มันทำได้ดีกว่านั้นอยู่ที่นี่เลย

yesterday_in_bkk = pendulum.yesterday()
tomorrow_in_bkk = pendulum.tomorrow()
tomorrow_in_london = pendulum.tomorrow('Europe/London')

สิ่งที่มันเจ๋งคือ เราสามารถขอ DateTime แบบ Relative ได้ง่าย ๆ เลย เช่นเราบอกว่า เราอยากได้วันและเวลาของเมื่อวาน ถ้าเป็นตัว Python เอง เราต้องเสียเวลาในการเรียก now() ออกมา แล้วเราก็ต้องใช้ timedelta ในการหักวัน หรือเพิ่มออกไปอีกกี่วันก็ว่าไปถึงเราจะได้ออกมา แต่อันนี้คือ เขาเตรียมเป็นคำสั่งออกมาให้เราเลย ทำให้เราเรียกแล้วได้เลยจบในบรรทัดเดียวเลย ทำให้เราสะดวกขึ้นมาก หรือกระทั่งว่า เราสามารถกำหนด Timezone ลงไปได้อีกว่า เราจะเอาวันและเวลาของเมื่อวานของที่ไหนก็ได้เหมือนกัน

หลักการมันไม่มีอะไรเลยนะ แค่ว่า มันเอาวัน และ เวลา ของตอนที่เราเรียกมา แล้วหักไป 1 วัน กับ เปลี่ยน Timezone ก็คือ หัก ไปตามจำนวนชั่วโมงที่ต่างกันเท่านั้นเอง แต่มันสะดวกไง ถ้าเราต้องทำเองถามว่าได้มั้ย ก็ได้ ไม่ได้ยาก แต่จะเสียเวลาทำทำไม กับ Code เราก็จะดูอ่านยากไปหมดรกมาก ๆ

Manipulation

หลังจากเราได้ DateTime Object มาแล้ว ก็ถึงเวลาที่เราจะต้องทำงานกับมันแล้ว โดยปกติแล้ว เราก็จะทำอยู่ไม่กี่อย่างกับข้อมูลพวกนี้ ก็คือ การบวก, ลบ การเปรียบเทียบ

>>> dt = pendulum.now()
2021-11-29T14:00:02.868434+07:00

>>> dt.add(months=1)
2021-12-29T14:00:15.699304+07:00

>>> dt.subtract(months=1)
2021-10-29T14:00:15.699304+07:00

การบวกลบ วัน เวลา ใส่เข้าไป เราสามารถทำได้ง่าย ๆ เลย ผ่านคำสั่ง add() และ substract() สิ่งที่เราจะต้องบอกมันก็คือว่า เราต้องการที่จะเดินหน้า หรือถอยหลังไปนานเท่าไหร่ เช่นในตัวอย่าง เราบอกให้มัน เดินหน้า และ ถอยไป  1 เดือน เราก็ใส่ month=1 ได้เลย หรือถ้าเราบอกว่า เราอยากให้มันถอยไป 1 วัน กับอีก 30 วินาที เราก็สามารถใส่เป็น 2 Argument คือ days=1 และ seconds=30

ที่ตกม้าตายกันเยอะ เรา และเพื่อนเราก็งง กันนานมากว่าเอ๊ะ ทำไมเรียกแล้วมันไม่ได้หว่า ทำไมมันพ่น Error บอกว่า ไม่มี Argument สิ่งที่จะบอกว่าอย่าลืมคือ พวกชื่อ Argument พวกนี้มันเติม -s ด้วยนะ เช่น เดือนจะเป็น months (มี -s ต่อท้าย) ด้วย ไม่งั้นเรียกแล้วแตกนะ

>>> dt_1 = pendulum.now().add(minutes=40)
2021-11-29T14:40:15.699304+07:00

dt.diff(dt_1)
<Period [2021-11-29T14:00:15.699304+07:00 -> 2021-11-29T14:40:15.699304+07:00]>

หรือถ้าเราบอกว่า เราอยากรู้ความต่างของเวลาทั้ง 2 ตัว เราอยากรู้ว่ามันห่างกันเท่าไหร่ เราก็สามารถหาได้ผ่านคำสั่งที่ชื่อว่า diff() โดยที่มันจะคืนกลับมาเป็น Period Object ที่เราจะเห็นว่า มันไม่ได้บอกอะไรเรามากนอกจากการที่เอา DateTime 2 อันมาวางอยู่ด้วยกัน

>>> dt.diff(dt_1).in_minutes()
40

ถ้าเราอยากรู้ว่า มันห่างกันเท่าไหร่ เราก็จะต้องเรียกมันออกมา โดยที่เราสามารถเรียกออกมาได้หมดเลยว่า มันห่างกัน กี่นาที กี่ชั่วโมง กี่วัน กี่เดือน กี่ปี ไปได้หมดอยู่ที่เราต้องการเลย จากตัวอย่าง มันก็จะได้ออกมา 40 นาที นั่นคือ dt_1 ห่างจาก dt ทั้งหมด 40 นาที ซึ่งถูกต้องเพราะ dt_1 มันเกิดจาก dt ที่บวกเข้าไป 40 นาทีนั่นเอง จะเห็นได้เลยว่า มันทำให้เราทำงานง่ายมาก ๆ ก่อนหน้านี้เวลาเราทำพวกนี้ เราจะแปลงมันให้กลายเป็น UNIX Timestamp ก่อนแล้วเอามาบวกลบอะไรก็ว่ากันไป แล้วค่อยแปลงกลับไปเป็นวันที่ เพราะเราไม่รู้ว่าบวก วัน และ เวลาง่าย ๆ ได้ยังไง

>>> period = df - df_1
>>> period.in_minutes()
40

นอกจากนั้น มันยังมี Shorthand ให้เราใช้แทน Diff ได้ด้วย โดยการเอา DateTime มาลบกันได้เลย โดยมันจะคืนกลับมาเป็น Period Object เหมือนกับตอนเราเรียกผ่าน diff() เลย ทำให้เราสามารถเรียกพวกคำสั่ง in_minutes() ต่าง ๆ ได้เหมือนกับตอนที่เราใช้ diff()

>>> dt.start_of('day')
2021-11-29T00:00:00+07:00

>>> dt.end_of('day')
2021-11-29T23:59:59.999999+07:00

มันจะมีเคสที่เราต้องการว่า เราอยาก Query ข้อมูลของวันนั้น ๆ เลย คือ 24 ชั่วโมงของวันที่เราต้องการโดยตรง ทำให้เราจะต้องเลือก Range ของเวลาที่ 0 นาฬิกา จนไปถึง 23 นาฬิกา 59 นาที 59 วินาที ของวันนั้น ๆ เลย โดยปกติแล้ว เราจะใช้วิธีการเอาวันเวลาปัจจุบันมา แล้วเอาแค่วัน เดือน ปี มาอย่างเดียว เอาเวลาออก แล้วย้อนกลับไปเป็น DateTime ใหม่ เราก็จะได้ จุดเริ่ม แล้วค่อย Delta เข้าไป 23 ชั่วโมง 59 นาที 59 วินาที เพื่อให้ได้สุดสิ้นสุด ดูปวดหัวอยู่เนอะ แต่ด้วย Pendulum ทำได้ง่ายกว่านั้นเยอะมาก

เราสามารถใช้คำสั่ง start_of และ end_of ได้เลย โดยสิ่งที่เราต้องบอกมันคือ เราจะหาจุดเริ่ม หรือจุดจบ ของอะไร เช่น วัน เราก็ใส่เป็น day ได้เลย หรือกระทั่ง ศตวรรษ เราก็ใส่ century ได้นะ จากตัวอย่างด้านบน เราอยากได้จุดเริ่ม และ จุดจบของวัน เราก็ใช้ start_of('day') ของวันที่เราต้องการ และ end_of('day') เพื่อหาจุดสิ้นสุดของวันที่เราต้องการได้ตรง ๆ เลย ง่ายมาก ๆ

dt.next(pendulum.MONDAY)
dt.previous(pendulum.WEDNESDAY, keep_time=True)

อีกอันที่เราเจอบ่อยมาก ๆ แล้วก็ปวดหัวมาก ๆ คือ วันจันทร์หน้า วันที่เท่าไหร่อะ เออหว่ะ ฮ่า ๆ เราต้องไปนั่งเทียบอีกว่า โอเควันนี้วันอะไร แล้วค่อยเอามาบวกเข้าไปกับของเดิมเพื่อให้เราได้วันจันทร์หน้าออกมา ปวดหัวอีก แน่นอนว่า พูดมาขนาดนี้ Pendulum ช่วยเราได้แน่นอน ผ่านคำสั่งชื่อว่า next() สิ่งที่มันรับเข้าไปคือ วันที่เราต้องการคือ Constant ของวันที่เขาเตรียมมาให้เราแล้ว จากตัวอย่างด้านบน เราบอกว่า เราขอวันจันทร์หน้าให้หน่อย ก็ทำง่าย ๆ ในบรรทัดเดียวเลย

อีกตัวอย่างในบรรทัดต่อมาคือเราใช้คำสั่ง previous หรือก็คือก่อนหน้า มันก็จะตรงข้ามกับ next() เลย ทำให้จากตัวอย่างเป็นการขอ วันพุธที่แล้วนั่นเอง แต่สังเกตที่ Argument ที่เราใส่เข้าไป มันจะมี keep_time หมายความว่า ให้ Pendulum เอาเวลาใส่เอาไว้ด้วย อย่าเอาทิ้งนะ เพราะโดยปกติแล้ว ถ้าเราไม่ได้บอกมัน มันจะเอาจุดเริ่มของวันมาให้เราเลยนั่นก็คือเที่ยงคืน เวลามันเลยหายไปนั่นเอง

>>> dt > dt_1
False

>>> dt < dt_1
True

>>> dt == dt_1
False

สุดท้าย คือเรื่องของการเปรียบเทียบแล้ว เราสามารถทำได้ง่าย ๆ เลยผ่านการใช้เครื่องหมายสำหรับการเปรียบเทียบทั้งหลายเช่น เท่ากับ มากกว่า และ น้อยกว่า ในการเปรียบเทียบได้เลยเหมือนกับเราทำงานกับตัวเลข โดยที่น้อยกว่ามันจะหมายถึงว่า มันมาก่อน และกลับกัน มากกว่าคือ มาทีหลังนั่นเอง นอกจากนั้น มันยังเตรียมพวก Function แปลก ๆ ที่เราน่าจะได้ใช้ด้วยเช่น การเช็ควันเกิด ก็คือ มันเช็คแค่วันกับเดือน แล้วไม่สนปี อะไรแบบนั้นก็มีเหมือนกัน ทำให้เราทำงานได้ง่ายขึ้นมาก

String Formatting

สุดท้าย ท้ายสุด เมื่อเราจัดการกับ DateTime เรียบร้อยแล้ว เราจะทำการเอามันออกไป อาจจะเอาไปเก็บลงในฐานข้อมูลต่าง ๆ หรืออาจจะไปแสดงผลให้กับผู้ใช้ ไม่ว่าแบบไหน มันก็จะมีวิธีการแสดงผลที่แตกต่างกันออกไป ทำให้เราจะต้องมีการทำ String Formatting เพื่อระบุรูปแบบของ วันเวลา ให้เป็นไปตามที่เราต้องการ

>>> dt.format('dddd Do MMMM YYYY HH:mm:ss')
'Monday 29th November 2021 14:00:15'

>>> dt.strftime('%A %-d %B %Y %I:%M:%S %p')
'Monday 29 November 2021 02:00:15 PM'

เริ่มจากแบบที่ปกติกันก่อน เราน่าจะคุ้นเคยกับการ Format DateTime แบบด้านบนแน่ ๆ จริง ๆ ก็คือทำได้เหมือนกันเลย แต่ ๆๆๆ Pendulum ซะอย่าง ทำไมมันต้องมานั่งจำ Format ละ

>>> dt.to_datetime_string()
'2021-11-29 14:00:15'

>>> dt.to_formatted_date_string()
'Nov 29, 2021'

>>> dt.to_iso8601_string()
'2021-11-29T14:00:15.699304+07:00'

เพื่อความง่าย Pendulum เขาเตรียม Format ต่าง ๆ เป็น Function สำเร็จรูปให้เรากดได้เลย ทำให้เราไม่ต้องมานั่งแคะว่า เอ๊ะ มันคืออะไรบ้างนะอะไรแบบนั้น อย่าง ISO8601 เป็นตัวที่เราใช้บ่อยมาก ๆ แต่เอาเข้าจริงเราก็จะไม่ได้แหละว่ามัน Format อะไร เราก็ต้องมานั่งเปิดดูทุกครั้งเสียเวลามาก เราก็เรียก Function สำเร็จนี้ได้เลย ก็ทำให้งานเร็วขึ้นมาก กับ Code ยังอ่านได้ง่ายขึ้นเยอะ จากเดิมที่เป็น Format เขียนมายาว ๆ ไม่รู้นะว่ามันคืออะไรถ้าอ่านผ่าน ๆ แต่อันนี้อ่านผ่าน ๆ ก็รู้แล้วว่าอ่อ ISO8601 ซึ่งเราก็รู้คร่าว ๆ อยู่แล้วว่ามันหน้าตายังไง

สรุป

Pendulum เป็น Library บน Python ตัวนึงที่ทำให้เราสามารถจัดการกับ DateTime ที่มักจะเป็น Data Type อันน่าปวดหัวได้ง่ายขึ้นมาก ๆ ตั้งแต่การเอาเข้า ไปถึงการจัดการกับมัน และท้ายสุดก็คือการ Format ออกมาเพื่อนำไปใช้งานต่อ เท่าที่เราใช้มา เราบอกเลยว่ามันล่นเวลาการทำงานเราได้เยอะมาก ๆ จนเอ๊ะ แค่เนี๊ยอะนะ มันล่นเวลาเราได้ขนาดนั้นเลยเหรอ กับยังไม่นับเรื่องของการ Maintain Code ที่ง่ายขึ้นมาก เพราะมันอ่านง่ายกว่าเดิมเยอะ แนะนำเลย ลองไปเล่นดู