DetectorGraph  2.0
fancyvendingmachine.cpp
Go to the documentation of this file.
1 // Copyright 2017 Nest Labs, Inc.
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14 
15 #include <iostream>
16 #include <vector>
17 #include <map>
18 #include <algorithm>
19 #include <numeric>
20 #include <memory>
21 
22 #include "graph.hpp"
23 #include "detector.hpp"
24 #include "graphanalyzer.hpp"
25 #include "processorcontainer.hpp"
26 #include "lag.hpp"
27 #include "dglogging.hpp"
28 
29 using namespace DetectorGraph;
30 using std::cout;
31 using std::endl;
32 
33 /**
34  * @file fancyvendingmachine.cpp
35  * @brief Sophisticated Vending Machine example using a Lag-based feedback loop.
36  *
37  * @section ex-fvm-intro Introduction
38  * You see, here things got a bit out of hand. All I was supposed to do was to
39  * code a single representative and fun example. But then I got legitimately
40  * nerd-snipped [1] and had to go all the way.
41  *
42  * @section ex-fvm-features Features
43  * This example provides a vending machine algorithm that keeps track of:
44  * - coin counting
45  * - overlapping-balance purchases
46  * - products in stock
47  * - product prices
48  * - canceling purchases half way through
49  * - change giving (including a solution to the Change-Making problem [2] for
50  * non-infinite sets of coins)
51  * - dynamic product refill
52  * - dynamic price updates
53  * - financial report generation
54  *
55  * @section ex-fvm-lts Large TopicState
56  * This example shows a concrete example of how to deal with
57  * a Large TopicState - large enough that one wouldn't any unnecessary copies
58  * of it. This is the case for the Look-Up Table generated by ChangeAlgo. That
59  * table is conveyed inside the ChangeAvailable TopicState to allow for
60  * efficient access to properties of that TopicState - namely to check whether
61  * change for a given amount can be given. The way this is accomplished is by
62  * using a shared_ptr to wrap the heap-allocated Look-Up Table.
63  *
64  * @section ex-fvm-arch Architecture
65  * The graph uses 6 detectors and 14 TopicStates to encode the different logic and
66  * data signals.
67  *
68  * The 'public' API to this graph is composed of:
69  * Inputs:
70  * - CoinInserted
71  * - MoneyBackButton
72  * - SelectedProduct
73  * - RefillProduct
74  * - RefillChange
75  * - PriceUpdate
76  * Outputs:
77  * - SaleProcessed
78  * - UserBalance
79  * - ReturnChange
80  * - FinancesReport
81  *
82  *
83  * The graph below shows the relationships between the topics (rectangles) and
84  * detectors (ellipses). Note that this graph can be automatically generated
85  * for any instance of DetectorGraph::Graph using DetectorGraph::GraphAnalyzer.
86  * @dot "FancyVendingMachine"
87 digraph GraphAnalyzer {
88  rankdir = "LR";
89  node[fontname=Helvetica];
90  size="12,5";
91 
92  "SelectedProduct" [label="0:SelectedProduct",style=filled, shape=box, color=lightblue];
93  "SelectedProduct" -> "SaleProcessor";
94  "MoneyBackButton" [label="1:MoneyBackButton",style=filled, shape=box, color=lightblue];
95  "MoneyBackButton" -> "UserBalanceDetector";
96  "CoinInserted" [label="2:CoinInserted",style=filled, shape=box, color=lightblue];
97  "CoinInserted" -> "CoinBankManager";
98  "CoinInserted" -> "UserBalanceDetector";
99  "RefillChange" [label="3:RefillChange",style=filled, shape=box, color=lightblue];
100  "RefillChange" -> "CoinBankManager";
101  "PriceUpdate" [label="4:PriceUpdate",style=filled, shape=box, color=lightblue];
102  "PriceUpdate" -> "ProductStockManager";
103  "RefillProduct" [label="5:RefillProduct",style=filled, shape=box, color=lightblue];
104  "RefillProduct" -> "ProductStockManager";
105  "LaggedSaleProcessed" [label="6:Lagged<SaleProcessed>",style=filled, shape=box, color=lightblue];
106  "LaggedSaleProcessed" -> "ProductStockManager";
107  "LaggedSaleProcessed" -> "UserBalanceDetector";
108  "LaggedSaleProcessed" -> "FinancesReportDetector";
109  "UserBalanceDetector" [label="7:UserBalanceDetector", color=blue];
110  "UserBalanceDetector" -> "UserBalance";
111  "UserBalanceDetector" -> "ReturnChange";
112  "ReturnChange" [label="8:ReturnChange",style=filled, shape=box, color=red];
113  "ReturnChange" -> "CoinBankManager";
114  "CoinBankManager" [label="9:CoinBankManager", color=blue];
115  "CoinBankManager" -> "ReleaseCoins";
116  "CoinBankManager" -> "ChangeAvailable";
117  "ChangeAvailable" [label="10:ChangeAvailable",style=filled, shape=box, color=red];
118  "ChangeAvailable" -> "SaleProcessor";
119  "ChangeAvailable" -> "FinancesReportDetector";
120  "ReleaseCoins" [label="11:ReleaseCoins",style=filled, shape=box, color=limegreen];
121  "UserBalance" [label="12:UserBalance",style=filled, shape=box, color=red];
122  "UserBalance" -> "SaleProcessor";
123  "UserBalance" -> "FinancesReportDetector";
124  "FinancesReportDetector" [label="13:FinancesReportDetector", color=blue];
125  "FinancesReportDetector" -> "FinancesReport";
126  "FinancesReport" [label="14:FinancesReport",style=filled, shape=box, color=limegreen];
127  "ProductStockManager" [label="15:ProductStockManager", color=blue];
128  "ProductStockManager" -> "StockState";
129  "StockState" [label="16:StockState",style=filled, shape=box, color=red];
130  "StockState" -> "SaleProcessor";
131  "SaleProcessor" [label="17:SaleProcessor", color=blue];
132  "SaleProcessor" -> "SaleProcessed";
133  "SaleProcessed" [label="18:SaleProcessed",style=filled, shape=box, color=red];
134  "SaleProcessed" -> "LagSaleProcessed";
135  "LagSaleProcessed" [label="19:Lag<SaleProcessed>", color=blue];
136  "LagSaleProcessed" -> "LaggedSaleProcessed" [style=dotted, color=red, constraint=false];
137 }
138  * @enddot
139  *
140  * @section ex-fvm-other-notes Other Notes
141  * Note that this entire application in contained in a single file for the sake
142  * of unity as an example. In real-world scenarios the suggested pattern is to
143  * split the code into:
144  *
145  @verbatim
146  detectorgraph/
147  include/
148  fancyvendingmachine.hpp (FancyVendingMachine header)
149  src/
150  fancyvendingmachine.hpp (FancyVendingMachine implementation)
151  detectors/
152  include/
153  UserBalanceDetector.hpp
154  SaleProcessor.hpp
155  ProductStockManager.hpp
156  CoinBankManager.hpp
157  FinancesReportDetector.hpp
158  src/
159  UserBalanceDetector.cpp
160  SaleProcessor.cpp
161  ProductStockManager.cpp
162  CoinBankManager.cpp
163  FinancesReportDetector.cpp
164  topicstates/
165  include/
166  CoinInserted.hpp
167  SelectedProduct.hpp
168  SaleProcessed.hpp
169  RefillProduct.hpp
170  PriceUpdate.hpp
171  StockState.hpp
172  UserBalance.hpp
173  MoneyBackButton.hpp
174  ReturnChange.hpp
175  FinancesReport.hpp
176  RefillChange.hpp
177  ReleaseCoins.hpp
178  ChangeAvailable.hpp
179  CoinType.hpp (enum and GetStr - common core datatypes used
180  across multiple topicstates are also commonly
181  stored here)
182  ProductIdType.hpp
183  src/
184  ChangeAvailable.cpp (TopicStates implementations, as needed)
185  CoinType.cpp (e.g GetCoinTypeStr())
186  ProductIdType.cpp
187  utils/ (For utility modules/algos, as needed)
188  include/
189  ChangeAlgo.hpp
190  src/
191  ChangeAlgo.cpp
192 
193  Some projects that use Protocol Buffers for its TopicStates may have:
194  proto/
195  some_data.proto
196 @endverbatim
197  *
198  * Although this example makes heavy use of C++11, that is NOT a requirement
199  * for DetectorGraph applications.
200  *
201  * @section ex-fvm-refs References
202  * - [1] Nerd Snipping - https://xkcd.com/356/
203  * - [2] Change-Making Problem - https://en.wikipedia.org/wiki/Change-making_problem
204  *
205  * @cond DO_NOT_DOCUMENT
206  */
207 enum CoinType
208 {
209  kCoinTypeNone = 0,
210  kCoinType5c = 5,
211  kCoinType10c = 10,
212  kCoinType25c = 25,
213  kCoinType50c = 50,
214  kCoinType1d = 100,
215 };
216 
217 enum ProductIdType{
218  kProductIdTypeNone = 0,
219  kSchokolade,
220  kApfelzaft,
221  kMate,
222  kFrischMilch,
223 };
224 
225 struct CoinInserted : public DetectorGraph::TopicState
226 {
227  CoinType coin;
228  CoinInserted(CoinType aCoin = kCoinTypeNone) : coin(aCoin) {}
229 };
230 
231 struct SelectedProduct : public DetectorGraph::TopicState
232 {
233  ProductIdType productId;
234  SelectedProduct(ProductIdType id = kProductIdTypeNone) : productId(id) {}
235 };
236 
237 struct SaleProcessed : public DetectorGraph::TopicState
238 {
239  ProductIdType productId;
240  int priceCents;
241  SaleProcessed(ProductIdType aProduct = kProductIdTypeNone, int aPrice = 0) : productId(aProduct), priceCents(aPrice) {}
242 };
243 
244 struct RefillProduct : public DetectorGraph::TopicState
245 {
246  ProductIdType productId;
247  int quantity;
248  RefillProduct(ProductIdType aProduct = kProductIdTypeNone, int aQuantity = 0) : productId(aProduct), quantity(aQuantity) {}
249 };
250 
251 //! [Mutually Atomic Variables]
252 struct PriceUpdate : public DetectorGraph::TopicState
253 {
254  ProductIdType productId;
255  int priceCents;
256  PriceUpdate(ProductIdType aProduct = kProductIdTypeNone, int aPrice = 0) : productId(aProduct), priceCents(aPrice) {}
257 };
258 //! [Mutually Atomic Variables]
259 
260 struct StockState : public DetectorGraph::TopicState
261 {
262  struct ProductState
263  {
264  int count;
265  int priceCents;
266  ProductState(int aCount = 0, int aPrice = 0) : count(aCount), priceCents(aPrice) {}
267  };
268 
269  std::map<ProductIdType, ProductState> products;
270 };
271 
272 struct UserBalance : public DetectorGraph::TopicState
273 {
274  int totalCents;
275 };
276 
277 //![Trivial TopicState]
278 struct MoneyBackButton : public DetectorGraph::TopicState
279 {
280 };
281 //![Trivial TopicState]
282 
283 struct ReturnChange : public DetectorGraph::TopicState
284 {
285  int totalCents;
286  ReturnChange(int total = 0) : totalCents(total) {}
287 };
288 
289 struct FinancesReport : public DetectorGraph::TopicState
290 {
291  FinancesReport(unsigned aBalance = 0) : balance(aBalance) {}
292  unsigned balance;
293 };
294 
295 namespace ChangeAlgo
296 {
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>;
303 
304  class ChangeLookupTable
305  {
306  public:
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()))
312  {
313  for (auto coin : mSetOfCoins)
314  {
315  assert(unsigned(coin) % mMinDenominator == 0);
316  }
317  assert(mMinDenominator != 0);
318  assert(mMaxChange % mMinDenominator == 0);
319 
320  /*
321  m = [[1, 2, 3, ... max_target], (for )
322  [{c1:,c2:,c3:}, ... {c1:,c2:,c3:}], (after trying to add mSetOfCoins[0])
323  [{c1:,c2:,c3:}, ... {c1:,c2:,c3:}], (after trying to add mSetOfCoins[1])
324  ...
325  [{c1:,c2:,c3:}, ... {c1:,c2:,c3:}], (after trying to add mSetOfCoins[-1]) <---- Only this is necessary after the table is built.
326  ]
327 
328  m[-1][x] is a list of sets of coins that produce X
329  */
330  unsigned midTargets = unsigned(mMaxChange / mMinDenominator);
331  unsigned targetColumnsRange = midTargets + 1;
332 
333  ChangeAlgo::LUTTable lutt = ChangeAlgo::LUTTable(mSetOfCoins.size(), ChangeAlgo::LUTRow(targetColumnsRange));
334 
335  for (unsigned coinIndex = 0; coinIndex < lutt.size(); ++coinIndex)
336  {
337  CoinType denominationType = mSetOfCoins[coinIndex];
338  unsigned denomination = static_cast<unsigned>(denominationType);
339  unsigned denominationSteps = unsigned(denomination / mMinDenominator);
340  // cout << "coinIndex=" << coinIndex << endl;
341  for (unsigned targetIdx = 0; targetIdx < targetColumnsRange; ++targetIdx)
342  {
343  unsigned target = targetIdx * mMinDenominator;
344 
345  // Use just this coin
346  if (denomination == target)
347  {
348  // Create new draw from a single coin
349  lutt[coinIndex][targetIdx].push_back(IncrementedDraw(mEmptyDraw, denominationType));
350  }
351  // Don't use this coin
352  else if (denomination > target)
353  {
354  if (coinIndex > 0)
355  {
356  // Copy from previous coin's solution (Up)
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)); // If we're not touching, why copying?
360  }
361  }
362  // Try adding two sets (using and not using this coin)
363  else
364  {
365  if (coinIndex > 0)
366  {
367  // Copy from previous coin's solution (Up)
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)); // If we're not touching, why copying?
371  }
372 
373  // Create new draws from incrementing solutions to smaller targets (Left)
374  for (ChangeAlgo::Draw leftDraw : lutt[coinIndex][targetIdx - denominationSteps])
375  {
376  lutt[coinIndex][targetIdx].push_back(IncrementedDraw(leftDraw, denominationType));
377  }
378  }
379  }
380  }
381  // lutt.back() the last LUTRow ot LUTTable containing the results considering all coins.
382  mLookupRow = lutt.back();
383  }
384 
385  Draw IncrementedDraw(const Draw& emptyDraw, CoinType denominationType) const
386  {
387  Draw draw = emptyDraw;
388  draw[denominationType] += 1;
389  return draw;
390  }
391 
392  Draw MakeEmptyDraw(const CoinSet& coinSet) const
393  {
394  Draw emptyDraw;
395  for (auto coinType : coinSet)
396  {
397  emptyDraw[coinType] = 0;
398  }
399  return emptyDraw;
400  }
401 
402  const LUTCell& GetChangeDraws(unsigned change) const
403  {
404  assert(mMinDenominator != 0);
405  assert(change % mMinDenominator == 0);
406  assert(change <= mMaxChange);
407 
408  unsigned t_idx = unsigned(change / mMinDenominator);
409  return mLookupRow.at(t_idx);
410  }
411 
412  unsigned GetDrawScore(const Draw& draw) const
413  {
414  return std::accumulate(draw.begin(), draw.end(), 0,
415  [](unsigned accumulator, const Draw::value_type& value) { return accumulator + value.second; });
416  }
417 
418  bool IsDrawPossible(const CoinStock& availableCoins, const Draw& draw) const
419  {
420  return std::all_of(availableCoins.begin(), availableCoins.end(), [&draw](const CoinStock::value_type& availableCoin) {
421  return (draw.at(availableCoin.first) <= availableCoin.second); // Draw has less-eq coins than available
422  });
423  }
424 
425  Draw GetSmallestChange(unsigned change) const
426  {
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); // Draw d1 has less total coins than d2
430  });
431  }
432 
433  Draw GetSmallestChange(const CoinStock& availableCoins, unsigned change) const
434  {
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);
439  });
440 
441  return *std::min_element(possibleDraws.begin(), possibleDraws.end(), [this](const Draw& d1, const Draw& d2) {
442  return GetDrawScore(d1) < GetDrawScore(d2); // Draw d1 has less total coins than d2
443  });
444  }
445 
446  bool CanGiveChange(const CoinStock& availableCoins, unsigned change) const
447  {
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);
451  });
452  }
453 
454  private:
455  /* const */ Draw mEmptyDraw;
456  /* const */ unsigned mMaxChange;
457  /* const */ CoinSet mSetOfCoins;
458  /* const */ unsigned mMinDenominator;
459  LUTRow mLookupRow;
460  };
461 }
462 
463 //! [TopicStates Inheritance Example]
464 struct RefillChange : public DetectorGraph::TopicState, public ChangeAlgo::CoinStock
465 {
466  RefillChange() {}
467  RefillChange(const ChangeAlgo::CoinStock& stock) : ChangeAlgo::CoinStock(stock) {}
468 };
469 
470 struct ReleaseCoins : public DetectorGraph::TopicState, public ChangeAlgo::Draw
471 {
472  ReleaseCoins() {}
473  ReleaseCoins(const ChangeAlgo::Draw& draw) : ChangeAlgo::Draw(draw) {}
474 };
475 //! [TopicStates Inheritance Example]
476 
477 //! [Immutable Shared Memory TopicState]
478 struct ChangeAvailable : public DetectorGraph::TopicState
479 {
480  ChangeAvailable() : mCoins(), mpChangeLookupTable() {}
481 
482  ChangeAvailable(const ChangeAlgo::CoinStock& aCoins, const std::shared_ptr<const ChangeAlgo::ChangeLookupTable>& aLookupTable)
483  : mCoins(aCoins)
484  , mpChangeLookupTable(aLookupTable)
485  {
486  }
487 
488  bool CanGiveChange(unsigned change) const
489  {
490  return mpChangeLookupTable->CanGiveChange(mCoins, change);
491  }
492 
493  ChangeAlgo::CoinStock mCoins;
494  std::shared_ptr<const ChangeAlgo::ChangeLookupTable> mpChangeLookupTable;
495 };
496 //! [Immutable Shared Memory TopicState]
497 
498 class UserBalanceDetector : public DetectorGraph::Detector
499 , public DetectorGraph::SubscriberInterface< Lagged<SaleProcessed> >
500 , public DetectorGraph::SubscriberInterface<CoinInserted>
501 , public DetectorGraph::SubscriberInterface<MoneyBackButton>
502 , public DetectorGraph::Publisher<UserBalance>
503 , public DetectorGraph::Publisher<ReturnChange>
504 {
505 public:
506  UserBalanceDetector(DetectorGraph::Graph* graph) : DetectorGraph::Detector(graph), mUserBalance()
507  {
508  Subscribe< Lagged<SaleProcessed> >(this);
509  Subscribe<CoinInserted>(this);
510  Subscribe<MoneyBackButton>(this);
511  SetupPublishing<UserBalance>(this);
512  SetupPublishing<ReturnChange>(this);
513  }
514 
515  virtual void Evaluate(const Lagged<SaleProcessed>& sale)
516  {
517  mUserBalance.totalCents -= sale.data.priceCents;
518  }
519 
520  virtual void Evaluate(const CoinInserted& inserted)
521  {
522  mUserBalance.totalCents += inserted.coin;
523  }
524 
525  virtual void Evaluate(const MoneyBackButton&)
526  {
527  Publisher<ReturnChange>::Publish(mUserBalance.totalCents);
528  mUserBalance.totalCents = 0;
529  }
530 
531  virtual void CompleteEvaluation()
532  {
533  DG_ASSERT(mUserBalance.totalCents >= 0);
534  DG_LOG("UserBalance total = %d cents", mUserBalance.totalCents);
535  Publisher<UserBalance>::Publish(mUserBalance);
536  }
537 
538 private:
539  UserBalance mUserBalance;
540 
541 };
542 
543 //![UnitTest-LowerUserBalanceOnSale]
544 void Test_LowerUserBalanceOnSale()
545 {
546  DetectorGraph::Graph graph;
547  UserBalanceDetector detector(&graph);
548  auto outTopic = graph.ResolveTopic<UserBalance>();
549  graph.PushData(CoinInserted(kCoinType1d));
550  graph.EvaluateGraph();
551  DG_ASSERT(outTopic->HasNewValue());
552  DG_ASSERT(outTopic->GetNewValue().totalCents == 100);
553 
554  graph.PushData(Lagged<SaleProcessed>(SaleProcessed(kProductIdTypeNone, 75)));
555  graph.EvaluateGraph();
556 
557  DG_ASSERT(outTopic->HasNewValue());
558  DG_ASSERT(outTopic->GetNewValue().totalCents == 25);
559 }
560 //![UnitTest-LowerUserBalanceOnSale]
561 
562 class SaleProcessor : public DetectorGraph::Detector
563 , public DetectorGraph::SubscriberInterface<UserBalance>
564 , public DetectorGraph::SubscriberInterface<SelectedProduct>
565 , public DetectorGraph::SubscriberInterface<StockState>
566 , public DetectorGraph::SubscriberInterface<ChangeAvailable>
567 , public DetectorGraph::Publisher<SaleProcessed>
568 {
569 public:
570  SaleProcessor(DetectorGraph::Graph* graph) : DetectorGraph::Detector(graph)
571  {
572  Subscribe<UserBalance>(this);
573  Subscribe<SelectedProduct>(this);
574  Subscribe<StockState>(this);
575  Subscribe<ChangeAvailable>(this);
576  SetupPublishing<SaleProcessed>(this);
577  }
578 
579  virtual void Evaluate(const UserBalance& aUserBalance)
580  {
581  mUserBalance = aUserBalance;
582  }
583 
584  virtual void Evaluate(const SelectedProduct& aSelection)
585  {
586  mSelection = aSelection;
587  DG_LOG("SaleProcessor; Selected ProductId=%d", mSelection.productId);
588  }
589 
590  virtual void Evaluate(const StockState& aStock)
591  {
592  mStock = aStock;
593  }
594 
595  virtual void Evaluate(const ChangeAvailable& aChangeAvailable)
596  {
597  mChangeAvailable = aChangeAvailable;
598  }
599 
600  virtual void CompleteEvaluation()
601  {
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))
607  {
608  Publish(SaleProcessed(mSelection.productId, stockForProduct->second.priceCents));
609  }
610  }
611 
612 private:
613  UserBalance mUserBalance;
614  SelectedProduct mSelection;
615  StockState mStock;
616  ChangeAvailable mChangeAvailable;
617 
618 };
619 
620 class ProductStockManager : public DetectorGraph::Detector
621 , public DetectorGraph::SubscriberInterface< Lagged<SaleProcessed> >
622 , public DetectorGraph::SubscriberInterface<RefillProduct>
623 , public DetectorGraph::SubscriberInterface<PriceUpdate>
624 , public DetectorGraph::Publisher<StockState>
625 {
626 public:
627  ProductStockManager(DetectorGraph::Graph* graph) : DetectorGraph::Detector(graph)
628  {
629  Subscribe< Lagged<SaleProcessed> >(this);
630  Subscribe<RefillProduct>(this);
631  Subscribe<PriceUpdate>(this);
632  SetupPublishing<StockState>(this);
633  }
634 
635  virtual void Evaluate(const Lagged<SaleProcessed>& aSale)
636  {
637  //remove from mStock
638  mStock.products[aSale.data.productId].count--;
639  DG_ASSERT(mStock.products[aSale.data.productId].count >= 0);
640  }
641 
642  virtual void Evaluate(const RefillProduct& aRefill)
643  {
644  // add to mStock
645  mStock.products[aRefill.productId].count += aRefill.quantity;
646  }
647 
648  virtual void Evaluate(const PriceUpdate& aUpdate)
649  {
650  // update product price
651  mStock.products[aUpdate.productId].priceCents = aUpdate.priceCents;
652  }
653 
654  virtual void CompleteEvaluation()
655  {
656  // publish mStock
657  Publish(mStock);
658  }
659 
660 private:
661  StockState mStock;
662 
663 };
664 
665 class CoinBankManager : public DetectorGraph::Detector
666 , public DetectorGraph::SubscriberInterface<ReturnChange>
667 , public DetectorGraph::SubscriberInterface<RefillChange>
668 , public DetectorGraph::SubscriberInterface<CoinInserted>
669 , public DetectorGraph::Publisher<ReleaseCoins>
670 , public DetectorGraph::Publisher<ChangeAvailable>
671 {
672 public:
673  CoinBankManager(DetectorGraph::Graph* graph)
674  : DetectorGraph::Detector(graph)
675  , mAvailable()
676  , mpChangeLookupTable(
677  std::make_shared<ChangeAlgo::ChangeLookupTable>(
678  ChangeAlgo::CoinSet({kCoinType5c, kCoinType10c, kCoinType25c, kCoinType50c, kCoinType1d}), 300))
679  {
680  Subscribe<ReturnChange>(this);
681  Subscribe<RefillChange>(this);
682  Subscribe<CoinInserted>(this);
683  SetupPublishing<ReleaseCoins>(this);
684  SetupPublishing<ChangeAvailable>(this);
685 
686  // ComputeLookup({kCoinType25c, kCoinType50c}, 200);
687  }
688 
689  virtual void Evaluate(const ReturnChange& aChange)
690  {
691  // remove from stock according to fancy proprietary change-giving
692  // algorithm. Ha! Ha, Ha..
693  ChangeAlgo::Draw returningChange = mpChangeLookupTable->GetSmallestChange(mAvailable, aChange.totalCents);
694 
695  Publisher<ReleaseCoins>::Publish(ReleaseCoins(returningChange));
696  }
697 
698  virtual void Evaluate(const RefillChange& aRefill)
699  {
700  // add refill to stock
701  for (auto coinRefill : aRefill)
702  {
703  mAvailable[coinRefill.first] += coinRefill.second;
704  }
705  }
706 
707  virtual void Evaluate(const CoinInserted& aInserted)
708  {
709  // add inserted coins to stock
710  mAvailable[aInserted.coin]++;
711  }
712 
713  virtual void CompleteEvaluation()
714  {
715  Publisher<ChangeAvailable>::Publish(ChangeAvailable(mAvailable, mpChangeLookupTable));
716  }
717 
718 private:
719  ChangeAlgo::CoinStock mAvailable;
720  std::shared_ptr<const ChangeAlgo::ChangeLookupTable> mpChangeLookupTable;
721 };
722 
723 class FinancesReportDetector : public DetectorGraph::Detector
724 , public DetectorGraph::SubscriberInterface<ChangeAvailable>
725 , public DetectorGraph::SubscriberInterface<UserBalance>
726 , public DetectorGraph::SubscriberInterface< Lagged<SaleProcessed> >
727 , public DetectorGraph::Publisher<FinancesReport>
728 {
729 public:
730  FinancesReportDetector(DetectorGraph::Graph* graph)
731  : DetectorGraph::Detector(graph)
732  , mChangeAvailable()
733  {
734  Subscribe<ChangeAvailable>(this);
735  Subscribe<UserBalance>(this);
736  Subscribe< Lagged<SaleProcessed> >(this);
737  SetupPublishing<FinancesReport>(this);
738  }
739 
740  virtual void Evaluate(const ChangeAvailable& changeAvailable)
741  {
742  mChangeAvailable = changeAvailable;
743  }
744 
745  virtual void Evaluate(const UserBalance& userBalance)
746  {
747  mUserBalance = userBalance;
748  }
749 
750  virtual void Evaluate(const Lagged<SaleProcessed>&)
751  {
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);
756  });
757 
758  Publish(FinancesReport(coinsBalance - mUserBalance.totalCents));
759  }
760 
761 private:
762  ChangeAvailable mChangeAvailable;
763  UserBalance mUserBalance;
764 };
765 
766 const char * GetCoinTypeStr(CoinType c)
767 {
768  switch(c)
769  {
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";
776  }
777  return "INVALID ENUM VALUE";
778 }
779 
780 const char * GetProductIdStr(ProductIdType p)
781 {
782  switch(p)
783  {
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";
789  }
790  return "INVALID ENUM VALUE";
791 }
792 
793 class FancyVendingMachine : public DetectorGraph::ProcessorContainer
794 {
795 public:
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>())
806  {
807  }
808 
809  ProductStockManager mProductStockManager;
810  CoinBankManager mCoinBankManager;
811  UserBalanceDetector mUserBalanceDetector;
812  SaleProcessor mSaleProcessor;
813  Lag<SaleProcessed> mSaleFeedBack;
814  FinancesReportDetector mFinancesReportDetector;
815  Topic<SaleProcessed>* saleTopic;
816  Topic<ReleaseCoins>* changeReleaseTopic;
817  Topic<FinancesReport>* financeReportTopic;
818 
819  virtual void ProcessOutput()
820  {
821  if (saleTopic->HasNewValue())
822  {
823  const auto sale = saleTopic->GetNewValue();
824  cout << "Sold " << GetProductIdStr(sale.productId) << " for " << sale.priceCents << endl;
825  }
826  if (changeReleaseTopic->HasNewValue())
827  {
828  const auto changeReleased = changeReleaseTopic->GetNewValue();
829  cout << "Money Returned ";
830  for (auto coin : changeReleased)
831  {
832  cout << coin.second << "x" << GetCoinTypeStr(coin.first) << ", ";
833  }
834  cout << endl;
835  }
836  if (financeReportTopic->HasNewValue())
837  {
838  const auto report = financeReportTopic->GetNewValue();
839  cout << "Current Balance: " << report.balance << endl;
840  }
841  }
842 };
843 
844 int main()
845 {
846  FancyVendingMachine fancyVendingMachine;
847 
848  fancyVendingMachine.ProcessData(RefillChange({{kCoinType25c, 0},
849  {kCoinType50c, 0}}));
850 
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));
857 
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));
863 
864  fancyVendingMachine.ProcessData(MoneyBackButton());
865 
866  fancyVendingMachine.ProcessData(CoinInserted(kCoinType25c));
867  fancyVendingMachine.ProcessData(CoinInserted(kCoinType50c));
868  fancyVendingMachine.ProcessData(CoinInserted(kCoinType50c));
869 
870  fancyVendingMachine.ProcessData(SelectedProduct(kApfelzaft));
871 
872  fancyVendingMachine.ProcessData(MoneyBackButton());
873 
874  GraphAnalyzer analyzer(fancyVendingMachine.mGraph);
875  analyzer.GenerateDotFile("fancy_vending_machine.dot");
876 
877  Test_LowerUserBalanceOnSale();
878 }
879 
880 /// @endcond DO_NOT_DOCUMENT
Implements a graph of Topics & Detectors with Input/Output APIs.
Definition: graph.hpp:127
void PushData(const TTopicState &aTopicState)
Push data to a specific topic in the graph.
Definition: graph.hpp:187
bool HasNewValue() const
Returns true if the new Data is available.
Definition: topic.hpp:166
A Base class for a basic Graph container.
Manage data and its handler.
Definition: topic.hpp:84
void DG_LOG(const char *aLogString,...)
Definition: dglogging.cpp:22
A templated TopicState used as the output of Lag.
Definition: lag.hpp:49
void Publish(const T &data)
Publish a new version of T to a Topic.
Definition: publisher.hpp:85
Topic< TTopicState > * ResolveTopic()
Find/add a topic in the detector graph.
Definition: graph.hpp:160
const T & GetNewValue() const
Returns reference to the new/latest value in the topic.
Definition: topic.hpp:179
Base struct for topic data types.
Definition: topicstate.hpp:52
Produces a Lagged<T> for T.
Definition: lag.hpp:93
Base class that implements a Publisher behavior.
Definition: publisher.hpp:66
A unit of logic in a DetectorGraph.
Definition: detector.hpp:68
A Pure interface that declares the Subscriber behavior.
Class that provides debugging/diagnostics to a DetectorGraph detector graph.
ErrorType EvaluateGraph()
Evaluate the whole graph.
Definition: graph.cpp:133
#define DG_ASSERT(condition)
Definition: dgassert.hpp:20