
จัดรูปแบบตัวเลข Python ให้รายงานเงินบาทอ่านง่าย พร้อมเทสต์
รายงานยอดขายที่มีทั้ง 12500.5, 900 และยอดติดลบ อ่านเทียบกันยากเมื่อแต่ละแถวแสดงทศนิยมไม่เท่ากัน ยิ่งต้องส่งข้อความสรุปให้ฝ่ายบัญชีทุกวัน การต่อช่องว่างและใส่คอมมาเองก็กลายเป็นงานซ่อมรายงานประจำครับ เราจะให้ Python จัดหน้าตัวเลขด้วยกติกาเดียวกัน แล้วใช้เทสต์ล็อกผลลัพธ์ก่อนส่งออก
WHY: ตัวเลขอ่านง่ายช่วยลดงานตรวจซ้ำ
- หลักพันและทศนิยมสองตำแหน่งช่วยให้คนอ่านแยก
12,500.50ออกจาก1,250.05ได้เร็วขึ้น - จัดยอดชิดขวาเพื่อไล่ดูหลักหน่วยและสตางค์ในรายงานข้อความที่ใช้ฟอนต์ความกว้างคงที่
- รวมรูปแบบไว้จุดเดียว เมื่อเปลี่ยนความกว้างคอลัมน์ก็ไม่ต้องแก้ช่องว่างทีละแถว
- ทดสอบยอดติดลบ ยอดศูนย์ และยอดใหญ่ได้ โดยไม่ต้องเปิดไฟล์แล้วกวาดตาตรวจทุกครั้ง
เครื่องมือหลักคือ format specification mini-language หรือภาษาขนาดเล็กสำหรับบอกวิธีแสดงค่า อยู่หลัง : ใน f-string เช่น f"{amount:>12,.2f}" เอกสารระบุว่ารูปแบบนี้ใช้กำหนดความกว้าง การจัดแนว การเติมช่องว่าง และความละเอียดของตัวเลขได้ อ่านรูปแบบมาตรฐานของ Python
ทำงานยังไง
มองว่าค่าเงินเป็นสินค้า ส่วน format spec เป็นป้ายราคาหน้าชั้นวางครับ เปลี่ยนป้ายให้เขียน 12,500.50 ไม่ได้แปลว่าเปลี่ยนจำนวนเงินในระบบ การจัดรูปแบบให้สตริงสำหรับแสดงผล จึงควรอยู่ปลายทางหลังคำนวณเสร็จ
อ่าน >12,.2f จากซ้ายไปขวา: > จัดชิดขวา, 12 คือความกว้างขั้นต่ำของทั้งช่อง, , แบ่งหลักพัน และ .2f แสดงแบบทศนิยมคงที่สองตำแหน่ง ความกว้างรวมเครื่องหมายลบ จุด และตัวคั่นด้วย ถ้ายอดยาวเกินช่อง Python จะไม่ตัดยอดให้พอดี ต้องเผื่อความกว้างเอง รายละเอียด width และ alignment
ตัวเลือกอื่นที่ใช้บ่อยคือ + สำหรับแสดงเครื่องหมายทั้งบวกและลบ, _.2f สำหรับแบ่งกลุ่มด้วยขีดล่าง และ .1% สำหรับแสดงสัดส่วนเป็นเปอร์เซ็นต์ เช่น 0.075 เป็น 7.5% ส่วน +010.2f เติมศูนย์ระหว่างเครื่องหมายกับตัวเลขเมื่อไม่ได้ระบุแนวจัดไว้ชัดเจน เหมาะกับช่องที่ตั้งใจใช้ศูนย์นำหน้า แต่รายงานยอดเงินทั่วไปใช้ช่องว่างอ่านสบายกว่า
format(amount, spec) เหมาะเมื่อส่งกติกามาจากตัวแปร ส่วน f-string เหมาะเมื่อต้องประกอบข้อความกับค่าในบรรทัดเดียว ทั้งคู่ใช้ format spec ได้ เราสามารถซ้อนช่องแทนค่าภายใน spec เช่น f"{amount:>{width},.{places}f}" เพื่อเลือกความกว้างและจำนวนทศนิยมตอนรันได้ด้วย เอกสารอนุญาตการซ้อนระดับนี้ แต่ไม่ให้ซ้อนลึกต่อไปอีก nested replacement fields
การระบุ , และ f ในตัวอย่างเป็นการเลือกรูปแบบเอง ไม่ได้ขอให้ใช้การตั้งค่าภูมิภาคหรือ locale ของเครื่อง อย่าเหมารวมทุกชนิดการจัดรูปแบบว่าไม่เกี่ยวกับ locale: เอกสารแยกชนิด n ไว้สำหรับรูปแบบตัวเลขตาม locale บทความนี้ไม่เรียกโมดูล locale และไม่ตั้งค่าภูมิภาค จึงไม่ได้สาธิตการทำรายงานหลายประเทศอัตโนมัติ
โค้ดตัวอย่างพร้อมเทสต์
บันทึกบล็อกนี้เป็น report.py แล้วรัน python3 -I -B report.py ใช้เฉพาะ standard library หรือไลบรารีที่มากับ Python ตัวอย่างเป็นยอดสุทธิรายสาขาสมมติวันที่ 24 ตุลาคม 2026 โดยสาขาที่ติดลบมีรายการคืนเงินมากกว่ายอดขาย
ผมใช้ Decimal สร้างยอดจากข้อความ และทดสอบ spec ที่ใช้กับเงินโดยตรง ไม่ถือว่าทุกตัวเลือกของ float จะใช้กับ Decimal ได้ทั้งหมด คอลัมน์ชื่อสาขาอยู่ท้ายแถวเพื่อลดปัญหาการจัดช่องภาษาไทย ส่วนคอลัมน์รหัสและจำนวนเงินใช้ตัวอักษรที่จัดแนวในฟอนต์ความกว้างคงที่ได้ง่าย
from decimal import Decimal, ROUND_HALF_UP
import unicodedata
import unittest
ROWS = [
("BKK", "บางกะปิ", Decimal("12500.50")),
("CNX", "เชียงใหม่", Decimal("900.00")),
("KKC", "ขอนแก่น", Decimal("-42.50")),
]
def baht(value, width=12):
places = 2
return f"{value:>{width},.{places}f}"
def render_summary(rows):
# รับยอดที่ผ่านกติกาคำนวณเงินของระบบแล้ว
lines = [f"{'CODE':<4} {'BAHT':>12} BRANCH"]
total = Decimal("0.00")
for code, branch, amount in rows:
lines.append(f"{code:<4} {baht(amount)} {branch}")
total += amount
lines.append(f"{'ALL':<4} {baht(total)} รวม")
return "\n".join(lines)
class ReportTests(unittest.TestCase):
def test_baht_and_nested_spec(self):
amount = Decimal("12500.50")
self.assertEqual(baht(amount), " 12,500.50")
self.assertEqual(baht(amount), format(amount, ">12,.2f"))
self.assertEqual(baht(Decimal("0")), " 0.00")
self.assertEqual(baht(Decimal("-42.50")), " -42.50")
self.assertEqual(baht(amount, width=4), "12,500.50")
def test_report_contract(self):
expected = "\n".join([
"CODE BAHT BRANCH",
"BKK 12,500.50 บางกะปิ",
"CNX 900.00 เชียงใหม่",
"KKC -42.50 ขอนแก่น",
"ALL 13,358.00 รวม",
])
self.assertEqual(render_summary(ROWS), expected)
self.assertEqual(
render_summary([]),
"CODE BAHT BRANCH\nALL 0.00 รวม",
)
def test_other_presentations(self):
self.assertEqual(format(1234567.5, "_.2f"), "1_234_567.50")
self.assertEqual(format(Decimal("42.5"), "+.2f"), "+42.50")
self.assertEqual(format(Decimal("-42.5"), "+010.2f"), "-000042.50")
self.assertEqual(format(Decimal("0.075"), ".1%"), "7.5%")
def test_display_is_not_a_money_policy(self):
value = 2.675
self.assertEqual(format(value, ".2f"), "2.67")
self.assertEqual(value, 2.675)
money = Decimal("2.675")
rounded = money.quantize(Decimal("0.01"), rounding=ROUND_HALF_UP)
self.assertEqual(rounded, Decimal("2.68"))
self.assertEqual(money, Decimal("2.675"))
def test_comma_cannot_be_used_with_n(self):
with self.assertRaises(ValueError):
format(1234, ",n")
def test_thai_marks_and_field_length(self):
label = "ก\u0e49" # ก ตามด้วยไม้โท
self.assertTrue(unicodedata.combining(label[1]))
self.assertEqual(len(label), 2)
self.assertEqual(format(label, ">4"), " " + label)
self.assertEqual(len(format(label, ">4")), 4)
if __name__ == "__main__": unittest.main()
ผมรันโค้ดนี้ด้วย Python 3.14.7 แล้ว unittest ทั้ง 6 เทสต์ผ่านครับ
ฟังก์ชันคืนข้อความแทนการพิมพ์เอง ทำให้เทสต์ตรวจรายงานทั้งฉบับได้ และผู้เรียกเลือกส่งลงไฟล์หรือพิมพ์ต่อได้ ยอดรวมในตัวอย่างคือ 13,358.00 บาท ส่วนเทสต์แถวว่างกำหนดให้ยังมีหัวตารางและยอดรวมศูนย์ พฤติกรรมเล็ก ๆ แบบนี้ควรตกลงก่อนเชื่อมกับงานส่งรายงานอัตโนมัติ
จุดที่คนพลาดบ่อย
- คิดว่า
.2fเปลี่ยนค่าที่เก็บไว้: มันกำหนดหน้าตาที่แสดง ตัวอย่าง2.675แบบ float แสดงเป็น2.67แต่ตัวแปรเดิมยังเท่าเดิม แยกกติกาปัดเงินจริงออกจากการแสดงผล เทสต์สาธิตDecimal.quantizeพร้อมROUND_HALF_UPแบบระบุชัดเจน ซึ่งเป็นเพียงกติกาตัวอย่าง ต้องเลือกให้ตรงระบบบัญชีของงาน - คิดว่า Decimal แก้กติกาปัดเศษให้เสร็จ: สร้างจากข้อความอย่าง
Decimal("2.675")ตามตัวอย่าง และตัดสินใจว่าจะปัดเมื่อใด ก่อนรวมยอดหรือหลังรวมยอด ฟังก์ชันรายงานนี้รับยอดที่เตรียมแล้ว ไม่ได้บังคับนโยบายดังกล่าวให้ทุกระบบ - ใส่
,คู่กับn: เทสต์ยืนยันว่าformat(1234, ",n")เกิดValueErrorเลือก,กับชนิดที่รองรับอย่างfเมื่อต้องการรูปแบบตามตัวอย่าง หรือศึกษาชนิดnและเงื่อนไข locale แยกก่อนใช้ - เผื่อช่องเฉพาะตัวเลขบวก: เครื่องหมายลบและคอมมากินพื้นที่ใน width ด้วย ทดสอบยอดคืนเงินและยอดใหญ่เสมอ ความกว้างเป็นขั้นต่ำ ไม่ใช่เพดาน ถ้าตัวเลขยาวเกิน ช่องถัดไปจะเลื่อนตาม
- ใช้ความยาวสตริงแทนความกว้างที่ตาเห็น: เทสต์ยืนยันว่า
กตามด้วยไม้โทนับได้สองอักขระ และ>4เติมสองช่องว่าง แต่ไม่ได้พิสูจน์ว่ามันกินพื้นที่สี่ช่องบนหน้าจอทุกแบบ เครื่องหมายภาษาไทยอาจวางซ้อนกับพยัญชนะ การเรนเดอร์ขึ้นกับฟอนต์และโปรแกรมปลายทาง แยกชื่อไทยไว้ท้ายแถว หรือใช้ตาราง HTML แล้วตรวจภาพจริง
ใช้ตรงไหนในงานจริง
- สรุปยอดขายหรือยอดคืนเงินรายสาขาในไฟล์ข้อความที่ทีมเปิดด้วยฟอนต์ความกว้างคงที่
- ทำข้อความสรุปเงินบาทส่งให้ผู้ดูแลงาน โดยใช้รูปแบบทศนิยมเดียวกันทุกแถว
- แสดงผลต่างยอดด้วย
+เพื่อให้เห็นทิศทางของการเปลี่ยนแปลงชัดขึ้น - แสดงอัตราส่วนด้วย
%โดยตกลงก่อนว่าค่าต้นทางเป็นสัดส่วน เช่น0.075ไม่ใช่เลข7.5
เก็บค่าตัวเลขไว้สำหรับคำนวณต่อ แล้วจัดรูปแบบตอนสร้างรายงาน หากไฟล์ปลายทางต้องใช้ตัวเลขจริง เช่น ข้อมูลนำเข้าระบบอื่น ควรแยกฟิลด์ค่าดิบออกจากข้อความที่มีคอมมาและคำว่า “บาท” เพื่อให้ผู้รับตีความได้ตรงกัน
เทียบกับทางเลือกอื่น
| approach | ดีตรงไหน | ระวังอะไร |
|---|---|---|
| f-string พร้อม spec | อ่านข้อความกับรูปแบบได้ในจุดเดียว | อย่าให้กติกาซ้ำหลายจุดจนแก้ไม่ครบ |
format(value, spec) | รับ spec จากตัวแปรได้ตรงไปตรงมา | ค่าแต่ละชนิดอาจรองรับ spec ต่างกัน |
| ต่อสตริงและช่องว่างเอง | ใช้กับข้อความสั้นที่ไม่ซับซ้อนได้ | ต้องดูแลลบ หลักพัน และความกว้างเอง |
ชนิด n ตาม locale | เหมาะเมื่อโจทย์ต้องอิงภูมิภาค | ต้องตรวจ locale ที่ใช้งาน และไม่ผสม ,n |
ตัวอย่างนี้ใช้รูปแบบพื้นฐานสำหรับงาน Python 3.10+ และตรวจการทำงานจริงบนเวอร์ชันที่ระบุไว้ ไม่ได้ใช้ตัวเลือกใหม่อย่าง z ซึ่งเอกสารระบุว่าเพิ่มใน Python 3.11 ถ้าปรับ spec เพิ่ม ควรตรวจเวอร์ชันขั้นต่ำและเทสต์ชนิดข้อมูลที่ระบบใช้ด้วย เอกสารตัวเลือกและเวอร์ชัน
Recap
- ใช้
,.2fเมื่ออยากได้หลักพันและทศนิยมสองตำแหน่ง - อ่าน
>12ว่าชิดขวาในช่องกว้างขั้นต่ำ 12 อักขระ - ใช้ nested spec เมื่อความกว้างหรือทศนิยมมาจากตัวแปร
- แยกการจัดหน้าจากนโยบายปัดเศษเงินจริง
- ทดสอบ Decimal กับ spec ที่เลือกใช้โดยตรง
- ตรวจยอดติดลบ ศูนย์ ยอดเกินช่อง และรายงานว่าง
- ตรวจภาษาไทยในโปรแกรมปลายทางก่อนส่งรายงาน