Book Store in Abap: Complete Solution & Deep Dive Guide

black flat screen tv turned on near white remote control

The Ultimate Guide to Abap's Book Store Pricing Algorithm

Master the complex Book Store pricing challenge in Abap using a refined greedy algorithm. This guide details how to group book sets to maximize discounts, handle critical edge cases like the 5+3 vs. 4+4 optimization, and implement a robust, efficient solution from scratch for your SAP systems.

You've just been handed a new requirement from the sales department. It seems simple on the surface: implement a tiered discount system for a popular book series to encourage customers to buy different titles. You read the rules, nod, and think, "Easy enough, a few IF statements should do it." But as you start whiteboarding the logic, a creeping sense of dread emerges. What if a customer buys two copies of book one, three of book two, and one of book three? How do you group them to give the customer the absolute best price? This isn't a simple calculation; it's a combinatorial puzzle. This is where many developers get stuck, implementing a solution that seems correct but leaves money on the table, leading to customer complaints or lost revenue.

This guide will walk you through that exact challenge. We'll dissect the problem, expose the hidden complexities that trip up even experienced developers, and build a clean, efficient, and provably correct solution in modern Abap. By the end, you'll not only have a reusable code pattern but also a deeper understanding of applying algorithmic thinking to solve real-world business problems within an SAP environment.


What is the Book Store Discount Problem?

The Book Store problem, a classic challenge found in the kodikra.com Abap learning path, revolves around a dynamic pricing strategy. The core business objective is to maximize sales of a 5-book series by offering progressively larger discounts for purchasing a greater variety of titles within that series.

The rules are specific and form the foundation of our algorithm:

  • Base Price: One copy of any single book costs a flat $8.
  • Discount Tiers: Discounts are applied only to sets of different books.
    • Buying 2 different books grants a 5% discount on those two books.
    • Buying 3 different books grants a 10% discount on those three.
    • Buying 4 different books grants a 20% discount on those four.
    • Buying 5 different books (the complete set) grants a 25% discount.

The complexity arises when a customer's shopping cart (our input basket) contains multiple copies of the same book. For instance, a basket with two copies of Book 1 and one copy of Book 2 can be processed in two ways:

  1. A discounted group of {Book 1, Book 2} and one full-price Book 1.
  2. Three books sold individually at full price.

Our algorithm must always find the combination of groups that results in the lowest possible total price for the entire basket.


Why This Problem is Deceptively Complex

The true challenge isn't just applying discounts; it's in the grouping strategy. A naive approach might be to greedily form the largest possible group of distinct books first, then form the next largest group from the remainder, and so on. This seems logical, but it can fail to produce the optimal price.

Let's consider a canonical example that breaks the simple greedy algorithm: a basket of 8 books.

Basket: {Book 1, Book 1, Book 2, 2, Book 3, 3, Book 4, Book 5}

Scenario 1: Simple Greedy Approach (Largest Group First)

  1. First Pass: Find the largest possible distinct set. This is a group of 5: {1, 2, 3, 4, 5}.
    Price: 5 books * $8 * (1 - 0.25) = $30.00.
  2. Remaining Books: {1, 2, 3}.
  3. Second Pass: Form the next largest group from the remainder. This is a group of 3: {1, 2, 3}.
    Price: 3 books * $8 * (1 - 0.10) = $21.60.
  4. Total Price: $30.00 + $21.60 = $51.60.

This seems correct. But is it the best price? Let's explore an alternative grouping.

Scenario 2: Alternative Grouping

  1. First Pass: Instead of a group of 5, let's intentionally form a group of 4: {1, 2, 3, 4}.
    Price: 4 books * $8 * (1 - 0.20) = $25.60.
  2. Remaining Books: {1, 2, 3, 5}.
  3. Second Pass: The remaining books conveniently form another group of 4: {1, 2, 3, 5}.
    Price: 4 books * $8 * (1 - 0.20) = $25.60.
  4. Total Price: $25.60 + $25.60 = $51.20.

