add join function (#204)

* add join function

* fix formatting to match single include

* add join test

* add join to documentation

* fix MSVC warning: signed/unsigned mismatch

Co-authored-by: Wim Leflere <wleflere@cochlear.com>
diff --git a/README.md b/README.md
index 92e45ad..0bf6de5 100644
--- a/README.md
+++ b/README.md
@@ -224,6 +224,10 @@
 render("{{ sort([3,2,1]) }}", data); // "[1,2,3]"
 render("{{ sort(guests) }}", data); // "[\"Jeff\", \"Patrick\", \"Tom\"]"
 
+// Join a list with a separator
+render("{{ join([1,2,3], \" + \") }}", data); // "1 + 2 + 3"
+render("{{ join(guests, \", \") }}", data); // "Jeff, Patrick, Tom"
+
 // Round numbers to a given precision
 render("{{ round(3.1415, 0) }}", data); // 3
 render("{{ round(3.1415, 3) }}", data); // 3.142
diff --git a/include/inja/function_storage.hpp b/include/inja/function_storage.hpp
index 1b6070b..b4bd092 100644
--- a/include/inja/function_storage.hpp
+++ b/include/inja/function_storage.hpp
@@ -65,6 +65,7 @@
     Sort,
     Upper,
     Super,
+    Join,
     Callback,
     ParenLeft,
     ParenRight,
@@ -109,6 +110,7 @@
     {std::make_pair("upper", 1), FunctionData { Operation::Upper }},
     {std::make_pair("super", 0), FunctionData { Operation::Super }},
     {std::make_pair("super", 1), FunctionData { Operation::Super }},
+    {std::make_pair("join", 2), FunctionData { Operation::Join }},
   };
 
 public:
diff --git a/include/inja/renderer.hpp b/include/inja/renderer.hpp
index d33cb46..ef951b9 100644
--- a/include/inja/renderer.hpp
+++ b/include/inja/renderer.hpp
@@ -528,6 +528,24 @@
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
+    case Op::Join: {
+      const auto args = get_arguments<2>(node);
+      const auto separator = args[1]->get<std::string>();
+      std::ostringstream os;
+      std::string sep;
+      for (const auto& value : *args[0]) {
+        os << sep;
+        if (value.is_string()) {
+          os << value.get<std::string>(); // otherwise the value is surrounded with ""
+        } else {
+          os << value;
+        }
+        sep = separator;
+      }
+      result_ptr = std::make_shared<json>(os.str());
+      json_tmp_stack.push_back(result_ptr);
+      json_eval_stack.push(result_ptr.get());
+    } break;
     case Op::ParenLeft:
     case Op::ParenRight:
     case Op::None:
diff --git a/single_include/inja/inja.hpp b/single_include/inja/inja.hpp
index 7fca9f3..03e19a0 100644
--- a/single_include/inja/inja.hpp
+++ b/single_include/inja/inja.hpp
@@ -1602,6 +1602,7 @@
     Sort,
     Upper,
     Super,
+    Join,
     Callback,
     ParenLeft,
     ParenRight,
@@ -1646,6 +1647,7 @@
     {std::make_pair("upper", 1), FunctionData { Operation::Upper }},
     {std::make_pair("super", 0), FunctionData { Operation::Super }},
     {std::make_pair("super", 1), FunctionData { Operation::Super }},
+    {std::make_pair("join", 2), FunctionData { Operation::Join }},
   };
 
 public:
@@ -4022,6 +4024,24 @@
       json_tmp_stack.push_back(result_ptr);
       json_eval_stack.push(result_ptr.get());
     } break;
+    case Op::Join: {
+      const auto args = get_arguments<2>(node);
+      const auto separator = args[1]->get<std::string>();
+      std::ostringstream os;
+      std::string sep;
+      for (const auto& value : *args[0]) {
+        os << sep;
+        if (value.is_string()) {
+          os << value.get<std::string>(); // otherwise the value is surrounded with ""
+        } else {
+          os << value;
+        }
+        sep = separator;
+      }
+      result_ptr = std::make_shared<json>(os.str());
+      json_tmp_stack.push_back(result_ptr);
+      json_eval_stack.push(result_ptr.get());
+    } break;
     case Op::ParenLeft:
     case Op::ParenRight:
     case Op::None:
diff --git a/test/test-functions.cpp b/test/test-functions.cpp
index e0f3a6e..4ec4c25 100644
--- a/test/test-functions.cpp
+++ b/test/test-functions.cpp
@@ -174,6 +174,11 @@
                       "[inja.exception.render_error] (at 1:22) variable 'sister' not found");
   }
 
+  SUBCASE("join") {
+    CHECK(env.render("{{ join(names, \" | \") }}", data) == "Jeff | Seb | Peter | Tom");
+    CHECK(env.render("{{ join(vars, \", \") }}", data) == "2, 3, 4, 0, -1, -2, -3");
+  }
+
   SUBCASE("isType") {
     CHECK(env.render("{{ isBoolean(is_happy) }}", data) == "true");
     CHECK(env.render("{{ isBoolean(vars) }}", data) == "false");