| [1052] | 1 | /* | 
|---|
 | 2 |  *   ORXONOX - the hottest 3D action shooter ever to exist | 
|---|
| [1505] | 3 |  *                    > www.orxonox.net < | 
|---|
| [1052] | 4 |  * | 
|---|
 | 5 |  * | 
|---|
 | 6 |  *   License notice: | 
|---|
 | 7 |  * | 
|---|
 | 8 |  *   This program is free software; you can redistribute it and/or | 
|---|
 | 9 |  *   modify it under the terms of the GNU General Public License | 
|---|
 | 10 |  *   as published by the Free Software Foundation; either version 2 | 
|---|
 | 11 |  *   of the License, or (at your option) any later version. | 
|---|
 | 12 |  * | 
|---|
 | 13 |  *   This program is distributed in the hope that it will be useful, | 
|---|
 | 14 |  *   but WITHOUT ANY WARRANTY; without even the implied warranty of | 
|---|
 | 15 |  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the | 
|---|
 | 16 |  *   GNU General Public License for more details. | 
|---|
 | 17 |  * | 
|---|
 | 18 |  *   You should have received a copy of the GNU General Public License | 
|---|
 | 19 |  *   along with this program; if not, write to the Free Software | 
|---|
 | 20 |  *   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA. | 
|---|
 | 21 |  * | 
|---|
 | 22 |  *   Author: | 
|---|
| [2087] | 23 |  *      Reto Grieder | 
|---|
| [1791] | 24 |  *      Fabian 'x3n' Landau | 
|---|
| [1052] | 25 |  *      Benjamin Grauer | 
|---|
| [1505] | 26 |  *   Co-authors: | 
|---|
| [1052] | 27 |  *      ... | 
|---|
 | 28 |  */ | 
|---|
 | 29 |  | 
|---|
 | 30 | /*! | 
|---|
| [2087] | 31 |     @file | 
|---|
| [1052] | 32 |     @brief Definition and Implementation of the Convert class. | 
|---|
 | 33 | */ | 
|---|
 | 34 |  | 
|---|
| [2087] | 35 | #ifndef _Converter_H__ | 
|---|
 | 36 | #define _Converter_H__ | 
|---|
| [1052] | 37 |  | 
|---|
| [1062] | 38 | #include "UtilPrereqs.h" | 
|---|
 | 39 |  | 
|---|
| [1052] | 40 | #include <string> | 
|---|
 | 41 | #include <sstream> | 
|---|
| [1837] | 42 | #include <typeinfo> | 
|---|
| [1052] | 43 |  | 
|---|
| [1747] | 44 | #include "Debug.h" | 
|---|
| [3280] | 45 | #include "StringUtils.h" | 
|---|
| [3232] | 46 | #include "TemplateUtils.h" | 
|---|
| [1064] | 47 |  | 
|---|
| [2087] | 48 | //////////////////////////////////// | 
|---|
 | 49 | //// ACTUAL CONVERSION SEQUENCE //// | 
|---|
 | 50 | //////////////////////////////////// | 
|---|
 | 51 | /* | 
|---|
 | 52 |     There is a distinct priority when choosing the right conversion function: | 
|---|
 | 53 |     Overwrite: | 
|---|
 | 54 |     1. (Partial) template specialisation of ConverterExplicit::convert() | 
|---|
 | 55 |     Fallbacks: | 
|---|
 | 56 |     2. Any possible implicit conversion. This includes 'FooBar' --> 'int' if FooBar defines operator float(). | 
|---|
 | 57 |     3. Global or member operators for stringstream when converting from or to std::string (or FROM const char*) | 
|---|
 | 58 |     4. (Partial) template specialisation of ConverterFallback::convert() | 
|---|
 | 59 |     5. Function that simply displays "Could not convert value" with type information obtained from typeid(). | 
|---|
| [1505] | 60 |  | 
|---|
| [2087] | 61 |     Notes: | 
|---|
 | 62 |     There has to be an exact type match when using template specialisations. | 
|---|
 | 63 |     Template specialisations can be defined after including this file. Any implicit cast function or iostream | 
|---|
 | 64 |     operator has to be declared BEFORE this file gets parsed. | 
|---|
| [1505] | 65 |  | 
|---|
| [2087] | 66 |     Defining your own functions: | 
|---|
 | 67 |     There are obviously 4 ways to specifiy a user defined conversion. What should I use? | 
|---|
| [1505] | 68 |  | 
|---|
| [2087] | 69 |     Usually, ConverterFallback fits quite well. You won't have to deal with the conversion from | 
|---|
 | 70 |     'MyClass' to 'MyClass' by using another explicit template specialisation to avoid ambiguities. | 
|---|
| [1505] | 71 |  | 
|---|
| [2087] | 72 |     However if you want to overwrite an implicit conversion or an iostream operator, you really need to | 
|---|
 | 73 |     make use of ConverterExplicit. | 
|---|
 | 74 | */ | 
