นากการ์ตูนใส่ฮู้ดสีเขียวจัดกองเหรียญและธนบัตรเปล่าให้เป็นคอลัมน์ตรงกันบนกระดานบัญชี มีตัวคั่นรูปจุลภาคและไม้บรรทัดจัดแนวขอบขวา

จัดรูปแบบตัวเลข Python ให้รายงานเงินบาทอ่านง่าย พร้อมเทสต์

น้องกายEngineering

รายงานยอดขายที่มีทั้ง 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 ที่เลือกใช้โดยตรง
  • ตรวจยอดติดลบ ศูนย์ ยอดเกินช่อง และรายงานว่าง
  • ตรวจภาษาไทยในโปรแกรมปลายทางก่อนส่งรายงาน