คำถามเหมือนจะง่าย แต่เราจะใช้การ คูณ + modulo ร่วมกับสมบัติ coprime และ birthday paradox กันครับ!

มีเงื่อนไขคือ

  1. เลขตัวเดียวกันต้องออกมาเป็น string เดียวกัน
  2. เลขใกล้เคียงกันต้องดูไม่ออกว่าคล้ายกับอันที่แล้ว
  3. one-way operation ก็ได้ แต่ถ้าย้อนได้จะดีมาก
  4. เป็นฟังก์ชั่น one-to-many ก็ได้ จริงๆก็ไม่อยากให้ซ้ำกันมาก
  5. string มีความยาวและชุดตัวอักษรที่อยากใช้ที่กำหนดเองได้
  6. ต้องเกิดมาได้ด้วยตัวเอง ไม่มี central authority หรือก็คือ ไม่สามารถเรียงได้เพราะไม่รู้ว่าใครใช้อะไรไปแล้วบ้าง

ผลที่ตามมาก็คือ

  • ถ้าไม่มีว่าจากเลข 1 ตัวก็คง random ทีละตัวอักษรไปแล้ว
  • ถ้าไม่มีข้อ 2 สามารถทำได้ง่ายๆโดยการสุ่มเลขขึ้นมาแล้วแปลงเป็น base-n เมื่อ n คือจำนวนตัวอักษรที่ต่างกัน สมมติฐาน 16 ก็มี 0123456789ABCDEF ให้ใช้ 16 ตัวอะไรงี้ (แล้วค่อยเอาไป map เป็นตัวอื่นตามชอบ)

เป้าหมายหลักๆคือระบบ User ID ในเกมเป็นแบบ generate offline ไม่มีเซิฟเวอร์รันให้ ถ้าจะป้องกันไม่ให้ซ้ำกันเราควรจะใช้ GUID (UUID) ในการ generate

Universally unique identifier - Wikipedia
Thus, anyone can create a UUID and use it to identify something with near certainty that the identifier does not…en.wikipedia.org

GUID นี่คือเลขที่เขาบอกว่าเสกขึ้นมาแล้วโอกาสซ้ำไม่น่าจะมีง่ายๆ แถมเสกกันรัวๆยังไงก็น่าจะโลกแตกก่อนเลขจะหมด (ใครฝันอยากจะมีอะไรเป็นของตัวเองหนึ่งเดียวในโลกซักวัน วันนี้ฝันเป็นจริงแล้วครับไปลองสร้าง GUID ของตัวเองขึ้นมาได้เดี๋ยวนี้เลยครับ!)

แต่ว่าหน้าตาของ GUID มันอ่านยาก (จริงๆเป็นเลขตัวเดียวแต่ขนาด 128-bit) เขียนยาก ถ้าจะให้ผู้เล่น add ID กันกว่าจะบอกกันหมดตายพอดี

644e1dd7–2a7f-18fb-b8ed-ed78c3f92c2b

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

ได้ไปตั้งกระทู้ใน StackOverflow จนได้ไอเดียคร่าวๆ เลยจะมาสรุปที่นี่ครับ

Any algorithm that use a number as a feed for generating random string?
Ok, thanks for the guidance @Jim Mischel. I read all the related pages and come to understand more about this. http:…stackoverflow.com

ทำไงให้เลขมั่ว

อย่างว่าถ้า algorithm มีแค่แปลงเป็นเลขฐาน มันดูเปลี่ยนไปก็จริงแต่มันดูไม่ค่อยมั่ว

12332958 -> BC2F9E

12332858 -> BC2F3A

เห็นมั้ยครับว่ามันคล้ายๆกัน (ก็แน่สิ!) ถ้าเป็นไปได้อยากได้อะไรที่แบบ

1000 -> ASDFAWEOJVWMPAWEFW

1001 -> FAWFJVMMCOOAWIEORO

ก่อนอื่นอย่าเพิ่งคิด string มาคิดจากเลขเป็นเลขก่อน แปลงเป็น string ทีหลังใช้แปลงเลขฐานก็ได้

Multiply Modulo

จากที่เซิจมาวิธีนี้น่าสนใจ คือคูณกับเลขนึงแล้ว mod อีกเลขนึง ถ้าคูณให้มันทะลุๆไปแล้ว mod เลขน่าจะมั่วพอควร

