انتقل إلى المحتوى
أمين أبوزيد
منشورBEG-011-L06

عرض عملي: بناء طابعة الإيصالات مع التحقق من المدخلات

  • الدرس 6 من 6
  • 12 دقيقة قراءة
  • مبتدئ
في هذا الدرس 11 موضوعات

هذا الدرس متاح حاليًا باللغة الإنجليزية فقط، ولم تُنشر نسخته العربية بعد.

Lesson Overview

Time to put the counter together. Over five lessons you collected separate tools: Scanner to read, the nextInt/nextLine fix for the leftover, parseInt and hasNextInt for numbers, validation loops for bad answers, and printf for aligned output. In this demo you combine them into one program, a receipt printer: it reads item names, quantities, and prices, rejects bad quantities and prices by asking again, and prints an aligned receipt with subtotal, tax, and total.

This is a worked demo. We build the program in six small steps, run each version, and include the planned mistake from the lecture on purpose: the skipped item name. You will watch the bug appear, and then fix it. After this lesson you are ready for the module lab, which asks you to build the same kind of receipt on your own.

Why this matters: real programs are four or five small techniques in a row. The skill here is building in layers: get a tiny version working, then add one thing at a time.

This lesson builds on every lesson in this module.

Learning Objectives

By the end of this lesson you will be able to:

  • Plan a small input program as steps: read, validate, calculate, print.
  • Combine a Scanner, validation loops, and printf in one program.
  • Reproduce and fix the nextInt/nextLine trap inside a larger program.
  • Build the receipt text with String.format while reading, and print it at the end.
  • Test an input program with good and bad inputs, and read the results.
  • Explain how the program protects itself from invalid quantities and prices.

Written Explanation

The plan

We want this output (the numbers come from the module lab):

text
Item        Qty    Price     Total
Pen           2     1.50      3.00
Notebook      1     4.25      4.25
Subtotal                      7.25
Tax (15%)                     1.09
Total                         8.34

In steps:

  1. Ask for an item name. An empty name means "no more items".
  2. Ask for the quantity. Re-ask until it is a whole number of 1 or more.
  3. Ask for the price. Re-ask until it is a number above zero.
  4. Calculate the line total (quantity times price) and add it to the subtotal.
  5. Keep the formatted line for later.
  6. After the last item, calculate tax (15%) and print the receipt.

The tax rate is a constant. A named final constant (BEG-009) means the 0.15 is written once and its meaning is clear.

Why the receipt is built in a String

The receipt has a header, then the item lines, then the totals. But we only learn the items while the user answers questions, and the prompts would mix into the receipt if we printed each line immediately. So we collect each formatted line as text with String.format, and print them all at the end. lines += ... appends a new row to the text collected so far. (A StringBuilder class does this job more efficiently, but you meet it later; for a handful of rows this is fine.)

Step 1: the skeleton and the planned mistake

Start with the simplest version that reads an item. First, the version with the mistake, where the quantity comes before the name and uses nextInt():

java
import java.util.Scanner;

public class Bug {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        System.out.print("Quantity: ");
        int qty = in.nextInt();
        System.out.print("Item name: ");
        String name = in.nextLine();
        System.out.printf("[%-10s] x %d%n", name, qty);
    }
}

Run it with 2, then Pen as the answers:

text
Quantity: Item name: [          ] x 2

The name prompt never waited: name is empty (the brackets show ten spaces of padding around nothing). This is exactly the trap from Lesson 2. The nextInt() left the line break, and nextLine() read it. Why it matters here: in a receipt, an empty item name is a silent data error: the program runs, the receipt prints, and a row has no name.

Step 2: the fix, and a better design

Lesson 2 gave the fixes. Here we choose the order: ask for the name first, since a line followed by a number is safe, and keep one throw-away in.nextLine(); after the last number of each item, because the loop then returns to the name question. That is the design in the final program below.

Step 3: the quantity loop

The validation loop from Lesson 5, with the minimum of 1 for the quantity:

java
int qty;
while (true) {
    System.out.print("Quantity: ");
    if (in.hasNextInt()) {
        qty = in.nextInt();
        if (qty >= 1) {
            break;
        }
    } else {
        in.next();
    }
    System.out.println("Enter a whole number, 1 or more.");
}