|---|
 | 75 |  | 
|---|
| [2171] | 76 | namespace orxonox | 
|---|
| [1505] | 77 | { | 
|---|
| [3196] | 78 |     namespace detail | 
|---|
| [2171] | 79 |     { | 
|---|
 | 80 |         //! Little template that maps integers to entire types (Alexandrescu 2001) | 
|---|
 | 81 |         template <int I> | 
|---|
 | 82 |         struct Int2Type { }; | 
|---|
 | 83 |     } | 
|---|
| [1505] | 84 |  | 
|---|
 | 85 |  | 
|---|
| [2171] | 86 |     /////////////////// | 
|---|
 | 87 |     // No Conversion // | 
|---|
 | 88 |     /////////////////// | 
|---|
| [1505] | 89 |  | 
|---|
| [2171] | 90 |     // Default template. No conversion available at all. | 
|---|
 | 91 |     template <class FromType, class ToType> | 
|---|
 | 92 |     struct ConverterFallback | 
|---|
| [1505] | 93 |     { | 
|---|
| [3196] | 94 |         FORCEINLINE static bool convert(ToType* output, const FromType& input) | 
|---|
| [2171] | 95 |         { | 
|---|
 | 96 |             COUT(2) << "Could not convert value of type " << typeid(FromType).name() | 
|---|
 | 97 |                     << " to type " << typeid(ToType).name() << std::endl; | 
|---|
 | 98 |             return false; | 
|---|
 | 99 |         } | 
|---|
 | 100 |     }; | 
|---|
| [2087] | 101 |  | 
|---|
| [2171] | 102 |     // If all else fails, try a dynamic_cast for pointer types. | 
|---|
 | 103 |     template <class FromType, class ToType> | 
|---|
 | 104 |     struct ConverterFallback<FromType*, ToType*> | 
|---|
| [1505] | 105 |     { | 
|---|
| [3196] | 106 |         FORCEINLINE static bool convert(ToType** output, FromType* const input) | 
|---|
| [2087] | 107 |         { | 
|---|
| [2171] | 108 |             ToType* temp = dynamic_cast<ToType*>(input); | 
|---|
 | 109 |             if (temp) | 
|---|
 | 110 |             { | 
|---|
 | 111 |                 *output = temp; | 
|---|
 | 112 |                 return true; | 
|---|
 | 113 |             } | 
|---|
 | 114 |             else | 
|---|
 | 115 |                 return false; | 
|---|
| [2087] | 116 |         } | 
|---|
| [2171] | 117 |     }; | 
|---|
 | 118 | } | 
|---|
| [1505] | 119 |  | 
|---|
 | 120 |  | 
|---|
| [2087] | 121 | /////////////////////// | 
|---|
 | 122 | // ConverterFallback // | 
|---|
 | 123 | /////////////////////// | 
|---|
 | 124 |  | 
|---|
 | 125 | // Default template for stringstream | 
|---|
 | 126 | template <class FromType, class ToType> | 
|---|
 | 127 | struct ConverterStringStream | 
|---|
| [1505] | 128 | { | 
|---|
| [3196] | 129 |     FORCEINLINE static bool convert(ToType* output, const FromType& input) | 
|---|
| [1505] | 130 |     { | 
|---|
| [2171] | 131 |         return orxonox::ConverterFallback<FromType, ToType>::convert(output, input); | 
|---|
| [1505] | 132 |     } | 
|---|
 | 133 | }; | 
|---|
 | 134 |  | 
|---|
 | 135 |  | 
|---|
 | 136 | ///////////// | 
