From 3216448bbc67d76da197591f349e81ab9f65113e Mon Sep 17 00:00:00 2001
From: Vojtech Bubnik <bubnikv@gmail.com>
Date: Fri, 29 Oct 2021 13:36:26 +0200
Subject: [PATCH] PlaceholderParser: implemented round(), digits() and
 zdigits() macros. round() rounds to an integer. This is a popular request,
 for example #3472

digits(value, num_digits, num_decimals) rounds to num_digits and
	num_decimals, left filled with spaces.
digits(value, num_digits) the same as digits(value, num_digits, 0)
    Neither decimal separator nor any decimals after decimal separator
    are emitted.
zdigits(...) is the same as digits(...) only left filled with zeros.
If the result does not fit num_digits, the result is never trimmed.
---
 src/libslic3r/PlaceholderParser.cpp         | 68 ++++++++++++++++++++-
 tests/libslic3r/test_placeholder_parser.cpp | 14 +++++
 2 files changed, 79 insertions(+), 3 deletions(-)

diff --git a/src/libslic3r/PlaceholderParser.cpp b/src/libslic3r/PlaceholderParser.cpp
index c4702aef2..1aa5a883e 100644
--- a/src/libslic3r/PlaceholderParser.cpp
+++ b/src/libslic3r/PlaceholderParser.cpp
@@ -238,6 +238,7 @@ namespace client
         int                 i() const { return data.i; }
         void                set_i(int v) { this->reset(); this->data.i = v; this->type = TYPE_INT; }
         int                 as_i() const { return (this->type == TYPE_INT) ? this->i() : int(this->d()); }
+        int                 as_i_rounded() const { return (this->type == TYPE_INT) ? this->i() : int(std::round(this->d())); }
         double&             d()       { return data.d; }
         double              d() const { return data.d; }
         void                set_d(double v) { this->reset(); this->data.d = v; this->type = TYPE_DOUBLE; }