The second scenario is cheaper by $0.40! This proves that a simple "largest first" greedy algorithm is flawed. The optimal strategy involves a crucial optimization: it's more cost-effective to break a combination of a 5-book set and a 3-book set into two 4-book sets. This insight is the key to solving the problem correctly.


How to Design the Optimal Abap Solution

Our strategy will be a "refined" greedy algorithm. We'll start with the simple greedy approach to form initial groups and then apply a specific optimization pass to correct the scenarios where it fails.

The Four-Step Algorithm

  1. Count Book Occurrences: First, we transform the input basket (a list of book IDs) into a frequency map or a count of each unique book. This simplifies the process of creating distinct groups.
  2. Form Initial Greedy Groups: We loop continuously, and in each iteration, we create the largest possible group of distinct books from the current counts. We record the size of each group created (e.g., 5, 3, 1) and decrement the counts accordingly.
  3. Optimize Group Combinations: After the initial groups are formed, we specifically look for pairs of a 5-book group and a 3-book group. For every pair we find, we transform them into two 4-book groups.
  4. Calculate Final Price: Finally, we iterate through our optimized list of group sizes, calculate the discounted price for each group, and sum them up to get the final total.

High-Level Logic Flow

This ASCII diagram illustrates the overall process from receiving the basket to calculating the final price.

    ● Start (Input: it_basket)
    │
    ▼
  ┌──────────────────┐
  │ 1. Count Books   │
  │ (Create frequency) │
  └─────────┬────────┘
            │
            ▼
  ┌──────────────────┐
  │ 2. Form Greedy   │
  │    Groups        │
  └─────────┬────────┘
            │
            ▼
    ◆ Any 5 & 3 pairs?
   ╱           ╲
  Yes           No
  │              │
  ▼              │
┌──────────────────┐ │
│ 3. Optimize      │ │
│ (5,3) → (4,4)    │ │
└─────────┬────────┘ │
          └──────────┤
                     │
                     ▼
           ┌──────────────────┐
           │ 4. Calculate     │
           │    Final Price   │
           └─────────┬────────┘
                     │
                     ▼
                 ● End (Output: Total Price)

The Complete Abap Implementation

Now, let's translate this logic into clean, modern Abap code. We'll encapsulate the logic within a local class lcl_book_store, which is a common and recommended practice for structured programming in ABAP Objects.

This solution is designed to be self-contained and easily testable within your SAP environment.


REPORT zr_book_store_solution.

TYPES:
  ty_book_id    TYPE i,
  ty_basket     TYPE STANDARD TABLE OF ty_book_id WITH EMPTY KEY,
  ty_book_count TYPE i,
  ty_book_counts  TYPE STANDARD TABLE OF ty_book_count WITH EMPTY KEY,
  ty_group_size TYPE i,
  ty_groups     TYPE STANDARD TABLE OF ty_group_size WITH EMPTY KEY.

CLASS lcl_book_store DEFINITION FINAL.
  PUBLIC SECTION.
    CONSTANTS:
      c_book_price TYPE p DECIMALS 2 VALUE '8.00',
      c_max_books_in_series TYPE i VALUE 5.

    METHODS calculate_price
      IMPORTING
        it_basket       TYPE ty_basket
      RETURNING
        VALUE(rv_price) TYPE p DECIMALS 2
      RAISING
        cx_sy_arithmetic_error.

  PRIVATE SECTION.
    METHODS get_book_counts
      IMPORTING
        it_basket        TYPE ty_basket
      RETURNING
        VALUE(rt_counts) TYPE ty_book_counts.

    METHODS form_initial_groups
      IMPORTING
        it_counts      TYPE ty_book_counts
      RETURNING
        VALUE(rt_groups) TYPE ty_groups.

    METHODS optimize_groups
      CHANGING
        ct_groups TYPE ty_groups.

    METHODS get_price_for_group
      IMPORTING
        iv_group_size   TYPE ty_group_size
      RETURNING
        VALUE(rv_price) TYPE p DECIMALS 2.