กำหนดให้ n เป็น input ส่วน x เป็นตัวคูณ และ m เป็นตัว modulo

ถ้าเราทำแบบนี้ (n*x)%m เลขก็น่าจะมั่วขึ้น เช่น x = 100, m=36

n = 5 : (5*100)%36 = 32

n = 6 : (6*100)%36 = 24

ได้ output ที่ไม่ค่อยจะเดาออกแล้ว

Coprime

แต่จะมั่นใจได้ไง สมมติว่าถ้า x = 4, m = 16 นี่ ใส่ input เข้าไป ผลจะมีแค่ 0,4,8,12 เท่านั้น นอกจากจะดูออกแล้ว range ของ function ยังมีแค่หยิบมือเองแม้ว่า domain จะเป็นเลขอะไรก็ได้ก็ตาม กลายเป็น function แปลงเลขที่แย่ทันที

แต่เรามาใช้สมบัติที่ว่าถ้าเลือก x กับ m เป็น coprime ซึ่งกัน คือมี หรม. (GCD) เป็น 1 (ก็คือประมาณว่าm หารด้วย x แล้วไม่ลงตัว หารด้วยตัวประกอบไหนๆของ x ก็ไม่ลงตัว ยกเว้น 1)

นอกจากนี้! ถ้าเลือก x และ m เป็น coprime กันแล้วยังมีสมบัติน่าสนใจที่ว่า input แต่ละตัวจะให้ผลไม่ซ้ำกันเลยตราบใดที่ยังน้อยกว่า m ทำให้เราแน่ใจได้อีกว่าไม่มี input 2 ตัวที่ให้ผลซ้ำกัน (จริงๆไม่มีสมบัตินี้ก็โอเค แต่มีแล้วยิ่งสบายใจ) หรือพูดได้อีกอย่างคือถ้าเลขน้อยกว่า m จะเป็นฟังก์ชั่นแบบ one-to-one นั่นเองครับ

ตัวอย่าง

(n * 5) % 16

5 กับ 16 เป็น coprime กัน มาดู output เมื่อใส่ input เป็น 0 ถึง 15

Input : 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15
Output : 0, 5, 10, 15, 4, 9, 14, 3, 8, 13, 2, 7, 12, 1, 6, 11

จะเห็นว่าเลขเดาไม่ค่อยออก (แต่ใคร modulo เก่งๆอาจจะเดาออก) ที่สำคัญคือเลขไม่ซ้ำกันครับ! (เช่น ลองหาดูซิว่ามี 11 กี่ตัว มี 9 กี่ตัว) แน่นอนว่า input เป็น 16 จะได้เลขซ้ำคือ 0 และตัวต่อๆไปจะเริ่มวนตาม sequence เดิม

ดังนั้นถ้าเราเปลี่ยนจากสุ่มเลขเป็นเรียงเลข 1,2,3,4,… แทน user แต่ละคน แล้วเอามาเข้าวิธีแบบนี้ก็จะได้เลขมั่วๆที่ไม่ชนกัน (เสียดายที่ผมทำไม่ได้เพราะไม่มีคนกลางเรียงเลขให้ ว่าจะใช้สุ่ม GUID มาแล้วตัดทอนให้ต่ำกว่า mเอา)

จะเลือก x และ m อย่างไร

เนื่องจาก m เป็นตัว modulo แสดงว่าค่าจะไปได้ไม่เกินนี้ แล้วก็ x ควรเป็นเลขที่เยอะใช้ได้ เพราะวิธีการทำให้เลขทะลุโลกของวิธีนี้คือการคูณให้ทะลุ mod ดังนั้นเราน่าจะเริ่มจากค่า m ก่อน

แน่นอนว่าค่าไม่เกิน m สิ่งที่ต้องคิดต่อไปคือ รหัสของเราจะรองรับได้ต่างๆกันทั้งหมดกี่รหัส

Birthday Paradox

