top of page
ค้นหา

"$now" JavaScript that knows date-words

  • รูปภาพนักเขียน: Sathit Jittanupat
    Sathit Jittanupat
  • 9 ก.ย. 2566
  • ยาว 2 นาที

ree

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


ตอนที่ออกแบบหน้าจอแสดงประวัติข้อมูล มีตัวเลือกช่วงเวลาให้ผู้ใช้เลือก เช่น "วันนี้", "เมื่อวานนี้", "7 วันที่แล้ว - 2 วันที่แล้ว" ซึ่งคำเหล่านั้นสามารถแปลงให้กลายเป็น คำสั่งช่วงวันที่ ใช้ค้นข้อมูลจากดาต้าเบสมาแสดง


ree

ระหว่างคำว่า "7 - 2 วันที่แล้ว" กับ "2 - 7 วันที่แล้ว" คำไหนเราเข้าใจง่ายกว่ากัน สำหรับผมถ้าไม่หลอกตัวเองก็ต้องบอกว่า "2 - 7 วันที่แล้ว"


ดูเหมือนว่าสมองมนุษย์สามารถรับรู้ โดยไม่ต้องเคร่งครัดลำดับก่อนหลังของเริ่มต้นและสิ้นสุด


ree

แต่กฏเกณฑ์ช่วงวันที่ของโปรแกรม ประกอบด้วยวันที่สองค่า เริ่มต้น และ สิ้นสุด มีความตรงไปตรงมาเพื่อง่ายในการประมวลผล เริ่มต้นต้องมาก่อนสิ้นสุดเสมอ ถ้าจะเขียนเงื่อนไขค้นหาข้อมูลก็จะได้ว่า หาข้อมูลที่มี "วันที่" มากกว่าหรือเท่ากับ "เริ่มต้น" และ "วันที่" น้อยกว่าหรือเท่ากับ "สิ้นสุด"

MongoDB query

{ “info.date”: { $gte: เริ่มต้น, $lte: สิ้นสุด } }

สมมติถ้าให้โปรแกรมถอดความแบบตรงไปตรงมา

"7 วันที่แล้ว" => "01/03"
"2 วันที่แล้ว" => "05/03"

"7 - 2 วันที่แล้ว" = "01/03 - 05/03" 
"2 - 7 วันที่แล้ว" = "05/03 - 01/03"

สรุปว่าภาษามนุษย์มีความยืดหยุ่นการระบุช่วงเวลา จะเอาเวลาที่น้อยกว่า (เก่า) หรือ มากกว่า (ใหม่) ขึ้นลงท้ายก็ได้ ไม่สามารถตีความว่า อยู่ข้างหน้าคือ เริ่มต้น และอยู่ข้างหลังคือ สิ้นสุด


ทีแรกผมก็คิดว่าไม่ยากอะไร ก็เพิ่มโค้ดตรวจสอบ ถ้าพบว่าเริ่มต้นมากกว่าสิ้นสุด ก็แค่สลับข้างกัน


แต่ใช้ไม่ได้ถ้าเป็น เดือน - เดือน หรือ ปี - ปี ..


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

"มกราคม" => "01/01 - 31/01"
"4 - 2 เดือนที่แล้ว" => "มกราคม - มีนาคม" => "01/01 - 31/03"

โปรแกรมจะต้องเลือกเอาขอบด้านเริ่มต้นหน่วยเวลาด้านหนึ่ง และขอบด้านสิ้นสุดของอีกด้านหนึ่ง


สมมติถ้าให้โปรแกรมถอดความแบบตรงไปตรงมา

"2 - 4 เดือนที่แล้ว" => "มีนาคม - มกราคม" => "01/03 - 31/01" => "31/01 - 01/03"

ถึงแม้ว่าโปรแกรมจะตรวจสอบและพยายามสลับวันที่เริ่มต้น-สิ้นสุด ก็ยังไม่ถูกต้องเพราะขอบเวลาผิดตั้งแต่แรก


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

"2 - 4 เดือนที่แล้ว" = "มีนาคม - มกราคม" <==> "มกราคม - มีนาคม" = "01/01 - 31/03"

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