@@ -319,7 +320,7 @@ namespace client
         expr unary_integer(const Iterator start_pos) const
         { 
             switch (this->type) {
-            case TYPE_INT :
+            case TYPE_INT:
                 return expr<Iterator>(this->i(), start_pos, this->it_range.end());
             case TYPE_DOUBLE:
                 return expr<Iterator>(static_cast<int>(this->d()), start_pos, this->it_range.end()); 
@@ -331,10 +332,25 @@ namespace client
             return expr();
         }
 
+        expr round(const Iterator start_pos) const
+        { 
+            switch (this->type) {
+            case TYPE_INT:
+                return expr<Iterator>(this->i(), start_pos, this->it_range.end());
+            case TYPE_DOUBLE:
+                return expr<Iterator>(static_cast<int>(std::round(this->d())), start_pos, this->it_range.end());
+            default:
+                this->throw_exception("Cannot round a non-numeric value.");
+            }
+            assert(false);
+            // Suppress compiler warnings.
+            return expr();
+        }
+
         expr unary_not(const Iterator start_pos) const
         { 
             switch (this->type) {
-            case TYPE_BOOL :
+            case TYPE_BOOL:
                 return expr<Iterator>(! this->b(), start_pos, this->it_range.end());
             default:
                 this->throw_exception("Cannot apply a not operator.");
@@ -549,6 +565,30 @@ namespace client
             }
         }
 
+        // Store the result into param1.
+        // param3 is optional
+        template<bool leading_zeros>
+        static void digits(expr &param1, expr &param2, expr &param3)
+        { 
+            throw_if_not_numeric(param1);
+            if (param2.type != TYPE_INT)
+                param2.throw_exception("digits: second parameter must be integer");
+            bool has_decimals = param3.type != TYPE_EMPTY;
+            if (has_decimals && param3.type != TYPE_INT)
+                param3.throw_exception("digits: third parameter must be integer");
+
+            char buf[256];
+            int  ndigits = std::clamp(param2.as_i(), 0, 64);
+            if (has_decimals) {
+                // Format as double.
+                int decimals = std::clamp(param3.as_i(), 0, 64);
+                sprintf(buf, leading_zeros ? "%0*.*lf" : "%*.*lf", ndigits, decimals, param1.as_d());
+            } else
+                // Format as int.
+                sprintf(buf, leading_zeros ? "%0*d" : "%*d", ndigits, param1.as_i_rounded());
+            param1.set_s(buf);
+        }
+
         static void regex_op(expr &lhs, boost::iterator_range<Iterator> &rhs, char op)
         {
             const std::string *subject  = nullptr;
@@ -930,6 +970,7 @@ namespace client
         { "additive_expression",        "Expecting an expression." },
         { "multiplicative_expression",  "Expecting an expression." },
         { "unary_expression",           "Expecting an expression." },
+        { "optional_parameter",         "Expecting a closing brace or an optional parameter." },
         { "scalar_variable_reference",  "Expecting a scalar variable reference."},
         { "variable_reference",         "Expecting a variable reference."},
         { "regular_expression",         "Expecting a regular expression."}
@@ -1194,6 +1235,10 @@ namespace client
                         { out = value.unary_not(out.it_range.begin()); }
                 static void to_int(expr<Iterator> &value, expr<Iterator> &out)
                         { out = value.unary_integer(out.it_range.begin()); }
+                static void round(expr<Iterator> &value, expr<Iterator> &out)
+                        { out = value.round(out.it_range.begin()); }
+                // For indicating "no optional parameter".
+                static void noexpr(expr<Iterator> &out) { out.reset(); }
             };
             unary_expression = iter_pos[px::bind(&FactorActions::set_start_pos, _1, _val)] >> (
                     scalar_variable_reference(_r1)                  [ _val = _1 ]
@@ -1207,7 +1252,12 @@ namespace client
                                                                     [ px::bind(&expr<Iterator>::max, _val, _2) ]
                 |   (kw["random"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > ')') 
                                                                     [ px::bind(&MyContext::random<Iterator>, _r1, _val, _2) ]
-                |   (kw["int"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::to_int,  _1,     _val) ]
+                |   (kw["digits"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > optional_parameter(_r1))
+                                                                    [ px::bind(&expr<Iterator>::digits<false>, _val, _2, _3) ]
+                |   (kw["zdigits"] > '(' > conditional_expression(_r1) [_val = _1] > ',' > conditional_expression(_r1) > optional_parameter(_r1))
+                                                                    [ px::bind(&expr<Iterator>::digits<true>, _val, _2, _3) ]
+                |   (kw["int"]   > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::to_int,  _1, _val) ]
+                |   (kw["round"] > '(' > conditional_expression(_r1) > ')') [ px::bind(&FactorActions::round,   _1, _val) ]
                 |   (strict_double > iter_pos)                      [ px::bind(&FactorActions::double_, _1, _2, _val) ]
                 |   (int_      > iter_pos)                          [ px::bind(&FactorActions::int_,    _1, _2, _val) ]
                 |   (kw[bool_] > iter_pos)                          [ px::bind(&FactorActions::bool_,   _1, _2, _val) ]
@@ -1216,6 +1266,12 @@ namespace client
                 );
             unary_expression.name("unary_expression");
 
+            optional_parameter = iter_pos[px::bind(&FactorActions::set_start_pos, _1, _val)] >> (
+                    lit(')')                                       [ px::bind(&FactorActions::noexpr, _val) ]
+                |   (lit(',') > conditional_expression(_r1) > ')') [ _val = _1 ]
+                );
+            optional_parameter.name("optional_parameter");
+
             scalar_variable_reference = 
                 variable_reference(_r1)[_a=_1] >>
                 (
@@ -1234,6 +1290,8 @@ namespace client
 
             keywords.add
                 ("and")
+                ("digits")
+                ("zdigits")
                 ("if")
                 ("int")
                 //("inf")
@@ -1244,6 +1302,7 @@ namespace client
                 ("min")
                 ("max")
                 ("random")
+                ("round")
                 ("not")
                 ("or")
                 ("true");
@@ -1266,6 +1325,7 @@ namespace client
                 debug(additive_expression);
                 debug(multiplicative_expression);
                 debug(unary_expression);
+                debug(optional_parameter);
                 debug(scalar_variable_reference);
                 debug(variable_reference);
                 debug(regular_expression);
@@ -1303,6 +1363,8 @@ namespace client
         RuleExpression multiplicative_expression;
         // Number literals, functions, braced expressions, variable references, variable indexing references.
         RuleExpression unary_expression;
+        // Accepting an optional parameter.
+        RuleExpression optional_parameter;
         // Rule to capture a regular expression enclosed in //.
         qi::rule<Iterator, boost::iterator_range<Iterator>(), spirit_encoding::space_type> regular_expression;
         // Evaluate boolean expression into bool.
diff --git a/tests/libslic3r/test_placeholder_parser.cpp b/tests/libslic3r/test_placeholder_parser.cpp
index 59784e940..abf7308f2 100644
--- a/tests/libslic3r/test_placeholder_parser.cpp
+++ b/tests/libslic3r/test_placeholder_parser.cpp
@@ -51,6 +51,20 @@ SCENARIO("Placeholder parser scripting", "[PlaceholderParser]") {
     SECTION("math: max(13.4, -1238.1)") { REQUIRE(std::stod(parser.process("{max(13.4, -1238.1)}")) == Approx(13.4)); }
     SECTION("math: int(13.4)") { REQUIRE(parser.process("{int(13.4)}") == "13"); }
     SECTION("math: int(-13.4)") { REQUIRE(parser.process("{int(-13.4)}") == "-13"); }
+    SECTION("math: round(13.4)") { REQUIRE(parser.process("{round(13.4)}") == "13"); }
+    SECTION("math: round(-13.4)") { REQUIRE(parser.process("{round(-13.4)}") == "-13"); }
+    SECTION("math: round(13.6)") { REQUIRE(parser.process("{round(13.6)}") == "14"); }
+    SECTION("math: round(-13.6)") { REQUIRE(parser.process("{round(-13.6)}") == "-14"); }
+    SECTION("math: digits(5, 15)") { REQUIRE(parser.process("{digits(5, 15)}") == "              5"); }
+    SECTION("math: digits(5., 15)") { REQUIRE(parser.process("{digits(5., 15)}") == "              5"); }
+    SECTION("math: zdigits(5, 15)") { REQUIRE(parser.process("{zdigits(5, 15)}") == "000000000000005"); }
+    SECTION("math: zdigits(5., 15)") { REQUIRE(parser.process("{zdigits(5., 15)}") == "000000000000005"); }
+    SECTION("math: digits(5, 15, 8)") { REQUIRE(parser.process("{digits(5, 15, 8)}") == "     5.00000000"); }
+    SECTION("math: digits(5., 15, 8)") { REQUIRE(parser.process("{digits(5, 15, 8)}") == "     5.00000000"); }
+    SECTION("math: zdigits(5, 15, 8)") { REQUIRE(parser.process("{zdigits(5, 15, 8)}") == "000005.00000000"); }
+    SECTION("math: zdigits(5., 15, 8)") { REQUIRE(parser.process("{zdigits(5, 15, 8)}") == "000005.00000000"); }
+    SECTION("math: digits(13.84375892476, 15, 8)") { REQUIRE(parser.process("{digits(13.84375892476, 15, 8)}") == "    13.84375892"); }
+    SECTION("math: zdigits(13.84375892476, 15, 8)") { REQUIRE(parser.process("{zdigits(13.84375892476, 15, 8)}") == "000013.84375892"); }
 
     // Test the "coFloatOrPercent" and "xxx_extrusion_width" substitutions.
     // first_layer_extrusion_width ratio_over first_layer_heigth.