เราจะเอา m เท่าจำนวนผู้เล่นทั้งหมดที่คาดไว้ก็ได้ แต่แบบนั้นอาจจะน้อยไปเพราะมีอีกปัญหาที่ว่า เราไม่ได้มีตัวกลาง run number แต่ละคนสุ่มเลขขึ้นมา ที่ต้องกังวลไม่ใช่ว่าจะมีผู้เล่นมากสุดกี่คน เราต้องกังวลว่าจะมี 2 คนในโลกนี้สุ่มชนกัน % สูงๆ ที่เลขเท่าไหร่ต่างหาก

Birthday problem - Wikipedia
The history of the problem is obscure. W. W. Rouse Ball indicated (without citation) that it was first discussed by…en.wikipedia.org

ปัญหานี้บอกว่ามีวันอยู่ 366 วัน ละมีคน n คนอยู่ในห้องเนี่ยมีโอกาสกี่ % ที่สองคนจะเกิดวันเดียวกันนั่งอยู่ในห้องนี้ ฟังดูเหมือนว่าเกิดยาก แต่จริงๆแค่ในห้องมี 70 คนก็โอกาส 99.9% แล้ว (แน่นอนถ้ามี 367 คนโอกาสจะเป็น 100% จาก pigeonhole principle)

ได้เจอกับปัญหานี้ครั้งแรกตอนพบกับ อ. ชัยพร ที่ ม.เกษตร อ. พูดอยู่หน้าห้องอย่างหล่อเลยว่า “ในห้องนี้… มีคนเกิดวันเดียวกัน” แล้วพอไล่จนเจอจริงๆตอนนั้นรู้สึกแบบโหหหหหหหห อ. เป็นนักมายากลรึเปล่าเนี่ย 555555

ถ้าเราเปลี่ยน n เป็นผู้เล่นเกมเรา แล้วเปลี่ยน 366 วันเป็น m คือรหัสทั้งหมดที่มีได้ เราก็จะสามารถคิดได้ครับว่า m เท่าไหร่ดี จาก n ที่เรากำหนดเอง (คิดว่าเกมตัวเองจะดังแค่ไหนล่ะ) แล้วก็จาก % ชนกันที่เริ่มรับไม่ได้ ที่เราก็จะกำหนดเองเป็น 50%

จะเห็นว่าใน wiki มีสูตรประมาณอยู่ เดี๋ยวเรามาใช้สูตรนี้ดีกว่า

หน้าตาของรหัส

อันนี้อาจจะต่างกันออกไปตามแต่ละคน แต่ผมว่าจะให้หน้าตาเป็นประมาณ

RG7-VKS-P3K-94T-QWC-... 

ขีดไม่เกี่ยว คือเป็นอักษรใหญ่ A-Z แล้วก็เลข 0–9 ที่เราต้องเอามาคิดคือ ที่รหัส length เท่านี้ จะรองรับได้ต่างกันทั้งหมดกี่แบบ ซึ่งใช้วิธีคิดแบบเหมือนตอนหาว่าเลขฐาน 2 length นี้มีต่างๆกันกี่แบบก็ได้ ก็คือสูตร 2^n เมื่อ n เป็น length ทีนี้เราก็เปลี่ยน 2 เป็นจำนวนแบบของตัวอักษรในรหัสเรา

จริงๆสูตรนี้มันไม่ได้วิเศษอะไรเลยแต่เป็น combinatorics เฉยๆ เช่น2^3 ก็คือ 2*2*2 หรือพูดอีกอย่างว่า “แต่ละรูมีได้ 2 แบบ ถ้า 3 รูก็ต้องมีได้ 2*2*2=8 แบบไม่ซ้ำกัน” (000,001,010,011,100,101,110,111)

เรามีอักษรทั้งหมด 32 แบบ (พอดีตัด 0,O,1,I ออกไปกลัวเห็นแล้วงง แล้วจริงๆบาง font น่าตัด B กับ 8 ออกไปด้วยเพิ่งนึกออก) ดังนั้น 32^n คือจำนวนผู้เล่นทั้งหมดที่รองรับ แต่แน่นอน m จะน้อยกว่านั้นมากเพราะถ้าใช้เท่านั้นเลยกลัวสุ่มไปซักพักก็เริ่มชนกันแล้ว

จำนวนผู้เล่นกับความยาวรหัส

