Book Store in Abap: Complete Solution & Deep Dive Guide
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:
- A discounted group of {Book 1, Book 2} and one full-price Book 1.
- 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)
- 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. - Remaining Books: {1, 2, 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. - 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
- 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. - Remaining Books: {1, 2, 3, 5}.
- Second Pass: The remaining books conveniently form another group of 4: {1, 2, 3, 5}.
Price: 4 books * $8 * (1 - 0.20) = $25.60. - 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
- 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.
- 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.
- 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.
- 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) |
|
|
| Brute Force / Permutations |
|
|
| Dynamic Programming |
|
|
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
- 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 TABLEwould 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_groupsmethod, 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.
- 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.
Post a Comment