איך להוריד את הדולר בפייתון

11/09/2022

שאלה מעניינת שפורסמה בפורום פייתון הזכירה לי שהכיף בתכנות הוא לא למצוא את התשובה. בסיפור הפעם מתכנת כתב תוכנית שמבקשת מחיר בדולרים, ורצה לעשות איזשהו חישוב על המחיר. בואו נניח שאנחנו רוצים להציע 10% הנחה. הבעיה היא שחלק מהמשתמשים הכניסו את המחיר עם התחילית $, והיה צריך למצוא דרך להיפטר ממנה. הנה קוד שהיה יכול להיות נקודת ההתחלה:

price = float(input("Original price is: "))
price_after_discount = price * 0.9

print(f"After coupon discount you'll pay {price_after_discount}")

והפעלה לדוגמה תקינה היא:

$ python removedollar.py
Original price is: 15
After coupon discount you'll pay 13.5

וכמובן שכשמכניסים את המטבע בהתחלה הכל נשבר:

Original price is: $10
Traceback (most recent call last):
  File "/Users/ynonp/tmp/blog/removedollar.py", line 1, in <module>
    price = float(input("Original price is: "))
ValueError: could not convert string to float: '$10'

מה עושים?

1. אופציה 1: למחוק את הדולר בהתחלה אם מכניסים אותו

גישה ראשונה לפיתרון יכולה להתבסס על זה שכשמשהו נשבר אנחנו מתקנים אותו. אז אם שמנו לב שמשתמשים מכניסים דולר בתחילת המחיר, פשוט נמחק את הדולר:

user_price = input("Original price is: ")
price = float(user_price.lstrip("$"))
price_after_discount = price * 0.9

print(f"After coupon discount you'll pay {price_after_discount}")

כמובן שהחיסרון הוא שאנחנו מוחקים רק דולר ורק מההתחלה. אם מישהו החליט להכניס מטבע שונה או בסוף במקום בהתחלה המעקף שלנו לא יעבוד.

2. אופציה 2: לנקות את הקלט עם ביטוי רגולארי

גישה יותר מתוחכמת היא לזהות את כל השטויות משני צידי המחיר עם ביטוי רגולארי ולקחת רק את המספרים:

import re

price = input("Original price is: ")
match = re.search(r'([\d.]+)', price)
if match:
    price = float(match.group(1))
    price_after_discount = price * 0.9

    print(f"After coupon discount you'll pay {price_after_discount}")
else:
    print(f"Invalid price {price}")

גישה זו יותר איטית ממחיקת רק התו הראשון, אבל גם יותר גמישה.

3. אופציה 3: לתפוס את השגיאה ולהציע למשתמש להכניס מחדש את הקלט

בחזרה לפשטות, אפשר להיות ישירים עם המשתמשים ולבקש מהם להקליד מספר בלי סימן מהצדדים. אם זה לא קרה נוכל לבקש מהם להקליד שוב:

while True:
    try:
        price = float(input("Original price is: "))
        price_after_discount = price * 0.9

        print(f"After coupon discount you'll pay {price_after_discount}")
        break
    except ValueError:
        print("Please type the price as number only (no $ sign)")

4. אופציה 4: להגביל את התווים שמשתמש יכול להקליד

וריאנט יותר מתוחכם על 3 יהיה להגביל ממש את המשתמש כך שאפשר יהיה להקליד רק ספרות או סימן נקודה. המודול readchar של פייתון עושה בדיוק את זה:

import sys, string
from readchar import readkey, key

def read_only_numbers(prompt):
    value = ""
    print(prompt, end="")
    sys.stdout.flush()

    while True:
        k = readkey()
        if k == key.ENTER:
            print()
            return float(value)
        if k not in string.digits and k != ".":
            continue

        print(k, end="")
        sys.stdout.flush()
        value += k


price = float(read_only_numbers("Original price is: "))
price_after_discount = price * 0.9

print(f"After coupon discount you'll pay {price_after_discount}")

5. סיכום

התרגיל הזה הוא אחת הסיבות שאני לא אוהב לחלק פיתרונות עם התרגילים פה באתר. לא משנה כמה פיתרונות ראיתם, תמיד אפשר לחשוב על עוד פיתרון והצורך לחפש את "הפיתרון הנכון" הוא בדיוק מה שעוצר אותנו מלהתקדם. המשחק כשרואים בעיה הוא לא לחפש איך "צריך" לפתור אותה, אלא איך "אפשר" לפתור אותה, ומה היתרונות והחסרונות של כל גישה.

נ.ב. כל הגישות כוללות טעות חישוב בגלל ש float-ים הם לא דרך טובה לייצג כסף. נסו למצוא איזה ערכים נותנים תוצאות לא נכונות, ולתקן באמצעות החלפת החישוב למספרים שלמים שמייצגים סנטים.