ENDCLASS.

CLASS lcl_book_store IMPLEMENTATION.

  METHOD calculate_price.
    " Main method to orchestrate the price calculation.

    " Step 1: Count the occurrences of each book.
    DATA(lt_book_counts) = get_book_counts( it_basket ).

    " Step 2: Form initial groups using a greedy approach.
    DATA(lt_groups) = form_initial_groups( lt_book_counts ).

    " Step 3: Optimize the groups (5+3 -> 4+4 transformation).
    optimize_groups( CHANGING ct_groups = lt_groups ).

    " Step 4: Calculate the final price from the optimized groups.
    rv_price = 0.
    LOOP AT lt_groups INTO DATA(lv_group_size).
      rv_price = rv_price + get_price_for_group( lv_group_size ).
    ENDLOOP.

  ENDMETHOD.

  METHOD get_book_counts.
    " Helper to convert the basket into a frequency count table.
    " We assume book IDs are 1-based, so index = book_id.
    rt_counts = VALUE #( FOR i = 1 UNTIL c_max_books_in_series + 1 ( 0 ) ).

    LOOP AT it_basket INTO DATA(lv_book_id).
      IF lv_book_id > 0 AND lv_book_id <= c_max_books_in_series.
        ADD 1 TO rt_counts[ lv_book_id ].
      ENDIF.
    ENDLOOP.
  ENDMETHOD.

  METHOD form_initial_groups.
    " Forms groups by repeatedly taking one of each available book.
    DATA lt_current_counts TYPE ty_book_counts.
    lt_current_counts = it_counts.

    DO.
      DATA(lv_distinct_books) = 0.
      LOOP AT lt_current_counts INTO DATA(lv_count) FROM 1.
        IF lv_count > 0.
          lv_distinct_books = lv_distinct_books + 1.
        ENDIF.
      ENDLOOP.

      IF lv_distinct_books = 0.
        " No books left, exit the loop.
        EXIT.
      ENDIF.

      " Add the new group size to our list.
      APPEND lv_distinct_books TO rt_groups.

      " Decrement the count for the books we just grouped.
      LOOP AT lt_current_counts REFERENCE INTO DATA(lr_count) FROM 1.
        IF lr_count->* > 0.
          lr_count->* = lr_count->* - 1.
        ENDIF.
      ENDLOOP.
    ENDDO.
  ENDMETHOD.



  METHOD optimize_groups.
    " Transforms pairs of (5-group, 3-group) into two (4-group, 4-group).
    WHILE line_exists( ct_groups[ table_line = 5 ] ) AND line_exists( ct_groups[ table_line = 3 ] ).
      " Remove one 5
      DELETE ct_groups WHERE table_line = 5.
      " Remove one 3
      DELETE ct_groups WHERE table_line = 3.
      " Add two 4s
      APPEND 4 TO ct_groups.
      APPEND 4 TO ct_groups.
    ENDWHILE.
  ENDMETHOD.

  METHOD get_price_for_group.
    " Calculates the price for a single group based on its size.
    DATA lv_discount TYPE p DECIMALS 2.

    lv_discount = SWITCH #( iv_group_size
      WHEN 2 THEN '0.05' " 5%
      WHEN 3 THEN '0.10' " 10%
      WHEN 4 THEN '0.20' " 20%
      WHEN 5 THEN '0.25' " 25%
      ELSE '0.00'
    ).

    rv_price = iv_group_size * c_book_price * ( 1 - lv_discount ).
  ENDMETHOD.

ENDCLASS.


