Tutorial

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

By Arnon Puitrakul - 30 พฤศจิกายน 2021

จัดการ 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 ที่ง่ายขึ้นมาก เพราะมันอ่านง่ายกว่าเดิมเยอะ แนะนำเลย ลองไปเล่นดู

Read Next...

จัดการข้อมูลบน Pandas ยังไงให้เร็ว 1000x ด้วย Vectorisation

จัดการข้อมูลบน Pandas ยังไงให้เร็ว 1000x ด้วย Vectorisation

เวลาเราทำงานกับข้อมูลอย่าง Pandas DataFrame หนึ่งในงานที่เราเขียนลงไปให้มันทำคือ การ Apply Function เข้าไป ถ้าข้อมูลมีขนาดเล็ก มันไม่มีปัญหาเท่าไหร่ แต่ถ้าข้อมูลของเราใหญ่ มันอีกเรื่องเลย ถ้าเราจะเขียนให้เร็วที่สุด เราจะทำได้โดยวิธีใดบ้าง วันนี้เรามาดูกัน...

ปั่นความเร็ว Python Script เกือบ 700 เท่าด้วย JIT บน Numba

ปั่นความเร็ว Python Script เกือบ 700 เท่าด้วย JIT บน Numba

Python เป็นภาษาที่เราใช้งานกันเยอะมาก ๆ เพราะความยืดหยุ่นของมัน แต่ปัญหาของมันก็เกิดจากข้อดีของมันนี่แหละ ทำให้เมื่อเราต้องการ Performance แต่ถ้าเราจะบอกว่า เราสามารถทำได้ดีทั้งคู่เลยละ จะเป็นยังไง เราขอแนะนำ Numba ที่ใช้งาน JIT บอกเลยว่า เร็วขึ้นแบบ 700 เท่าตอนที่ทดลองกันเลย...

Humanise the Number in Python with "Humanize"

Humanise the Number in Python with "Humanize"

หลายวันก่อน เราทำงานแล้วเราต้องการทำงานกับตัวเลขเพื่อให้มันอ่านได้ง่ายขึ้น จะมานั่งเขียนเองก็เสียเวลา เลยไปนั่งหา Library มาใช้ จนไปเจอ Humanize วันนี้เลยจะเอามาเล่าให้อ่านกันว่า มันทำอะไรได้ แล้วมันล่นเวลาการทำงานของเราได้ยังไง...

ทำไม 0.3 + 0.6 ถึงได้ 0.8999999 กับปัญหา Floating Point Approximation

ทำไม 0.3 + 0.6 ถึงได้ 0.8999999 กับปัญหา Floating Point Approximation

การทำงานกับตัวเลขทศนิยมบนคอมพิวเตอร์มันมีความลับซ่อนอยู่ เราอาจจะเคยเจอเคสที่ เอา 0.3 + 0.6 แล้วมันได้ 0.899 ซ้ำไปเรื่อย ๆ ไม่ได้ 0.9 เพราะคอมพิวเตอร์ไม่ได้มองระบบทศนิยมเหมือนกับคนนั่นเอง บางตัวมันไม่สามารถเก็บได้ เลยจำเป็นจะต้องประมาณเอา เราเลยเรียกว่า Floating Point Approximation...