"สัปดาห์นี้ - เดือนนี้" = "ต้นสัปดาห์นี้ - ปลายเดือนนี้"
"เดือนนี้ - สัปดาห์นี้" = "ต้นเดือนนี้ - ปลายสัปดาห์นี้"


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


เวอร์ชั่นหนึ่ง


เป้าหมายแรกทีเดียว เพื่อเปลี่ยน User Interface หรือวิธีการป้อนข้อมูลวันที่ โดยเปลี่ยนไปใช้ input ง่าย ๆ รับข้อความ แล้วใช้โค้ดนี้แปลข้อความนี้กลายเป็นค่าวันที่ที่ถูกต้อง


โปรแกรมทั่วไปมีความพยายามออกแบบการป้อนวันที่หลายแนวทาง เช่น แบ่งเป็น 3 ช่อง วัน, เดือน, ปี แล้วพยายามใช้ตัวช่วยเป็น drop down วันที่ 1 - 31, หรือ ชื่อเดือน บางที่ก็ใช้ DatePicker เปิดปฏิทินขึ้นมาให้จิ้มเลือก


ree

ข้อเสนอของผมคือเปิดโอกาสให้ผู้ใช้ป้อนวันที่ตามสไตล์ที่ตนเองถนัด (free input) แล้วโปรแกรมฉลาดพอที่จะเข้าใจข้อความนั้น ตัวอย่างเช่น

"7" => วันที่ 7 เดือนนี้ (อ้างอิงกับเวลาปัจจุบัน)
"7/8" => วันที่ 7 เดือน 8 ปีนี้ (อ้างอิงกับเวลาปัจจุบัน)
"7/8/66" 
"7/8/23"
"7/8/2566"
"7/8/2023"
"2023-08-07"
"Mon Aug 07 2023" 
คำข้างต้นทั้งหมดแปลได้เหมือนกัน เป็นวันที่ 7 เดือน 8 ปี 2566 (2023) 
"8/66"
แปลได้เป็นช่วงวันที่ของเดือน 8 ปี 2566 (2023)

ขึ้นอยู่กับโปรแกรมว่าจะกำหนดใช้วันที่ต้นเดือน หรือปลายเดือนไปใช้ เช่น หากเป็นวันที่เอกสารอาจใช้ 01/08/2022 หากเป็นเงื่อนไขวันที่ของรายงานจะใช้ช่วงวันที่ 01/08/2022 - 31/08/2022


เมื่อมีความยืดหยุ่นในการแปลงค่าวันที่ ทำให้คิดถึงการใช้ข้อความที่เป็นคำเฉพาะอื่น ๆ ที่ใช้บ่อย เช่น "วันนี้" "today" "เมื่อวานนี้" "yesterday" "พรุ่งนี้" "tomorrow" ชื่อวันของสัปดาห์ และชื่อเดือน แบบย่อและเต็ม ภาษาไทยและภาษาอังกฤษ


นำไปสู่การออกแบบให้ผู้ใช้สามารถระบุเงื่อนไขรายงาน ด้วยภาษาวันที่ที่เป็นอิสระ เช่น รายงานยอดขาย ผู้ใช้สามารถเลือกเงื่อนไขช่วงวันที่ ด้วยคำว่า "เดือนนี้", "เดือนที่แล้ว", "30 วันล่าสุด" ฯลฯ​​ เราสามารถแปลงคำเหล่านี้เป็นช่วงวันที่



รอบปีบัญชี (fiscal year)


เพิ่มความสามารถเข้าใจ "ปีบัญชี" ที่แตกต่างจากปีปฏิทิน ใช้กับรายงานทางบัญชีสำหรับกิจการไม่ได้เริ่มรอบบัญชีที่ต้นปีปฏิทิน

"1/3 fiscal" => รอบบัญชีนี้​ (12 เดือน) เริ่มต้นจากวันที่ 1 เดือน 3 ของปีนี้
"1/3 last fiscal"  => รอบบัญชีที่แล้ว (12 เดือน) เริ่มต้นจากวันที่ 1 เดือน 3 ของปีที่แล้ว

ree

คำนวณกำหนดชำระ


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