This rejects text, decimals such as 2.5, zero, and negatives. The lab demands exactly this: not a whole number, or less than 1, means ask again.

Step 4: the price loop

The price loop is the same shape with hasNextDouble() and price > 0. Note that it is the same shape as the quantity loop: when the checks follow a pattern, you can copy the structure and change the type and the condition. (Later you will learn to avoid this repetition with methods.)

java
double price;
while (true) {
    System.out.print("Price: ");
    if (in.hasNextDouble()) {
        price = in.nextDouble();
        if (price > 0) {
            break;
        }
    } else {
        in.next();
    }
    System.out.println("Enter a price above zero, like 1.50.");
}
in.nextLine();              // finish the price line

price > 0 rejects NaN too, as Lesson 5 explained, because every comparison with NaN is false. The last line is the throw-away call: the price was the last token on its line, and the next thing the program reads is the next item's name with nextLine().

Step 5: calculate, collect, print

java
double lineTotal = qty * price;
subtotal += lineTotal;
lines += String.format("%-12s %3d %8.2f %9.2f%n", name, qty, price, lineTotal);

The format string is what makes the columns: name 12 wide, left-aligned; quantity 3 wide; price 8 wide with two decimals; total 9 wide with two decimals; each separated by one space. The header and the totals rows use the same widths, with %s in place of the numbers for the header, and a wider left column for the labels of the totals.

At the end:

java
double tax = subtotal * TAX_RATE;
System.out.printf("%-12s %3s %8s %9s%n", "Item", "Qty", "Price", "Total");
System.out.print(lines);
System.out.printf("%-25s %9.2f%n", "Subtotal", subtotal);
System.out.printf("%-25s %9.2f%n", "Tax (15%)", tax);
System.out.printf("%-25s %9.2f%n", "Total", subtotal + tax);

%-25s is 12 + 1 + 3 + 1 + 8 = 25 characters, which is exactly the width of the three left columns plus their spaces, so the totals land under the Total column. Counting widths like this is how you design a table on paper before coding it.

One detail: the label Tax (15%) is passed as a value for %s, not as part of the format string, so the % in it is safe. If you wrote it inside a format string, you would need %%.

Examples

Example 1: the whole program (simple)

java
import java.util.Scanner;

public class Receipt {
    public static void main(String[] args) {
        final double TAX_RATE = 0.15;
        Scanner in = new Scanner(System.in);
        String lines = "";
        double subtotal = 0;

        while (true) {
            System.out.print("Item name (empty to finish): ");
            String name = in.nextLine().trim();
            if (name.isEmpty()) {
                break;
            }

            int qty;
            while (true) {
                System.out.print("Quantity: ");
                if (in.hasNextInt()) {
                    qty = in.nextInt();
                    if (qty >= 1) {
                        break;
                    }
                } else {
                    in.next();
                }
                System.out.println("Enter a whole number, 1 or more.");
            }

            double price;
            while (true) {
                System.out.print("Price: ");
                if (in.hasNextDouble()) {
                    price = in.nextDouble();
                    if (price > 0) {
                        break;
                    }
                } else {
                    in.next();
                }
                System.out.println("Enter a price above zero, like 1.50.");
            }
            in.nextLine();              // finish the price line

            double lineTotal = qty * price;
            subtotal += lineTotal;
            lines += String.format("%-12s %3d %8.2f %9.2f%n", name, qty, price, lineTotal);
        }

        double tax = subtotal * TAX_RATE;
        System.out.println();
        System.out.printf("%-12s %3s %8s %9s%n", "Item", "Qty", "Price", "Total");
        System.out.print(lines);
        System.out.printf("%-25s %9.2f%n", "Subtotal", subtotal);
        System.out.printf("%-25s %9.2f%n", "Tax (15%)", tax);
        System.out.printf("%-25s %9.2f%n", "Total", subtotal + tax);
    }
}

The outer loop collects items. An empty name leaves it. The inner loops are the validators. Everything after the loops can rely on qty being at least 1 and price above zero.

Example 2: a run with bad quantities (practical)

The module's example input: Pen, 2, 1.50, then Notebook, then the quantities zero, 0, and 1, then the price 4.25, then an empty name. The run printed this (the typed answers were read from a file, so only the prompts and messages show):