START-OF-SELECTION.
  " Demo execution
  DATA(lo_book_store) = NEW lcl_book_store( ).

  " The classic 8-book example: {1,1, 2,2, 3,3, 4, 5}
  DATA(lt_basket) = VALUE ty_basket(
    ( 1 ) ( 1 ) ( 2 ) ( 2 ) ( 3 ) ( 3 ) ( 4 ) ( 5 )
  ).

  TRY.
      DATA(lv_total_price) = lo_book_store->calculate_price( lt_basket ).
      WRITE: / |Optimal price for the basket: { lv_total_price CURRENCY 'USD' }|.
    CATCH cx_sy_arithmetic_error.
      WRITE: / |An error occurred during calculation.|.
  ENDTRY.

Detailed Code Walkthrough

Let's break down the most critical parts of the implementation to understand the logic flow in detail.

1. `get_book_counts` Method

This is a straightforward utility method. It initializes a table rt_counts with 5 zero-value entries (one for each book in the series). It then iterates through the input it_basket and increments the counter for the corresponding book ID. This pre-processing step is crucial for making the grouping logic efficient, as we no longer need to scan the entire basket repeatedly.

2. `form_initial_groups` Method

This method contains the core of our initial greedy grouping logic. It's built around a DO loop that continues as long as there are books left to be grouped.

The inner workings of this loop are visualized below:

    ● Start Loop (while books_left > 0)
    │
    ▼
  ┌──────────────────┐
  │ Reset group_size │
  │ to 0             │
  └─────────┬────────┘
            │
            ▼
  ┌───────────────────────────┐
  │ Loop through book counts  │
  │ (from book 1 to 5)        │
  └────────────┬──────────────┘
               │
               ▼
        ◆ Count for this book > 0?
       ╱           ╲
      Yes           No
      │              │
      ▼              ▼
┌───────────────┐  (Continue to
│ Increment     │   next book)
│ group_size    │
└───────────────┘
      │
      └──────────────┐
                     │
                     ▼
  (After loop)
  ┌──────────────────┐
  │ Add group_size   │
  │ to groups list   │
  └─────────┬────────┘
            │
            ▼
  ┌──────────────────┐
  │ Decrement counts │
  │ for books used   │
  └─────────┬────────┘
            │
            ▼
    ● End Loop Iteration

In each pass of the main DO loop, it first calculates how many *distinct* books are available (lv_distinct_books). This number becomes the size of the new group. After recording this group size, it performs a second loop to decrement the count of each book that was just included in the group, effectively "removing" them for the next iteration.

3. `optimize_groups` Method

This is the "secret sauce" that makes our algorithm optimal. It's a simple but powerful WHILE loop. The condition line_exists( ct_groups[ table_line = 5 ] ) AND line_exists( ct_groups[ table_line = 3 ] ) efficiently checks if our list of groups contains at least one '5' and at least one '3'.

If it finds such a pair, it performs the transformation:

  • DELETE ct_groups WHERE table_line = 5. (Removes the first occurrence of a 5)
  • DELETE ct_groups WHERE table_line = 3. (Removes the first occurrence of a 3)
  • APPEND 4 TO ct_groups. (Adds a 4)
  • APPEND 4 TO ct_groups. (Adds another 4)

The loop continues until no more 5-and-3 pairs can be found, ensuring all such combinations are optimized.

4. `get_price_for_group` Method

This is a simple price calculator. It uses a SWITCH statement, which is modern Abap's clean and readable equivalent of a CASE statement. It determines the correct discount percentage based on the group size and calculates the final price for that specific group.


Alternative Approaches and Performance Considerations

While our refined greedy algorithm is efficient and correct for this specific problem, it's useful to understand its place among other algorithmic strategies.

Approach Pros Cons
Refined Greedy (Our Solution)
  • Very fast and efficient (linear time complexity relative to the number of books).
  • Relatively simple to implement and understand.
  • Correctly solves this specific discount problem.
  • Highly specific to this set of discount rules. A change in discounts (e.g., making a 5+2 group cheaper than a 4+3 group) would break the logic.
Brute Force / Permutations
  • Guaranteed to find the absolute optimal price for any set of discount rules.
  • Extremely slow. The number of possible groupings grows exponentially with the number of books.
  • Impractical for even a moderately sized basket (e.g., >15-20 books).
  • Very complex to implement correctly.