"due 30" => กำหนด (นับวันที่ต่อไปอีก)​ 30 วัน
"due 30+" => กำหนด (นับวันที่ต่อไปอีก) 30 วัน ปัดไปสิ้นเดือน
"due 12 เดือน" => กำหนด (นับวันที่ต่อไปอีก) 12 เดือน ใช้กับเงื่อนไขอายุสัญญา

ree

ช่วงวันที่แบบยืดหยุ่น


เป็นโค้ดล่าสุด ปรับปรุงการแปลข้อความช่วงวันที่ตามที่เล่าไว้ตอนต้น ใช้ในการออกแบบตัวเลือกเงื่อนไขช่วงวันที่ เพื่อแสดงข้อมูลตามช่วงเวลานั้น โดยใช้ภาษาที่เข้าใจง่าย


ree


โค้ดอยู่ใน gist (github)


สำหรับนักพัฒนาผู้สนใจหรือต้องการพัฒนาต่อ ผมวางโค้ดเวอร์ชั่นล่าสุดไว้ใน gist ได้แรงบันดาลใจหรือต้นแบบมาจาก Moment.js ที่เป็นโค้ดเกี่ยวกับการคำนวณ และแสดงผลเป็นข้อความวันที่ภาษาต่างๆ ต้องออกตัวว่าเป็นโค้ดเก่า ตั้งแต่ยังไม่มี ES6 จึงไม่ได้ใช้ modern JavaScript ยกเว้นการปรับปรุงช่วงหลังอาจมีปนมาบ้าง


โค้ดนี้ช่วยจัดการด้าน input เกี่ยวกับวันที่ที่ Moment.js ไม่ได้ทำ โดยเฉพาะโค้ด translator ภาษาไทย ที่มีแต่นักพัฒนาไทยเข้าใจ


ข้อดีของ now.js ไม่มี dependency ใด ๆ เป็นไฟล์เดี่ยว สามารถเพิ่มเป็น tag script อยู่ใน html แล้วเรียกใช้ $now(..)ได้เลย

<script src="now.js"></script>

ไฟล์ทดสอบ test-now-exp.html สามารถเอาไปวางไว้ในโฟลเดอร์เดียวกับ now.js สามารถเปิดทดสอบจาก file:// ในคอมพิวเตอร์ด้วย browser ได้ทันที โดยไม่จำเป็นต้องมี http server


ree

ตัวอย่างการเรียกใช้


ใช้ executer (ไม่ใช้ translator)

$now( "$today()" )
$now( "$this(month)" )
$now( "$last(2, year)" )

ใช้ translator "en" (อังกฤษ) หรือ "th" (ไทย+อังกฤษ)

$now( "today", "en" )
$now( "this month", "th" )
$now( "2 ปีที่แล้ว", "th")

ค่าที่ได้จาก $now คือ Period object ซึ่งสามารถ chain method ต่อ เพื่อเอาไปใช้ตามต้องการ


$now( ).toString( )

คืนค่ากลับมาเป็นข้อความ "YYYY-MM-DD - YYYY-MM-DD"


$now( ).begin( )

$now( ).end( )

คืนค่ากลับมาเป็น At object ซึ่งสามารถ chain method ต่อได้ดังนี้


$now( ).begin( ).toDate( )

คืนค่ากลับมาเป็น Javascript Date object


$now( ).begin( ).toString( )

คืนค่ากลับมาเป็น ISO Date "YYYY-MM-DD"


$now( ).begin( ).valueOf( )

คืนค่ากลับมาเป็น Javascript Date value



ข้อความที่ยังรอพัฒนา


ยังมีบางข้อความที่ติดค้างเพราะความซับซ้อน คิดไม่ออกว่าจะออกแบบให้อธิบายด้วย chain method หรือ executer ได้อย่างไร ขึ้นอยู่กับวาระโอกาสที่เหมาะสม เมื่อใดที่มีความจำเป็นต้องนำไปประยุกต์ใช้ ก็อาจหาทางปรับปรุงในอนาคต

"เดือนนี้ของปีที่แล้ว" => ?
"ไตรมาศที่แล้วเมื่อ 2 ปีก่อน" => ?


อ้างอิง



jsat66@gmail.com (2023–09–09)







 
 
 

ความคิดเห็น


Post: Blog2_Post
  • Facebook

©2020 by Scraft On Cloud. Proudly created with Wix.com

bottom of page