เมื่อรู้สูตรเกี่ยวโยงระหว่างความยาวกับจำนวนผู้เล่นแล้ว เรามาดูดีกว่าว่ายาวแค่ไหนรับได้กี่คนแบบ ถ้ามากกว่านี้อีกนิดโอกาสสุ่มมาซ้ำกันจะเป็น 50%

อันนี้เขียนโปรแกรม python ไล่ออกมาให้ดูโดยใช้สูตรประมาณของ Birthday Paradox

เนื่องจากคิดว่าจะมีผู้เล่นมาเล่นเกม Mel Cadence เกิน 6 ล้านคน (นี่! มั่นใจมาจากไหน) ก็เลยคิดว่าความยาว 9 กำลังดี รหัสสวยด้วย (เช่น 5XT-339-A67 )

เพื่อความเข้าใจ แน่นอนว่าจริงๆแล้วรหัส 9 ตัวนี้รองรับผู้เล่นได้ทั้งหมด 32⁹ = 35,184,372,088,832 คนเลยครับ (ถ้ามาเล่นกันขนาดนั้นก็ดีสิ) แต่ว่าถ้าเกิน 6,983,947 คนเมื่อไหร่โอกาสสุ่มชนกันก็จะเยอะขึ้นเป็น 50% แล้วครับ แต่ผมมั่นใจว่าถึงชนกันจริงคนที่โดนชนน่าจะเบื่อเลิกเล่นไปก่อนแล้ว

เลือก x และ m ได้แล้ว

ในที่สุดเราก็จะได้ว่าควรจะเลือก mเป็น 35,184,372,088,832–1 นั่นเองครับ แล้วก็ xที่แบบเว่อร์ๆหน่อยแล้วก็ หรม. กับเลขตะกี้ได้ 1 ด้วย พอดีว่าเลขเมื่อกี้ลงท้ายด้วย 1 เป็นเลขที่คิดว่าน่าจะหา x ไม่ยาก พิมพ์มั่วๆแล้วให้ลงท้ายด้วย 7 หรือ 9 น่าจะโอกาสได้สูง

ยังไงก็ไปลองใช้ Wolfram Alpha เช็คได้นะ พิมพ์ GCD(x,m) แบบนี้แล้วมันรู้เรื่อง

Wolfram|Alpha: Computational Knowledge Engine
Wolfram|Alpha is more than a search engine. It gives you access to the world's facts and data and calculates answers…www.wolframalpha.com

จากธรรมชาติของ modulo บางทีสูตรเราจะมี bias ด้วยนะครับ เช่น (n * 35084372034839) % 35184372088831 เลือกมาเนี่ย ถ้าลองใส่เลขง่ายๆเช่น 1,2,3,… ดู

n=1 : 35,084,372,034,839

n=2 : 34,984,371,980,847

n=3 : 34,884,371,926,855

n=4 : 34,784,371,872,863

จะเห็นว่าผลไม่ค่อยน่าพอใจ เช่นเลขสองตัวหน้าดูคล้ายๆกัน เลขตรงกลางก็มีคล้ายๆกัน แปลงเป็นฐาน 32 แล้วตัวอักษรแรกๆต้องคล้ายกันแน่ๆ ที่เป็นงี้เพราะเลือก x ใกล้กับ m เกินไป ทำให้เวลาวน mod อีกรอบแล้วลงแถวๆเดิม (เลขต่างกันตามทฤษฎีจริง แต่หน้าตามันดันคล้ายกัน) input จริงๆคงไม่ได้เรียงสวยๆงี้เท่าไหร่เพราะสุ่มมา แต่ถ้าใครแคร์ก็พยายามหา x ใหม่ดูครับ (อย่าลืมเรื่อง coprime)

ถ้า input ห่างกันเป็น 100 ก็น่าจะโอเคอยู่

n=334 : 1,784,354,055,503

n=434 : 26,968,720,745,134

สร้าง input

มั่นใจได้แล้วว่า (n * 35084372034839) % 35184372088831 น่าจะได้เลขมั่วดี (ไม่ต้องมาแฮคเกมผมนะในเกมไม่ได้ใช้เลขนี้แน่ๆ) แถมไม่ซ้ำด้วย ตราบเท่าที่ input n ยังไม่เกิน 35184372088831 ทีนี้เรามาสร้าง n ที่ว่ากันดีกว่า