|---|
| [2087] | 137 | // OStream // | 
|---|
| [1505] | 138 | ///////////// | 
|---|
 | 139 |  | 
|---|
| [2087] | 140 | namespace fallbackTemplates | 
|---|
| [1505] | 141 | { | 
|---|
| [2087] | 142 |     template <class FromType> | 
|---|
| [3196] | 143 |     FORCEINLINE bool operator <<(std::ostream& outstream,  const FromType& input) | 
|---|
| [2087] | 144 |     { | 
|---|
 | 145 |         std::string temp; | 
|---|
| [2171] | 146 |         if (orxonox::ConverterFallback<FromType, std::string>::convert(&temp, input)) | 
|---|
| [2087] | 147 |         { | 
|---|
 | 148 |             std::operator <<(outstream, temp); | 
|---|
 | 149 |             return true; | 
|---|
 | 150 |         } | 
|---|
 | 151 |         else | 
|---|
 | 152 |             return false; | 
|---|
 | 153 |     } | 
|---|
 | 154 | } | 
|---|
| [1505] | 155 |  | 
|---|
| [2087] | 156 | // template that evaluates whether we can convert to std::string via ostringstream | 
|---|
| [1505] | 157 | template <class FromType> | 
|---|
| [2087] | 158 | struct ConverterStringStream<FromType, std::string> | 
|---|
| [1505] | 159 | { | 
|---|
| [3196] | 160 |     FORCEINLINE static bool convert(std::string* output, const FromType& input) | 
|---|
| [1505] | 161 |     { | 
|---|
| [2087] | 162 |         using namespace fallbackTemplates; | 
|---|
 | 163 |         // this operator call only chooses fallbackTemplates::operator<< if there's no other fitting function | 
|---|
| [1505] | 164 |         std::ostringstream oss; | 
|---|
 | 165 |         if (oss << input) | 
|---|
 | 166 |         { | 
|---|
 | 167 |             (*output) = oss.str(); | 
|---|
 | 168 |             return true; | 
|---|
 | 169 |         } | 
|---|
 | 170 |         else | 
|---|
 | 171 |             return false; | 
|---|
 | 172 |     } | 
|---|
 | 173 | }; | 
|---|
 | 174 |  | 
|---|
| [2087] | 175 |  | 
|---|
 | 176 | ///////////// | 
|---|
 | 177 | // IStream // | 
|---|
 | 178 | ///////////// | 
|---|
 | 179 |  | 
|---|
 | 180 | namespace fallbackTemplates | 
|---|
| [1625] | 181 | { | 
|---|
| [2087] | 182 |     template <class ToType> | 
|---|
| [3196] | 183 |     FORCEINLINE bool operator >>(std::istream& instream, ToType& output) | 
|---|
| [2087] | 184 |     { | 
|---|
| [2171] | 185 |         return orxonox::ConverterFallback<std::string, ToType> | 
|---|
| [2087] | 186 |             ::convert(&output, static_cast<std::istringstream&>(instream).str()); | 
|---|
 | 187 |     } | 
|---|
| [1625] | 188 | } | 
|---|
 | 189 |  | 
|---|
| [2087] | 190 | // template that evaluates whether we can convert from std::string via ostringstream | 
|---|
| [1505] | 191 | template <class ToType> | 
|---|
| [2087] | 192 | struct ConverterStringStream<std::string, ToType> | 
|---|
| [1505] | 193 | { | 
|---|
| [3196] | 194 |     FORCEINLINE static bool convert(ToType* output, const std::string& input) | 
|---|
| [1505] | 195 |     { | 
|---|
| [2087] | 196 |         using namespace fallbackTemplates; | 
|---|
| [1505] | 197 |         std::istringstream iss(input); | 
|---|
| [2087] | 198 |         // this operator call only chooses fallbackTemplates::operator>> if there's no other fitting function | 
|---|
| [1505] | 199 |         if (iss >> (*output)) | 
|---|
| [2087] | 200 |         { | 
|---|
| [1505] | 201 |             return true; | 
|---|
| [2087] | 202 |         } | 
|---|
| [1505] | 203 |         else | 
|---|
 | 204 |             return false; | 
|---|
 | 205 |     } | 
|---|
 | 206 | }; | 
|---|
 | 207 |  | 
