Bỏ qua tới nội dung
Kiên

Tiền không phải là float, và float mới là nửa dễ

· 4 phút đọc

3 chỗ làm tròn · 1 luật

Lưu giá thành số nguyên theo đơn vị nhỏ nhất thì làm một buổi là xong. Cái tốn kém là quyết định làm tròn xảy ra ở đâu và bao nhiêu lần — markup áp từng phòng rồi cộng lại không bằng markup áp lên tổng rồi làm tròn một lần, và khách có thể đọc được một cái tổng không khớp với mấy dòng ngay phía trên.

  • tiền
  • pricing
  • correctness
  • backend
Nội dung
  1. Nửa mà ai cũng biết rồi
  2. Markup theo từng phòng, hay markup trên tổng
  3. Luật làm tròn: chọn một lần, và viết ra
  4. Cái hiển thị và cái thu tiền
  5. Quy đổi là chỗ thứ ba, và nó có timestamp
  6. Con số mình không có
  7. Luật

Nửa mà ai cũng biết rồi

Giá không phải là float. Lưu số tiền dưới dạng số nguyên theo đơn vị nhỏ nhất của loại tiền đó, để mã tiền tệ nằm ngay cạnh, và đừng bao giờ để một phân số nhị phân bén mảng tới con số mà khách sẽ đọc. Nói ra thì chẳng ai cãi. Gọi đó là nửa dễ: một quyết định về schema, một cái helper, vài tuần đi comment review. Trang này không phản đối gì chỗ đó, và cũng không nói về chỗ đó. Nửa đắt tiền là: làm tròn không phải một bước. Nó là một tập các chỗ, và câu hỏi quyết định giá của bạn có đúng hay không là một số tiền đi qua bao nhiêu chỗ trong đó trước khi lên tới màn hình.

Markup theo từng phòng, hay markup trên tổng

Một ngã ba rất cụ thể, lấy từ offer khách sạn. Bên mình có hai pattern supplier đã viết thành tài liệu: một là supplier trả về đúng một token đặt N phòng và báo một giá cho cả cụm. Hai là giá mỗi phòng độc lập với nhau, và cái offer mình hiển thị là một tổ hợp được dựng ra — về mặt cấu trúc, giá của nó là tổng của các mức giá phòng độc lập.

Markup của một tenant có hai chỗ đặt đều hợp lý. Áp vào giá từng phòng, làm tròn từng kết quả, rồi cộng. Hoặc cộng giá gốc trước, áp markup, làm tròn một lần. Cả hai đều hợp lý, cả hai không ra cùng một con số, và không cái nào là một con bug bạn chỉ tay vào được.

Trên một booking, chênh lệch là một đơn vị làm tròn, đôi khi vài đơn vị — nhìn trong review thì vô hình. Cộng qua một tháng thì nó không có trần: nó lớn theo sản lượng và không tự triệt tiêu, vì làm tròn half-up không đối xứng quanh một phép cộng. Bản nhìn thấy được còn tệ hơn bản kế toán. Khách đọc bảng chi tiết theo phòng, cộng các dòng lại, rồi ra một số khác với cái tổng nằm ngay dưới — đó không còn là cuộc trao đổi về làm tròn với bộ phận tài chính nữa, mà là cuộc trao đổi về niềm tin với support.

Luật làm tròn: chọn một lần, và viết ra

Cái mình ngại không phải là chọn sai luật. Là không chọn gì cả. Chẳng ai viết ra "half-up, tới đơn vị nhỏ nhất của loại tiền" — nó cứ thế xảy ra, thừa hưởng từ cái formatter nào chạy sau cùng: một toFixed trong component UI, một helper trong template, một cột có scale cố định, hoặc một response của supplier vốn đã được làm tròn theo quy ước của họ trước khi mình kịp nhìn thấy. Mỗi chỗ đó là một quyết định về tiền, do một người lúc ấy không hề nghĩ về tiền đưa ra. Và ngay khi danh sách tenant có thêm một loại tiền không có phần lẻ, hoặc có ba chữ số lẻ, thì cái mặc định thừa hưởng kia sai ở đâu đó, và sai một cách im lặng.

Nên luật được phát biểu đúng một lần — mode và độ chính xác, theo từng loại tiền — trong một hàm, và số chỗ được phép gọi hàm đó ít tới mức liệt kê ra được.

Cái hiển thị và cái thu tiền

Kỷ luật thứ hai: làm tròn để hiển thị là một phép chiếu, và phép chiếu thì không bao giờ chảy ngược. Con số khách bị thu là con số nguyên đang lưu. Dáng bug cần canh là một giá trị đã làm tròn để hiển thị bị đọc ngược lại làm đầu vào cho một phép tính: ngay lúc đó, tầng giao diện đã trở thành một bên tham gia vào việc tính giá, mà phía dưới không ai biết.

Quy đổi là chỗ thứ ba, và nó có timestamp

Supplier báo giá bằng tiền của họ; tenant bán bằng một loại tiền khác. Quy đổi có làm tròn, nên nó là chỗ thứ ba. Quy đổi còn dùng một tỷ giá, mà tỷ giá thì gắn với một thời điểm. Lưu số tiền đã quy đổi mà không lưu kèm tỷ giá và thời điểm sinh ra nó, thì tính lại booking đó sau này là ra giá khác — cùng một booking trả lời khác nhau ở hai ngày, và không có cách nào phân biệt bug với thị trường. Nên số tiền đã quy đổi được lưu như một dữ kiện, kèm tỷ giá và timestamp, chứ không phải một thứ dẫn xuất chờ được tính lại. Khách trả bao nhiêu và nợ supplier bao nhiêu là hai dữ kiện ở hai loại tiền; bán xong rồi thì không bên nào tính ra bên nào nữa.

Con số mình không có

Trang này không có con số đối soát nào. Mình chưa từng chạy bản diff cả tháng giữa markup theo dòng và markup trên tổng, nên mình không nói được chênh lệch là bao nhiêu, theo chiều nào. Lập luận ở đây là về kỷ luật, không phải về một khoản tiền thu hồi được. Ai bảo họ sửa xong chuyện làm tròn rồi đưa bạn một con số, thì họ đã đo. Mình thì chưa.

Luật

Tiền cần một cách biểu diễn, một luật làm tròn, và một chỗ duy nhất áp luật đó. Trên site này có một bài khác nói về ba service không thống nhất với nhau về một cái giá. Bài này nhỏ hơn và ngượng hơn: một service không thống nhất với chính nó, vì cùng một số tiền đi hai đường khác nhau qua khâu làm tròn, mà cả hai đường đều do những người viết đúng viết ra.