218 kProductIdTypeNone = 0,
228 CoinInserted(CoinType aCoin = kCoinTypeNone) : coin(aCoin) {}
233 ProductIdType productId;
234 SelectedProduct(ProductIdType
id = kProductIdTypeNone) : productId(id) {}
239 ProductIdType productId;
241 SaleProcessed(ProductIdType aProduct = kProductIdTypeNone,
int aPrice = 0) : productId(aProduct), priceCents(aPrice) {}
246 ProductIdType productId;
248 RefillProduct(ProductIdType aProduct = kProductIdTypeNone,
int aQuantity = 0) : productId(aProduct), quantity(aQuantity) {}
254 ProductIdType productId;
256 PriceUpdate(ProductIdType aProduct = kProductIdTypeNone,
int aPrice = 0) : productId(aProduct), priceCents(aPrice) {}
266 ProductState(
int aCount = 0,
int aPrice = 0) : count(aCount), priceCents(aPrice) {}
269 std::map<ProductIdType, ProductState> products;
286 ReturnChange(
int total = 0) : totalCents(total) {}
291 FinancesReport(
unsigned aBalance = 0) : balance(aBalance) {}
297 using CoinSet = std::vector<CoinType>;
298 using CoinStock = std::map<CoinType, unsigned>;
299 using Draw = std::map<CoinType, unsigned>;
300 using LUTCell = std::vector<Draw>;
301 using LUTRow = std::vector<LUTCell>;
302 using LUTTable = std::vector<LUTRow>;
304 class ChangeLookupTable
307 ChangeLookupTable(
const ChangeAlgo::CoinSet& setOfCoins,
unsigned maxChange)
308 : mEmptyDraw(MakeEmptyDraw(setOfCoins))
309 , mMaxChange(maxChange)
310 , mSetOfCoins(setOfCoins)
311 , mMinDenominator(*
std::min_element(setOfCoins.begin(), setOfCoins.end()))
313 for (
auto coin : mSetOfCoins)
315 assert(
unsigned(coin) % mMinDenominator == 0);
317 assert(mMinDenominator != 0);
318 assert(mMaxChange % mMinDenominator == 0);
330 unsigned midTargets = unsigned(mMaxChange / mMinDenominator);
331 unsigned targetColumnsRange = midTargets + 1;
333 ChangeAlgo::LUTTable lutt = ChangeAlgo::LUTTable(mSetOfCoins.size(), ChangeAlgo::LUTRow(targetColumnsRange));
335 for (
unsigned coinIndex = 0; coinIndex < lutt.size(); ++coinIndex)
337 CoinType denominationType = mSetOfCoins[coinIndex];
338 unsigned denomination =
static_cast<unsigned>(denominationType);
339 unsigned denominationSteps = unsigned(denomination / mMinDenominator);
341 for (
unsigned targetIdx = 0; targetIdx < targetColumnsRange; ++targetIdx)
343 unsigned target = targetIdx * mMinDenominator;
346 if (denomination == target)
349 lutt[coinIndex][targetIdx].push_back(IncrementedDraw(mEmptyDraw, denominationType));
352 else if (denomination > target)
357 ChangeAlgo::LUTCell& fromCell = lutt[coinIndex - 1][targetIdx];
358 ChangeAlgo::LUTCell& toCell = lutt[coinIndex][targetIdx];
359 std::copy(fromCell.begin(), fromCell.end(), std::back_inserter(toCell));
368 ChangeAlgo::LUTCell& fromCell = lutt[coinIndex - 1][targetIdx];
369 ChangeAlgo::LUTCell& toCell = lutt[coinIndex][targetIdx];
370 std::copy(fromCell.begin(), fromCell.end(), std::back_inserter(toCell));
374 for (ChangeAlgo::Draw leftDraw : lutt[coinIndex][targetIdx - denominationSteps])
376 lutt[coinIndex][targetIdx].push_back(IncrementedDraw(leftDraw, denominationType));
382 mLookupRow = lutt.back();
385 Draw IncrementedDraw(
const Draw& emptyDraw, CoinType denominationType)
const 387 Draw draw = emptyDraw;
388 draw[denominationType] += 1;
392 Draw MakeEmptyDraw(
const CoinSet& coinSet)
const 395 for (
auto coinType : coinSet)
397 emptyDraw[coinType] = 0;
402 const LUTCell& GetChangeDraws(
unsigned change)
const 404 assert(mMinDenominator != 0);
405 assert(change % mMinDenominator == 0);
406 assert(change <= mMaxChange);
408 unsigned t_idx = unsigned(change / mMinDenominator);
409 return mLookupRow.at(t_idx);
412 unsigned GetDrawScore(
const Draw& draw)
const 414 return std::accumulate(draw.begin(), draw.end(), 0,
415 [](
unsigned accumulator,
const Draw::value_type& value) {
return accumulator + value.second; });
418 bool IsDrawPossible(
const CoinStock& availableCoins,
const Draw& draw)
const 420 return std::all_of(availableCoins.begin(), availableCoins.end(), [&draw](
const CoinStock::value_type& availableCoin) {
421 return (draw.at(availableCoin.first) <= availableCoin.second);
425 Draw GetSmallestChange(
unsigned change)
const 427 const LUTCell& draws = GetChangeDraws(change);
428 return *std::min_element(draws.begin(), draws.end(), [
this](
const Draw& d1,
const Draw& d2) {
429 return GetDrawScore(d1) < GetDrawScore(d2);
433 Draw GetSmallestChange(
const CoinStock& availableCoins,
unsigned change)
const 435 const LUTCell& draws = GetChangeDraws(change);
436 LUTCell possibleDraws;
437 std::copy_if(draws.begin(), draws.end(), std::back_inserter(possibleDraws), [
this, &availableCoins](
const Draw& d) {
438 return IsDrawPossible(availableCoins, d);
441 return *std::min_element(possibleDraws.begin(), possibleDraws.end(), [
this](
const Draw& d1,
const Draw& d2) {
442 return GetDrawScore(d1) < GetDrawScore(d2);
446 bool CanGiveChange(
const CoinStock& availableCoins,
unsigned change)
const 448 const LUTCell& draws = GetChangeDraws(change);
449 return (
bool)std::count_if(draws.begin(), draws.end(), [
this, &availableCoins](
const Draw& d) {
450 return IsDrawPossible(availableCoins, d);
458 unsigned mMinDenominator;
467 RefillChange(
const ChangeAlgo::CoinStock& stock) : ChangeAlgo::CoinStock(stock) {}
473 ReleaseCoins(
const ChangeAlgo::Draw& draw) : ChangeAlgo::Draw(draw) {}
480 ChangeAvailable() : mCoins(), mpChangeLookupTable() {}
482 ChangeAvailable(
const ChangeAlgo::CoinStock& aCoins,
const std::shared_ptr<const ChangeAlgo::ChangeLookupTable>& aLookupTable)
484 , mpChangeLookupTable(aLookupTable)
488 bool CanGiveChange(
unsigned change)
const 490 return mpChangeLookupTable->CanGiveChange(mCoins, change);
493 ChangeAlgo::CoinStock mCoins;
494 std::shared_ptr<const ChangeAlgo::ChangeLookupTable> mpChangeLookupTable;
508 Subscribe< Lagged<SaleProcessed> >(
this);
509 Subscribe<CoinInserted>(
this);
510 Subscribe<MoneyBackButton>(
this);
511 SetupPublishing<UserBalance>(
this);
512 SetupPublishing<ReturnChange>(
this);
517 mUserBalance.totalCents -= sale.
data.priceCents;
520 virtual void Evaluate(
const CoinInserted& inserted)
522 mUserBalance.totalCents += inserted.coin;
525 virtual void Evaluate(
const MoneyBackButton&)
528 mUserBalance.totalCents = 0;
531 virtual void CompleteEvaluation()
534 DG_LOG(
"UserBalance total = %d cents", mUserBalance.totalCents);
539 UserBalance mUserBalance;
544 void Test_LowerUserBalanceOnSale()
547 UserBalanceDetector detector(&graph);
549 graph.
PushData(CoinInserted(kCoinType1d));
552 DG_ASSERT(outTopic->GetNewValue().totalCents == 100);
558 DG_ASSERT(outTopic->GetNewValue().totalCents == 25);
572 Subscribe<UserBalance>(
this);
573 Subscribe<SelectedProduct>(
this);
574 Subscribe<StockState>(
this);
575 Subscribe<ChangeAvailable>(
this);
576 SetupPublishing<SaleProcessed>(
this);
579 virtual void Evaluate(
const UserBalance& aUserBalance)
581 mUserBalance = aUserBalance;
584 virtual void Evaluate(
const SelectedProduct& aSelection)
586 mSelection = aSelection;
587 DG_LOG(
"SaleProcessor; Selected ProductId=%d", mSelection.productId);
590 virtual void Evaluate(
const StockState& aStock)
595 virtual void Evaluate(
const ChangeAvailable& aChangeAvailable)
597 mChangeAvailable = aChangeAvailable;
600 virtual void CompleteEvaluation()
602 const auto stockForProduct = mStock.products.find(mSelection.productId);
603 if (stockForProduct != mStock.products.end() &&
604 stockForProduct->second.count > 0 &&
605 stockForProduct->second.priceCents <= mUserBalance.totalCents &&
606 mChangeAvailable.CanGiveChange(mUserBalance.totalCents - stockForProduct->second.priceCents))
608 Publish(SaleProcessed(mSelection.productId, stockForProduct->second.priceCents));
613 UserBalance mUserBalance;
614 SelectedProduct mSelection;
616 ChangeAvailable mChangeAvailable;
629 Subscribe< Lagged<SaleProcessed> >(
this);
630 Subscribe<RefillProduct>(
this);
631 Subscribe<PriceUpdate>(
this);
632 SetupPublishing<StockState>(
this);
638 mStock.products[aSale.
data.productId].count--;
642 virtual void Evaluate(
const RefillProduct& aRefill)
645 mStock.products[aRefill.productId].count += aRefill.quantity;
648 virtual void Evaluate(
const PriceUpdate& aUpdate)
651 mStock.products[aUpdate.productId].priceCents = aUpdate.priceCents;
654 virtual void CompleteEvaluation()
676 , mpChangeLookupTable(
677 std::make_shared<ChangeAlgo::ChangeLookupTable>(
678 ChangeAlgo::CoinSet({kCoinType5c, kCoinType10c, kCoinType25c, kCoinType50c, kCoinType1d}), 300))
680 Subscribe<ReturnChange>(
this);
681 Subscribe<RefillChange>(
this);
682 Subscribe<CoinInserted>(
this);
683 SetupPublishing<ReleaseCoins>(
this);
684 SetupPublishing<ChangeAvailable>(
this);
689 virtual void Evaluate(
const ReturnChange& aChange)
693 ChangeAlgo::Draw returningChange = mpChangeLookupTable->GetSmallestChange(mAvailable, aChange.totalCents);
698 virtual void Evaluate(
const RefillChange& aRefill)
701 for (
auto coinRefill : aRefill)
703 mAvailable[coinRefill.first] += coinRefill.second;
707 virtual void Evaluate(
const CoinInserted& aInserted)
710 mAvailable[aInserted.coin]++;
713 virtual void CompleteEvaluation()
719 ChangeAlgo::CoinStock mAvailable;
720 std::shared_ptr<const ChangeAlgo::ChangeLookupTable> mpChangeLookupTable;
734 Subscribe<ChangeAvailable>(
this);
735 Subscribe<UserBalance>(
this);
736 Subscribe< Lagged<SaleProcessed> >(
this);
737 SetupPublishing<FinancesReport>(
this);
740 virtual void Evaluate(
const ChangeAvailable& changeAvailable)
742 mChangeAvailable = changeAvailable;
745 virtual void Evaluate(
const UserBalance& userBalance)
747 mUserBalance = userBalance;
752 const ChangeAlgo::CoinStock& stock = mChangeAvailable.mCoins;
753 unsigned coinsBalance = std::accumulate(stock.begin(), stock.end(), 0,
754 [](
unsigned accumulator,
const ChangeAlgo::CoinStock::value_type& value) {
755 return accumulator + (value.first * value.second);
758 Publish(FinancesReport(coinsBalance - mUserBalance.totalCents));
762 ChangeAvailable mChangeAvailable;
763 UserBalance mUserBalance;
766 const char * GetCoinTypeStr(CoinType c)
770 case kCoinType5c:
return "5c";
771 case kCoinType10c:
return "10c";
772 case kCoinType25c:
return "25c";
773 case kCoinType50c:
return "50c";
774 case kCoinType1d:
return "1d";
775 case kCoinTypeNone:
return "NOT A COIN";
777 return "INVALID ENUM VALUE";
780 const char * GetProductIdStr(ProductIdType p)
784 case kSchokolade:
return "Schokolade";
785 case kApfelzaft:
return "Apfelzaft";
786 case kMate:
return "Mate";
787 case kFrischMilch:
return "FrischMilch";
788 case kProductIdTypeNone:
return "NOT A PRODUCT";
790 return "INVALID ENUM VALUE";
796 FancyVendingMachine()
797 : mProductStockManager(&mGraph)
798 , mCoinBankManager(&mGraph)
799 , mUserBalanceDetector(&mGraph)
800 , mSaleProcessor(&mGraph)
801 , mSaleFeedBack(&mGraph)
802 , mFinancesReportDetector(&mGraph)
803 , saleTopic(mGraph.ResolveTopic<SaleProcessed>())
804 , changeReleaseTopic(mGraph.ResolveTopic<ReleaseCoins>())
805 , financeReportTopic(mGraph.ResolveTopic<FinancesReport>())
809 ProductStockManager mProductStockManager;
810 CoinBankManager mCoinBankManager;
811 UserBalanceDetector mUserBalanceDetector;
812 SaleProcessor mSaleProcessor;
814 FinancesReportDetector mFinancesReportDetector;
819 virtual void ProcessOutput()
824 cout <<
"Sold " << GetProductIdStr(sale.productId) <<
" for " << sale.priceCents << endl;
828 const auto changeReleased = changeReleaseTopic->
GetNewValue();
829 cout <<
"Money Returned ";
830 for (
auto coin : changeReleased)
832 cout << coin.second <<
"x" << GetCoinTypeStr(coin.first) <<
", ";
838 const auto report = financeReportTopic->
GetNewValue();
839 cout <<
"Current Balance: " << report.balance << endl;
846 FancyVendingMachine fancyVendingMachine;
848 fancyVendingMachine.ProcessData(RefillChange({{kCoinType25c, 0},
849 {kCoinType50c, 0}}));
851 fancyVendingMachine.ProcessData(PriceUpdate(kFrischMilch, 200));
852 fancyVendingMachine.ProcessData(PriceUpdate(kSchokolade, 100));
853 fancyVendingMachine.ProcessData(PriceUpdate(kApfelzaft, 150));
854 fancyVendingMachine.ProcessData(RefillProduct(kFrischMilch, 5));
855 fancyVendingMachine.ProcessData(RefillProduct(kSchokolade, 4));
856 fancyVendingMachine.ProcessData(RefillProduct(kApfelzaft, 3));
858 fancyVendingMachine.ProcessData(CoinInserted(kCoinType25c));
859 fancyVendingMachine.ProcessData(CoinInserted(kCoinType50c));
860 fancyVendingMachine.ProcessData(CoinInserted(kCoinType50c));
861 fancyVendingMachine.ProcessData(CoinInserted(kCoinType50c));
862 fancyVendingMachine.ProcessData(SelectedProduct(kApfelzaft));
864 fancyVendingMachine.ProcessData(MoneyBackButton());
866 fancyVendingMachine.ProcessData(CoinInserted(kCoinType25c));
867 fancyVendingMachine.ProcessData(CoinInserted(kCoinType50c));
868 fancyVendingMachine.ProcessData(CoinInserted(kCoinType50c));
870 fancyVendingMachine.ProcessData(SelectedProduct(kApfelzaft));
872 fancyVendingMachine.ProcessData(MoneyBackButton());
875 analyzer.GenerateDotFile(
"fancy_vending_machine.dot");
877 Test_LowerUserBalanceOnSale();
Implements a graph of Topics & Detectors with Input/Output APIs.
void PushData(const TTopicState &aTopicState)
Push data to a specific topic in the graph.
bool HasNewValue() const
Returns true if the new Data is available.
A Base class for a basic Graph container.
Manage data and its handler.
void DG_LOG(const char *aLogString,...)
A templated TopicState used as the output of Lag.
void Publish(const T &data)
Publish a new version of T to a Topic.
Topic< TTopicState > * ResolveTopic()
Find/add a topic in the detector graph.
const T & GetNewValue() const
Returns reference to the new/latest value in the topic.
Base struct for topic data types.
Produces a Lagged<T> for T.
Base class that implements a Publisher behavior.
A unit of logic in a DetectorGraph.
A Pure interface that declares the Subscriber behavior.
Class that provides debugging/diagnostics to a DetectorGraph detector graph.
ErrorType EvaluateGraph()
Evaluate the whole graph.
#define DG_ASSERT(condition)