text
Item name (empty to finish): Quantity: Price: Item name (empty to finish): Quantity: Enter a whole number, 1 or more.
Quantity: Enter a whole number, 1 or more.
Quantity: Price: Item name (empty to finish): 
Item         Qty    Price     Total
Pen            2     1.50      3.00
Notebook       1     4.25      4.25
Subtotal                       7.25
Tax (15%)                      1.09
Total                          8.34

Two bad quantities (zero, 0) each produced the message and a new prompt, and the third answer, 1, was accepted. The receipt has the same numbers as the lab's example, with columns aligned (the exact spacing differs slightly; the lab only requires aligned columns). The tax is 7.25 * 0.15 = 1.0875, which displays as 1.09, and the total 8.3375 displays as 8.34.

Example 3: a bigger basket and the limits of the display (realistic)

With three items, Pen 2 at 1.50, Notebook 1 at 4.25, and Cable 3 at 19.99:

text
Item         Qty    Price     Total
Pen            2     1.50      3.00
Notebook       1     4.25      4.25
Cable          3    19.99     59.97
Subtotal                      67.22
Tax (15%)                     10.08
Total                         77.30

By hand: 3.00 + 4.25 + 59.97 = 67.22. Tax: 67.22 * 0.15 = 10.083, shown as 10.08. Total: 77.303, shown as 77.30. The rows add up here, but this is not guaranteed: a displayed total can differ from the displayed sum by one cent. Real billing systems define a rounding rule and use exact decimal arithmetic (a later module); remember that printf rounds for display only.

Backend Insight

This little program has the skeleton of a real service:

  1. Input layer: read raw data and refuse bad data at the border.
  2. Logic: calculate totals from validated values.
  3. Output layer: format the result for a reader.

In a backend, the input layer is a controller or API endpoint, validation is done by a framework (Bean Validation, previewed in Lesson 5), the logic is a service, and the output is JSON instead of a text table. The separation is the same, and it is why you should resist the urge to mix "ask the user" code with "calculate the total" code. A later module breaks this program into methods to do exactly that.

Two other lessons from the demo apply to real software: build in layers (each step ran before the next was added), and test the ugly inputs first: abc, 0, empty, and extreme values.

Common Mistakes

Mistake 1: The skipped item name

Reading a number and then a line without the throw-away nextLine(). In this program, put in.nextLine(); after the price read.

Mistake 2: Columns that drift

Different widths in the header, the item rows, and the totals. Count characters, and reuse the same widths.

Mistake 3: Validating the quantity but not the price

A negative price makes a negative line total, and a total that is lower than it should be. Every number that comes from outside needs a range check.

Mistake 4: Assuming the input never ends

If the input ends in the middle of a question (for example a test file with missing answers), next() throws a NoSuchElementException; that was the real error in a test run. Interactive users rarely meet it; scripts and tests do.

Exercises

Try each exercise yourself before opening the solutions.

Level A: Foundation

A1. In the receipt program, why is the item name read with nextLine() and the quantity with nextInt()?

أظهر الحل A1

A1. The name may contain spaces, so it needs a whole line. The quantity is a single whole number, so nextInt() fits, preceded by hasNextInt() to avoid a crash.

A2. Which line prevents the item name from being skipped on the second item, and where does it sit?

أظهر الحل A2

A2. The throw-away in.nextLine(); after the price loop. It consumes the rest of the price line, so the next nextLine() for the name waits for real input.

A3. What does an empty item name do in this program?

أظهر الحل A3

A3. It ends the item loop (break), and the receipt is printed with what was collected.

Level B: Application

B1. Change the tax rate to 8% and the label to match. Which two places must change? How could you avoid changing the label by hand?

أظهر الحل B1

B1. Change TAX_RATE to 0.08 and the label text "Tax (15%)" to "Tax (8%)". To avoid the second edit, build the label with the constant: String.format("Tax (%.0f%%)", TAX_RATE * 100), which turns 0.08 into 8 (not run separately; based on the compiled %.1f%% pattern of Lesson 4).

B2. Add a Discount row that subtracts 10% of the subtotal before tax. Describe the changes, then compute the receipt for a subtotal of 100.00.