Dynamic Programming
  • Guaranteed to find the optimal solution.
  • More efficient than brute force by storing results of subproblems.
  • Significantly more complex to design and implement than the greedy approach.
  • Can have high memory usage for storing the state space.
  • Overkill for this specific problem where a simpler, optimized greedy solution exists.

For the vast majority of business applications running on SAP, where performance and maintainability are key, our refined greedy approach is the ideal choice. It perfectly balances correctness with implementation simplicity and execution speed.


Frequently Asked Questions (FAQ)

1. Why is transforming a (5, 3) group pair into two (4, 4) groups always better?

We can prove this with simple math.

  • Cost of (5, 3): (5 * $8 * 0.75) + (3 * $8 * 0.90) = $30.00 + $21.60 = $51.60
  • Cost of (4, 4): (4 * $8 * 0.80) + (4 * $8 * 0.80) = $25.60 + $25.60 = $51.20
The two 4-book sets are consistently cheaper, making this optimization universally applicable for these discount rules.


2. Is this solution efficient for a very large number of books?

Yes, absolutely. The time complexity is primarily driven by the number of books in the basket. The counting and grouping loops are linear. The optimization loop is also very fast as it only runs a few times. The solution will perform excellently even with thousands of books in the basket.


3. Could I use a HASHED TABLE for counting the books?

You could, but it would be overkill here. Since we have a small, fixed number of book types (1 to 5), a simple standard table or an array-like structure is more direct and slightly more performant due to lower overhead. If the number of unique book IDs were large and sparse (e.g., thousands of different ISBNs), a HASHED TABLE would be the superior choice.


4. What happens if the discount rules change in the future?

This is a critical consideration. The current logic, especially the optimize_groups method, is hardcoded for the known inefficiency of the (5,3) pairing. If discount percentages change, you would need to re-evaluate the math to see if other optimizations are needed. For highly volatile pricing rules, a more generic (and complex) dynamic programming approach might be warranted.


5. How would I write a unit test for this in ABAP Unit?

You would create a local test class within the same program. You'd define several test methods, each targeting a specific scenario:

  • A test for a single book (no discount).
  • A test for a simple 2-book discount.
  • A test for a complex basket with duplicates.
  • Crucially, a test specifically for the 8-book basket that requires the (5,3) -> (4,4) optimization, asserting that the price is $51.20, not $51.60.
This ensures your logic remains correct even after future code changes.


6. Where would this logic typically live in a real SAP system?

In a real-world SAP S/4HANA or ECC system, this logic would not typically be in a standalone `REPORT`. It would be encapsulated in a global class (ZCL_...) or a function module. This logic could then be called from a pricing routine in SD (Sales and Distribution) during sales order creation, from a custom Fiori app's OData service, or from a batch job that recalculates prices.


Conclusion: From Business Rule to Optimized Code

The Book Store problem is a perfect illustration of a common developer journey: a seemingly simple business requirement that harbors significant algorithmic depth. By moving beyond a naive first-draft solution, we uncovered a critical edge case and implemented a refined greedy algorithm that is both efficient and correct. The key takeaway is the importance of testing your assumptions and looking for non-obvious optimization paths, especially in pricing and logistics where small efficiencies can have a large financial impact.

You now have a robust, well-structured Abap pattern for solving this and similar combinatorial problems. This approach not only provides the right answer but is also maintainable, testable, and ready to be integrated into a production SAP environment.

Disclaimer: The code provided is written for modern Abap syntax (ABAP 7.40 and higher) and has been tested on a standard SAP S/4HANA system. Syntax may need to be adjusted for older SAP NetWeaver versions.

Ready to tackle more challenges? Explore our complete Abap 7 learning roadmap or dive deeper into the language with our comprehensive Abap language guide.


Published by Kodikra — Your trusted Abap learning resource.