แน่นอนว่าใช้ GUID เลยไม่ได้เพราะ 128-bit มันใหญ่กว่านี้มาก จะเอา GUID มาหั่นเลยก็ไม่ดีเพราะบางส่วนของ GUID มันไม่ random กลัวจะแย่ลงอีก จะสุ่ม int ขึ้นมาโดยไม่พึ่ง GUID ก็เป็นอีกทางเลือกที่อาจจะดีกว่าเอา GUID มาต้มยำด้วยซ้ำ แต่ส่วนตัวอยากเก็บ GUID ไว้เช็คขั้นสุดท้าย แล้วก็อยากให้พิสูจน์ได้เสมอว่า GUID แปลงมาเป็นชื่ออ่านง่ายได้ (ไม่ต้องแปลงกลับได้ก็ได้)

ก็เลยคิดว่าวิธีคิดเลขขึ้นมาจาก GUID ที่น่าสนใจคือ GetHashFunction ใน C# ซึ่งก็ไม่รู้ว่า Microsoft เขาใส่วิธีคิดมายังไง แต่น่าจะเชื่อใจได้ (ระวัง! hash อาจจะเปลี่ยนได้ในเวอร์ชั่น .NET ต่อไป ฯลฯ)

ออกมาจากฟังก์ชั่นที่ว่าเป็น int ซึ่งค่า max คือ 2,147,483,647 แล้วก็อย่าลืมว่า hash บางทีมันไปทางลบได้ด้วย แต่ mod ของเราดำเนินการกับค่าบวก ดังนั้นถ้าเปลี่ยนเป็น uint ก็จะได้ 4,294,967,295 น้อยกว่า m 35,184,372,088,831 พอสมควรดังนั้นน่าจะใช้ได้

ระวังเรื่อง cross platform!

สมมติจะไปพิสูจน์แปลง GUID เป็นชื่ออ่านง่ายที่อื่นด้วย ที่ต้องระวังคือ GetHashFunction คงไม่ได้ implement เหมือน C# สมมติถ้าจะไปทำใน Javascript บนเว็บ ถ้าเซฟ hash เก็บไว้ด้วยก็ต้องระวังว่าเลขแบบ 35,184,372,088,831 เนี่ยเอามาใช้ที่ภาษาอื่นเช่น Javascript ที่ไม่มี type แล้วมันจะเป็นอะไรรึเปล่า (ยังไมได้ลอง)

ทดลอง generate รัวๆ

น่าจะ ok ครับ!

อย่าลืมว่าโค้ดมันมาจากเลขฐาน ถ้าผลการคำนวณออกมาเป็นเลขน้อยๆเมื่อไหร่ ผลอาจจะกลายเป็นงี้ได้ครับ 222-22D-EA5 เพราะว่าเราเอา 0 กับ 1 ออกไปแล้ว เลข 2 เลยกลายเป็นตัวแทนของ 0 ครับ ถ้าโค้ดออกมาแบบนั้นเมื่อไหร่อาจจะความแตกได้ แต่โอกาสคิดว่าไม่มากเท่าไหร่ครับ

คำหยาบ

บางคนเลือกที่จะเอา A E I O U หรือหนึ่งในนี้ออกไปด้วยเพราะกลัวว่าจะสุ่มออกมาเป็นคำหยาบ หรือใครไม่อยากเอาออกลองเอา substring ความยาว 3 มาเช็คกับ list นี้ดูก็ได้ครับ