|---|
| [2171] | 208 | namespace orxonox | 
|---|
| [1625] | 209 | { | 
|---|
| [1505] | 210 |  | 
|---|
| [2171] | 211 |     /////////////////// | 
|---|
 | 212 |     // Implicit Cast // | 
|---|
 | 213 |     /////////////////// | 
|---|
| [1625] | 214 |  | 
|---|
| [2171] | 215 |     // implicit cast not possible, try stringstream conversion next | 
|---|
 | 216 |     template <class FromType, class ToType> | 
|---|
| [3196] | 217 |     FORCEINLINE bool convertImplicitely(ToType* output, const FromType& input, detail::Int2Type<false>) | 
|---|
| [1505] | 218 |     { | 
|---|
| [2171] | 219 |         return ConverterStringStream<FromType, ToType>::convert(output, input); | 
|---|
| [1505] | 220 |     } | 
|---|
 | 221 |  | 
|---|
| [2171] | 222 |     // We can cast implicitely | 
|---|
 | 223 |     template <class FromType, class ToType> | 
|---|
| [3196] | 224 |     FORCEINLINE bool convertImplicitely(ToType* output, const FromType& input, detail::Int2Type<true>) | 
|---|
| [2171] | 225 |     { | 
|---|
 | 226 |         (*output) = static_cast<ToType>(input); | 
|---|
| [2087] | 227 |         return true; | 
|---|
| [1505] | 228 |     } | 
|---|
 | 229 |  | 
|---|
 | 230 |  | 
|---|
| [2171] | 231 |     //////////////////////////////// | 
|---|
 | 232 |     // ConverterExplicit Fallback // | 
|---|
 | 233 |     //////////////////////////////// | 
|---|
| [1505] | 234 |  | 
|---|
| [2171] | 235 |     // Default template if no specialisation is available | 
|---|
 | 236 |     template <class FromType, class ToType> | 
|---|
 | 237 |     struct ConverterExplicit | 
|---|
 | 238 |     { | 
|---|
| [3234] | 239 |         enum { probe = ImplicitConversion<FromType, ToType>::exists }; | 
|---|
| [3196] | 240 |         FORCEINLINE static bool convert(ToType* output, const FromType& input) | 
|---|
| [2171] | 241 |         { | 
|---|
 | 242 |             // Try implict cast and probe first. If a simple cast is not possible, it will not compile | 
|---|
 | 243 |             // We therefore have to out source it into another template function | 
|---|
| [3196] | 244 |             return convertImplicitely(output, input, detail::Int2Type<probe>()); | 
|---|
| [2171] | 245 |         } | 
|---|
 | 246 |     }; | 
|---|
| [2087] | 247 |  | 
|---|
 | 248 |  | 
|---|
| [2171] | 249 |     ////////////////////// | 
|---|
 | 250 |     // Public Functions // | 
|---|
 | 251 |     ////////////////////// | 
|---|
| [2087] | 252 |  | 
|---|
| [2171] | 253 |     /** | 
|---|
 | 254 |     @brief | 
|---|
 | 255 |         Converts any value to any other as long as there exists a conversion. | 
|---|
 | 256 |         Otherwise, the conversion will generate a runtime warning and return false. | 
|---|
 | 257 |         For information about the different conversion methods (user defined too), see the section | 
|---|
 | 258 |         'Actual conversion sequence' in this file above. | 
|---|
 | 259 |     */ | 
|---|
 | 260 |     template <class FromType, class ToType> | 
|---|
| [3196] | 261 |     FORCEINLINE bool convertValue(ToType* output, const FromType& input) | 
|---|
| [2171] | 262 |     { | 
|---|
 | 263 |         return ConverterExplicit<FromType, ToType>::convert(output, input); | 
|---|
 | 264 |     } | 
|---|
| [2087] | 265 |  | 
|---|
| [2171] | 266 |     // Calls convertValue and returns true if the conversion was successful. | 
|---|
 | 267 |     // Otherwise the fallback is used. | 
|---|
 | 268 |     /** | 
|---|
 | 269 |     @brief | 
|---|
 | 270 |         Converts any value to any other as long as there exists a conversion. | 
|---|
 | 271 |         Otherwise, the conversion will generate a runtime warning and return false. | 
|---|
 | 272 |         For information about the different conversion methods (user defined too), see the section | 
|---|
 | 273 |         'Actual conversion sequence' in this file above. | 
|---|
 | 274 |         If the conversion doesn't succeed, 'fallback' is written to '*output'. | 
|---|
 | 275 |     @param fallback | 
|---|
 | 276 |         A default value that gets written to '*output' if there is no conversion. | 
|---|
 | 277 |     */ | 
|---|
 | 278 |     template<class FromType, class ToType> | 
|---|
| [3196] | 279 |     FORCEINLINE bool convertValue(ToType* output, const FromType& input, const ToType& fallback) | 
|---|
| [1625] | 280 |     { | 
|---|
| [2171] | 281 |         if (convertValue(output, input)) | 
|---|
 | 282 |             return true; | 
|---|
 | 283 |         else | 
|---|
 | 284 |         { | 
|---|
 | 285 |             (*output) = fallback; | 
|---|
 | 286 |             return false; | 
|---|
 | 287 |         } | 
|---|
| [1625] | 288 |     } | 
|---|
 | 289 |  | 
|---|
| [2171] | 290 |     // Directly returns the converted value, even if the conversion was not successful. | 
|---|
 | 291 |     template<class FromType, class ToType> | 
|---|
| [3196] | 292 |     FORCEINLINE ToType getConvertedValue(const FromType& input) | 
|---|
| [1505] | 293 |     { | 
|---|
| [2171] | 294 |         ToType output; | 
|---|
 | 295 |         convertValue(&output, input); | 
|---|
 | 296 |         return output; | 
|---|
| [1505] | 297 |     } | 
|---|
| [2171] | 298 |  | 
|---|
 | 299 |     // Directly returns the converted value, but uses the fallback on failure. | 
|---|
 | 300 |     template<class FromType, class ToType> | 
|---|
| [3196] | 301 |     FORCEINLINE ToType getConvertedValue(const FromType& input, const ToType& fallback) | 
|---|
| [1505] | 302 |     { | 
|---|
| [2171] | 303 |         ToType output; | 
|---|
 | 304 |         convertValue(&output, input, fallback); | 
|---|
 | 305 |         return output; | 
|---|
| [1505] | 306 |     } | 
|---|
| [2171] | 307 |  | 
|---|
 | 308 |     // Like getConvertedValue, but the template argument order is in reverse. | 
|---|
 | 309 |     // That means you can call it exactly like static_cast<ToType>(fromTypeValue). | 
|---|
 | 310 |     template<class ToType, class FromType> | 
|---|
| [3196] | 311 |     FORCEINLINE ToType multi_cast(const FromType& input) | 
|---|
| [1505] | 312 |     { | 
|---|
| [2171] | 313 |         ToType output; | 
|---|
 | 314 |         convertValue(&output, input); | 
|---|
 | 315 |         return output; | 
|---|
| [1505] | 316 |     } | 
|---|
 | 317 |  | 
|---|
| [2171] | 318 |     //////////////////////////////// | 
|---|
 | 319 |     // Special string conversions // | 
|---|
 | 320 |     //////////////////////////////// | 
|---|
 | 321 |  | 
|---|
 | 322 |     // Delegate conversion from const char* to std::string | 
|---|
 | 323 |     template <class ToType> | 
|---|
 | 324 |     struct ConverterExplicit<const char*, ToType> | 
|---|
| [1625] | 325 |     { | 
|---|
| [3196] | 326 |         FORCEINLINE static bool convert(ToType* output, const char* input) | 
|---|
| [1625] | 327 |         { | 
|---|
| [2171] | 328 |             return convertValue<std::string, ToType>(output, input); | 
|---|
| [1625] | 329 |         } | 
|---|
| [2171] | 330 |     }; | 
|---|
 | 331 |  | 
|---|
 | 332 |     // These conversions would exhibit ambiguous << or >> operators when using stringstream | 
|---|
 | 333 |     template <> | 
|---|
 | 334 |     struct ConverterExplicit<char, std::string> | 
|---|
 | 335 |     { | 
|---|
| [3196] | 336 |         FORCEINLINE static bool convert(std::string* output, const char input) | 
|---|
| [1625] | 337 |         { | 
|---|
| [2171] | 338 |             *output = std::string(1, input); | 
|---|
 | 339 |             return true; | 
|---|
| [1625] | 340 |         } | 
|---|
| [2171] | 341 |     }; | 
|---|
 | 342 |     template <> | 
|---|
 | 343 |     struct ConverterExplicit<unsigned char, std::string> | 
|---|
 | 344 |     { | 
|---|
| [3196] | 345 |         FORCEINLINE static bool convert(std::string* output, const unsigned char input) | 
|---|
| [2171] | 346 |         { | 
|---|
 | 347 |             *output = std::string(1, input); | 
|---|
 | 348 |             return true; | 
|---|
 | 349 |         } | 
|---|
 | 350 |     }; | 
|---|
 | 351 |     template <> | 
|---|
 | 352 |     struct ConverterExplicit<std::string, char> | 
|---|
 | 353 |     { | 
|---|
| [3196] | 354 |         FORCEINLINE static bool convert(char* output, const std::string input) | 
|---|
| [2171] | 355 |         { | 
|---|
 | 356 |             if (input != "") | 
|---|
 | 357 |                 *output = input[0]; | 
|---|
 | 358 |             else | 
|---|
 | 359 |                 *output = '\0'; | 
|---|
 | 360 |             return true; | 
|---|
 | 361 |         } | 
|---|
 | 362 |     }; | 
|---|
 | 363 |     template <> | 
|---|
 | 364 |     struct ConverterExplicit<std::string, unsigned char> | 
|---|
 | 365 |     { | 
|---|
| [3196] | 366 |         FORCEINLINE static bool convert(unsigned char* output, const std::string input) | 
|---|
| [2171] | 367 |         { | 
|---|
 | 368 |             if (input != "") | 
|---|
 | 369 |                 *output = input[0]; | 
|---|
 | 370 |             else | 
|---|
 | 371 |                 *output = '\0'; | 
|---|
 | 372 |             return true; | 
|---|
 | 373 |         } | 
|---|
 | 374 |     }; | 
|---|
| [1625] | 375 |  | 
|---|
| [2171] | 376 |  | 
|---|
 | 377 |     // bool to std::string | 
|---|
 | 378 |     template <> | 
|---|
 | 379 |     struct ConverterExplicit<bool, std::string> | 
|---|
 | 380 |     { | 
|---|
| [3196] | 381 |         FORCEINLINE static bool convert(std::string* output, const bool& input) | 
|---|
| [2171] | 382 |         { | 
|---|
 | 383 |             if (input) | 
|---|
 | 384 |               *output = "true"; | 
|---|
 | 385 |             else | 
|---|
 | 386 |               *output = "false"; | 
|---|
| [2662] | 387 |             return true; | 
|---|
| [2171] | 388 |         } | 
|---|
 | 389 |     }; | 
|---|
| [1625] | 390 |  | 
|---|
| [2171] | 391 |     // std::string to bool | 
|---|
 | 392 |     template <> | 
|---|
 | 393 |     struct ConverterExplicit<std::string, bool> | 
|---|
 | 394 |     { | 
|---|
 | 395 |         static bool convert(bool* output, const std::string& input) | 
|---|
 | 396 |         { | 
|---|
 | 397 |             std::string stripped = getLowercase(removeTrailingWhitespaces(input)); | 
|---|
 | 398 |             if (stripped == "true" || stripped == "on" || stripped == "yes") | 
|---|
 | 399 |             { | 
|---|
 | 400 |               *output = true; | 
|---|
 | 401 |               return true; | 
|---|
 | 402 |             } | 
|---|
 | 403 |             else if (stripped == "false" || stripped == "off" || stripped == "no") | 
|---|
 | 404 |             { | 
|---|
 | 405 |               *output = false; | 
|---|
 | 406 |               return true; | 
|---|
 | 407 |             } | 
|---|
 | 408 |  | 
|---|
 | 409 |             std::istringstream iss(input); | 
|---|
 | 410 |             if (iss >> (*output)) | 
|---|
 | 411 |                 return true; | 
|---|
 | 412 |             else | 
|---|
 | 413 |                 return false; | 
|---|
 | 414 |         } | 
|---|
 | 415 |     }; | 
|---|
 | 416 | } | 
|---|
 | 417 |  | 
|---|
| [1052] | 418 | #endif /* _Convert_H__ */ | 
|---|