أظهر الحل B2

B2. After computing subtotal, add double discount = subtotal * 0.10;, print a Discount row in the same style as the other totals, and compute tax on subtotal - discount. For 100.00: discount 10.00, taxable 90.00, tax 13.50, total 103.50. (Hand calculation.)

B3. The receipt prints a header and totals even when the user adds no items. Is that a bug? Describe one way to behave better.

أظهر الحل B3

B3. It is not a crash, but it is poor behaviour: an empty receipt is noise. After the loop, check whether subtotal is zero (or whether any item was added) and print "No items" instead.

Level C: Challenge

C1. Add a validation for the item name: reject names longer than 12 characters by asking again, with a message that says the limit. Where in the program does the new check go, and which String method gives the length?

أظهر الحل C1

C1. The check goes right after reading the name, inside the outer loop, as another small validation loop around the name. name.length() gives the length (BEG-009). Re-ask with Names can have at most 12 characters.

C2. Allow quantities up to 99 only. Write the new condition. Then add the matching error message and explain why a maximum is useful for a real shop.

أظهر الحل C2

C2. if (qty >= 1 && qty <= 99) { break; } with the message Enter a whole number from 1 to 99. A maximum catches typos such as 1000 for 10, which would produce a large wrong bill.

Mini Challenge

Start the module lab, BEG-011-LAB1 Receipt printer, without copying this code. Close this page and write your own version from the six steps in "The plan". Then compare with Example 1 and list three differences and whether each one is a style choice or a real improvement. Test your own version with this list of inputs, and write down the result of each:

  1. An empty first item name.
  2. Quantity abc, 0, -1, 2.5, then 3.
  3. Price 0, free, then 9.99.
  4. A name with spaces such as Blue Pen.

Check your work: nothing crashes on bad input; every re-prompt says what is valid; columns line up with the header; and the subtotal, tax, and total match your hand calculation.

Summary & Next Lesson

Key takeaways

  • A real program combines small tools: read with Scanner, validate in loops, calculate, then format with printf/String.format.
  • The planned mistake, nextInt() followed by nextLine(), silently produces an empty name. The throw-away nextLine() fixes it.
  • Validation loops guarantee that code after them can rely on the value.
  • Build with String.format while reading and print the receipt at the end, so prompts do not mix with the table.
  • printf rounds for display. Exact money arithmetic is a later topic.

Next lesson

This is the last lesson of the module. The next module, BEG-012, starts with "Making decisions with if", the tool you previewed in the validation loops.

Self-check

  1. Why does the program use String.format to build lines instead of printing each one?
  2. What does price > 0 reject?
  3. Where is the throw-away nextLine() placed, and why?
  4. How do you keep the header and rows aligned?
  5. What happens at the end of the program if no item was entered?
أظهر الإجابات
  1. The prompts and answers appear while reading, so printing rows immediately would mix them into the table. Collecting and printing at the end keeps the receipt clean.
  2. Zero, negative numbers, and NaN, because NaN > 0 is false.
  3. After the price loop, so the rest of the price line is consumed before the next item name is read with nextLine().
  4. Use the same widths in the header, the rows, and the totals labels.
  5. The header is printed with totals of 0.00 and no rows.

References

  • Oracle, Java SE 21 API, Formatter. Supports the width, precision, and - flag rules behind the aligned columns, %n, and the display rounding.
  • Oracle, Java SE 21 API, Scanner. Supports the token and line reading behaviour that makes the throw-away nextLine() necessary.
  • OWASP Cheat Sheet Series, Input Validation Cheat Sheet. Supports validating the form and the meaning of input at the point it is received.
  • The receipt program and the bug demo were compiled and run with JDK 21 and the quoted outputs are real. The architectural comparison to controllers, services, and JSON output is general background, not verified against a page here.

التطبيق

طبّق هذا الدرس بمختبر الوحدة وتمارينها واختبارها.

المختبر والتمارين الاختبار

تعليم الدرس كمكتمل اختياري ويمكنك التراجع عنه في أي وقت. يبقى تقدمك محفوظًا في هذا المتصفح.

اكتب للبحث في المستويات والمراحل والوحدات والدروس.