// Thanks to : https://github.com/klhurley/ElementalEngine2/blob/master/Common/Databases/BadWords.dbx
 private readonly string[] filters = {“fuc”,”fuk”,”fuq”,”fux”,”fck”,”coc”,”cok”,”coq”,”kox”,”koc”,”kok”,”koq”,”cac”,”cak”,”caq”,”kac”,”kak”,”kaq”,”dic”,”dik”,”diq”,”dix”,”dck”,”pns”,”psy”,”fag”,”fgt”,”ngr”,”nig”,”cnt”,”knt”,”sht”,”dsh”,”twt”,”bch”,”cum”,”clt”,”kum”,”klt”,”suc”,”suk”,”suq”,”sck”,”lic”,”lik”,”liq”,”lck”,”jiz”,”jzz”,”gay”,”gey”,”gei”,”gai”,”vag”,”vgn”,”sjv”,”fap”,”prn”,”lol”,”jew”,”joo”,”gvr”,”pus”,”pis”,”pss”,”snm”,”tit”,”fku”,”fcu”,”fqu”,”hor”,”slt”,”jap”,”wop”,”kik”,”kyk”,”kyc”,”kyq”,”dyk”,”dyq”,”dyc”,”kkk”,”jyz”,”prk”,”prc”,”prq”,”mic”,”mik”,”miq”,”myc”,”myk”,”myq”,”guc”,”guk”,”guq”,”giz”,”gzz”,”sex”,”sxx”,”sxi”,”sxe”,”sxy”,”xxx”,”wac”,”wak”,”waq”,”wck”,”pot”,”thc”,”vaj”,”vjn”,”nut”,”std”,”lsd”,”poo”,”azn”,”pcp”,”dmn”,”orl”,”anl”,”ans”,”muf”,”mff”,”phk”,”phc”,”phq”,”xtc”,”tok”,”toc”,”toq”,”mlf”,”rac”,”rak”,”raq”,”rck”,”sac”,”sak”,”saq”,”pms”,”nad”,”ndz”,”nds”,”wtf”,”sol”,”sob”,”fob”,”sfu”};

ย้อนกลับได้ด้วย Multiplicative Inverse

จาก Eric Lippert พระเจ้า C# ที่เป็นต้นเรื่องที่ไปอ่านวิธีนี้มา ใน (n*x)%m ที่ทำไป ถ้า x และ m เป็น coprime กันเมื่อไหร่เขาบอกว่าสามารถ derive ได้ว่ามีตัวเลขวิเศษอีกตัวนึงแน่ๆที่น้อยกว่า m ชื่อ y ที่สามารถใช้ย้อนกลับการแปลงได้ครับ

Math from scratch, part thirteen: multiplicative inverses
Here's an interesting question: what is the behavior of this infinite sequence? WOLOG let's make the simplifying…ericlippert.com

แต่พอดีอ่านแล้วยังไม่เข้าใจเลยไม่เอามาลง แล้วก็ไม่ได้อยากเอามาใช้ด้วยเพราะแปลงย้อนกลับไปก็ได้แค่ hash ไปไม่ถึง GUID อยู่ดี (ข้อมูลหายไปแล้วเพราะ hashing เป็น one-way) ถ้าใครชอบ math ลองเข้าไปอ่านดูก็ได้ครับผมยังงงๆอยู่เลย 555

Hashids

Hashids
Generate short unique ids from integers. Use in url shortening or as unique ids.hashids.org

แนะนำให้ลองไปดูนี่ด้วยครับ แนวคิดคล้ายๆกันตรงที่แปลงเลขเป็น base-n เมื่อ n คือแบบอักษรที่ต้องการ เรื่องความมั่ว แทนที่จะใช้สมบัติวนของ modulo อันนี้ใช้ salt ในการสลับตัวอักษรเรื่อยๆตลอด algorithm ครับ แล้วก็ใช้ salt เดิมย้อนกลับได้ ป้องกันคำหยาบได้ในตัว แต่มีพิสูจน์แล้วว่าไม่ secure คือสามารถเขียนโปรแกรมโจมตีหา salt คืนมาได้เร็วกว่า brute force ดังนั้นถ้าแคร์เรื่องความยากของการย้อนกลับวิธีที่เขียนที่นี่อาจจะเดายากกว่าว่า x กับ m เป็นเท่าไหร่ แต่ทางนั้นมี library สำเร็จรูปให้ใช้ด้วยนะ

ความยาวอ่านลวกๆไม่แน่ใจ แต่เหมือนจะออกมายาวเท่าๆตัวเลขที่ใส่ไป ถ้าอยาก fixed ให้ยาวกว่านั้นสามารถใส่เลขเป็น array ได้ (ใช้ array ก็ทำให้กลายเป็นตัวฝังข้อมูลได้) หาข้อมูลมาเติมจนกว่าจะได้ความยาวที่ต้องการ หรือจะเอาเลขมาเพิ่มความยาวตามวิธีของ blog นี้แล้วค่อยไปเข้า hashids ก็ได้