├── CMakeLists.txt ├── DownloadProject ├── DownloadProject.CMakeLists.cmake.in ├── DownloadProject.cmake └── LICENSE ├── LICENSE ├── README.md ├── unformat.h ├── unformat_benchmark.cpp └── unformat_test.cpp /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.0) 2 | 3 | project(Unformat) 4 | 5 | set(CMAKE_CXX_STANDARD 11) 6 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 7 | 8 | include(DownloadProject/DownloadProject.cmake) 9 | 10 | # Functional test 11 | download_project(PROJ googletest 12 | GIT_REPOSITORY https://github.com/google/googletest.git 13 | GIT_TAG release-1.8.0 14 | ${UPDATE_DISCONNECTED_IF_AVAILABLE} 15 | ) 16 | # Prevent GoogleTest from overriding our compiler/linker options 17 | # when building with Visual Studio 18 | set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) 19 | add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR}) 20 | add_executable(unformat_test unformat.h unformat_test.cpp) 21 | target_link_libraries(unformat_test gtest gmock_main) 22 | 23 | # Benchmark 24 | if (CMAKE_VERSION VERSION_LESS 3.2) 25 | set(UPDATE_DISCONNECTED_IF_AVAILABLE "") 26 | else() 27 | set(UPDATE_DISCONNECTED_IF_AVAILABLE "UPDATE_DISCONNECTED 1") 28 | endif() 29 | 30 | download_project(PROJ benchmark 31 | GIT_REPOSITORY https://github.com/google/benchmark.git 32 | GIT_TAG v1.3.0 33 | ${UPDATE_DISCONNECTED_IF_AVAILABLE} 34 | ) 35 | 36 | set(BENCHMARK_ENABLE_TESTING OFF) 37 | add_subdirectory(${benchmark_SOURCE_DIR} ${benchmark_BINARY_DIR}) 38 | add_executable(unformat_benchmark unformat.h unformat_benchmark.cpp) 39 | target_link_libraries(unformat_benchmark benchmark) -------------------------------------------------------------------------------- /DownloadProject/DownloadProject.CMakeLists.cmake.in: -------------------------------------------------------------------------------- 1 | # Distributed under the OSI-approved MIT License. See accompanying 2 | # file LICENSE or https://github.com/Crascit/DownloadProject for details. 3 | 4 | cmake_minimum_required(VERSION 2.8.2) 5 | 6 | project(${DL_ARGS_PROJ}-download NONE) 7 | 8 | include(ExternalProject) 9 | ExternalProject_Add(${DL_ARGS_PROJ}-download 10 | ${DL_ARGS_UNPARSED_ARGUMENTS} 11 | SOURCE_DIR "${DL_ARGS_SOURCE_DIR}" 12 | BINARY_DIR "${DL_ARGS_BINARY_DIR}" 13 | CONFIGURE_COMMAND "" 14 | BUILD_COMMAND "" 15 | INSTALL_COMMAND "" 16 | TEST_COMMAND "" 17 | ) 18 | -------------------------------------------------------------------------------- /DownloadProject/DownloadProject.cmake: -------------------------------------------------------------------------------- 1 | # Distributed under the OSI-approved MIT License. See accompanying 2 | # file LICENSE or https://github.com/Crascit/DownloadProject for details. 3 | # 4 | # MODULE: DownloadProject 5 | # 6 | # PROVIDES: 7 | # download_project( PROJ projectName 8 | # [PREFIX prefixDir] 9 | # [DOWNLOAD_DIR downloadDir] 10 | # [SOURCE_DIR srcDir] 11 | # [BINARY_DIR binDir] 12 | # [QUIET] 13 | # ... 14 | # ) 15 | # 16 | # Provides the ability to download and unpack a tarball, zip file, git repository, 17 | # etc. at configure time (i.e. when the cmake command is run). How the downloaded 18 | # and unpacked contents are used is up to the caller, but the motivating case is 19 | # to download source code which can then be included directly in the build with 20 | # add_subdirectory() after the call to download_project(). Source and build 21 | # directories are set up with this in mind. 22 | # 23 | # The PROJ argument is required. The projectName value will be used to construct 24 | # the following variables upon exit (obviously replace projectName with its actual 25 | # value): 26 | # 27 | # projectName_SOURCE_DIR 28 | # projectName_BINARY_DIR 29 | # 30 | # The SOURCE_DIR and BINARY_DIR arguments are optional and would not typically 31 | # need to be provided. They can be specified if you want the downloaded source 32 | # and build directories to be located in a specific place. The contents of 33 | # projectName_SOURCE_DIR and projectName_BINARY_DIR will be populated with the 34 | # locations used whether you provide SOURCE_DIR/BINARY_DIR or not. 35 | # 36 | # The DOWNLOAD_DIR argument does not normally need to be set. It controls the 37 | # location of the temporary CMake build used to perform the download. 38 | # 39 | # The PREFIX argument can be provided to change the base location of the default 40 | # values of DOWNLOAD_DIR, SOURCE_DIR and BINARY_DIR. If all of those three arguments 41 | # are provided, then PREFIX will have no effect. The default value for PREFIX is 42 | # CMAKE_BINARY_DIR. 43 | # 44 | # The QUIET option can be given if you do not want to show the output associated 45 | # with downloading the specified project. 46 | # 47 | # In addition to the above, any other options are passed through unmodified to 48 | # ExternalProject_Add() to perform the actual download, patch and update steps. 49 | # The following ExternalProject_Add() options are explicitly prohibited (they 50 | # are reserved for use by the download_project() command): 51 | # 52 | # CONFIGURE_COMMAND 53 | # BUILD_COMMAND 54 | # INSTALL_COMMAND 55 | # TEST_COMMAND 56 | # 57 | # Only those ExternalProject_Add() arguments which relate to downloading, patching 58 | # and updating of the project sources are intended to be used. Also note that at 59 | # least one set of download-related arguments are required. 60 | # 61 | # If using CMake 3.2 or later, the UPDATE_DISCONNECTED option can be used to 62 | # prevent a check at the remote end for changes every time CMake is run 63 | # after the first successful download. See the documentation of the ExternalProject 64 | # module for more information. It is likely you will want to use this option if it 65 | # is available to you. Note, however, that the ExternalProject implementation contains 66 | # bugs which result in incorrect handling of the UPDATE_DISCONNECTED option when 67 | # using the URL download method or when specifying a SOURCE_DIR with no download 68 | # method. Fixes for these have been created, the last of which is scheduled for 69 | # inclusion in CMake 3.8.0. Details can be found here: 70 | # 71 | # https://gitlab.kitware.com/cmake/cmake/commit/bdca68388bd57f8302d3c1d83d691034b7ffa70c 72 | # https://gitlab.kitware.com/cmake/cmake/issues/16428 73 | # 74 | # If you experience build errors related to the update step, consider avoiding 75 | # the use of UPDATE_DISCONNECTED. 76 | # 77 | # EXAMPLE USAGE: 78 | # 79 | # include(DownloadProject) 80 | # download_project(PROJ googletest 81 | # GIT_REPOSITORY https://github.com/google/googletest.git 82 | # GIT_TAG master 83 | # UPDATE_DISCONNECTED 1 84 | # QUIET 85 | # ) 86 | # 87 | # add_subdirectory(${googletest_SOURCE_DIR} ${googletest_BINARY_DIR}) 88 | # 89 | #======================================================================================== 90 | 91 | 92 | set(_DownloadProjectDir "${CMAKE_CURRENT_LIST_DIR}") 93 | 94 | include(CMakeParseArguments) 95 | 96 | function(download_project) 97 | 98 | set(options QUIET) 99 | set(oneValueArgs 100 | PROJ 101 | PREFIX 102 | DOWNLOAD_DIR 103 | SOURCE_DIR 104 | BINARY_DIR 105 | # Prevent the following from being passed through 106 | CONFIGURE_COMMAND 107 | BUILD_COMMAND 108 | INSTALL_COMMAND 109 | TEST_COMMAND 110 | ) 111 | set(multiValueArgs "") 112 | 113 | cmake_parse_arguments(DL_ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) 114 | 115 | # Hide output if requested 116 | if (DL_ARGS_QUIET) 117 | set(OUTPUT_QUIET "OUTPUT_QUIET") 118 | else() 119 | unset(OUTPUT_QUIET) 120 | message(STATUS "Downloading/updating ${DL_ARGS_PROJ}") 121 | endif() 122 | 123 | # Set up where we will put our temporary CMakeLists.txt file and also 124 | # the base point below which the default source and binary dirs will be 125 | if (NOT DL_ARGS_PREFIX) 126 | set(DL_ARGS_PREFIX "${CMAKE_BINARY_DIR}") 127 | endif() 128 | if (NOT DL_ARGS_DOWNLOAD_DIR) 129 | set(DL_ARGS_DOWNLOAD_DIR "${DL_ARGS_PREFIX}/${DL_ARGS_PROJ}-download") 130 | endif() 131 | 132 | # Ensure the caller can know where to find the source and build directories 133 | if (NOT DL_ARGS_SOURCE_DIR) 134 | set(DL_ARGS_SOURCE_DIR "${DL_ARGS_PREFIX}/${DL_ARGS_PROJ}-src") 135 | endif() 136 | if (NOT DL_ARGS_BINARY_DIR) 137 | set(DL_ARGS_BINARY_DIR "${DL_ARGS_PREFIX}/${DL_ARGS_PROJ}-build") 138 | endif() 139 | set(${DL_ARGS_PROJ}_SOURCE_DIR "${DL_ARGS_SOURCE_DIR}" PARENT_SCOPE) 140 | set(${DL_ARGS_PROJ}_BINARY_DIR "${DL_ARGS_BINARY_DIR}" PARENT_SCOPE) 141 | 142 | # The way that CLion manages multiple configurations, it causes a copy of 143 | # the CMakeCache.txt to be copied across due to it not expecting there to 144 | # be a project within a project. This causes the hard-coded paths in the 145 | # cache to be copied and builds to fail. To mitigate this, we simply 146 | # remove the cache if it exists before we configure the new project. It 147 | # is safe to do so because it will be re-generated. Since this is only 148 | # executed at the configure step, it should not cause additional builds or 149 | # downloads. 150 | file(REMOVE "${DL_ARGS_DOWNLOAD_DIR}/CMakeCache.txt") 151 | 152 | # Create and build a separate CMake project to carry out the download. 153 | # If we've already previously done these steps, they will not cause 154 | # anything to be updated, so extra rebuilds of the project won't occur. 155 | # Make sure to pass through CMAKE_MAKE_PROGRAM in case the main project 156 | # has this set to something not findable on the PATH. 157 | configure_file("${_DownloadProjectDir}/DownloadProject.CMakeLists.cmake.in" 158 | "${DL_ARGS_DOWNLOAD_DIR}/CMakeLists.txt") 159 | execute_process(COMMAND ${CMAKE_COMMAND} -G "${CMAKE_GENERATOR}" 160 | -D "CMAKE_MAKE_PROGRAM:FILE=${CMAKE_MAKE_PROGRAM}" 161 | . 162 | RESULT_VARIABLE result 163 | ${OUTPUT_QUIET} 164 | WORKING_DIRECTORY "${DL_ARGS_DOWNLOAD_DIR}" 165 | ) 166 | if(result) 167 | message(FATAL_ERROR "CMake step for ${DL_ARGS_PROJ} failed: ${result}") 168 | endif() 169 | execute_process(COMMAND ${CMAKE_COMMAND} --build . 170 | RESULT_VARIABLE result 171 | ${OUTPUT_QUIET} 172 | WORKING_DIRECTORY "${DL_ARGS_DOWNLOAD_DIR}" 173 | ) 174 | if(result) 175 | message(FATAL_ERROR "Build step for ${DL_ARGS_PROJ} failed: ${result}") 176 | endif() 177 | 178 | endfunction() 179 | -------------------------------------------------------------------------------- /DownloadProject/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Crascit 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **[Source available on GitHub](https://github.com/adamyaxley/Unformat)** 2 | 3 | # Unformat 4 | Parsing and extraction of original data from brace style "{}" formatted strings. It basically _unformats_ what you thought was formatted for good. 5 | 6 | ## Quick Example 7 | Unformat is simple to use and works on all basic types. See the below example for extracting a `std::string` and an 'int' 8 | ```c++ 9 | std::string name; 10 | int age; 11 | 12 | unformat("Harry is 18 years old.", "{} is {} years old.", name, age); 13 | // name == "Harry" and age == 18 14 | ``` 15 | As an optimisation, if the format string is known at compile time, it can be parsed into a constant expression by making use of `ay::make_format`. In tests, this increases runtime speed by a factor of 3 or 4. 16 | ```c++ 17 | std::string name; 18 | int age; 19 | 20 | constexpr auto format = ay::make_format("{} is {} years old."); 21 | unformat("Harry is 18 years old.", format, name, age); 22 | // name == "Harry" and age == 18 23 | ``` 24 | 25 | ## How do I use this library? 26 | Unformat is a single-file header only library so integration is easy. All you need to do is copy `unformat.h` into your project, and away you go. 27 | 28 | ## Public Domain 29 | This software is completely open source and in the public domain. See LICENSE for details. 30 | 31 | ## Contributing 32 | Pull requests are very welcome. You may also create Issues and I will have a look into it as soon as I can. 33 | 34 | ## Speed 35 | Unformat is super awesome back to the future style lightning fast compared to traditional parsing methods. Below is the output from Google Benchmark on unformat_benchmark.cpp. Great Scott! 36 | ``` 37 | Run on (16 X 2993 MHz CPU s) 38 | 03/13/19 18:10:57 39 | -------------------------------------------------------------------- 40 | Benchmark Time CPU Iterations 41 | -------------------------------------------------------------------- 42 | Unformat 72 ns 71 ns 8960000 43 | Unformat_ConstChar 69 ns 70 ns 8960000 44 | Unformat_ConstexprMakeFormat 36 ns 35 ns 19478261 45 | StdStringStream 844 ns 854 ns 896000 46 | StdRegex 9975 ns 10010 ns 64000 47 | StdScanf 1716 ns 1726 ns 407273 48 | ``` 49 | -------------------------------------------------------------------------------- /unformat.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #ifdef _MSC_VER 4 | #if _MSC_VER >= 1910 && _HAS_CXX17 5 | #define UNFORMAT_CPP17 6 | #endif 7 | #endif 8 | 9 | #include 10 | #ifdef UNFORMAT_CPP17 11 | #include 12 | #endif 13 | 14 | namespace 15 | { 16 | template 17 | void unformat_signed_int(const char* input, const char* inputEnd, T& output) noexcept 18 | { 19 | output = 0; 20 | int sign = 1; 21 | if (*input == '-') 22 | { 23 | sign = -1; 24 | ++input; 25 | } 26 | 27 | #ifndef UNFORMAT_DISABLE_PLUS_SIGN 28 | // Ignore plus 29 | if (*input == '+') 30 | { 31 | ++input; 32 | } 33 | #endif 34 | 35 | while (input != inputEnd) 36 | { 37 | output *= 10; 38 | output += (*input - '0'); 39 | ++input; 40 | } 41 | output *= sign; 42 | } 43 | 44 | template 45 | void unformat_unsigned_int(const char* input, const char* inputEnd, T& output) noexcept 46 | { 47 | output = 0; 48 | while (input != inputEnd) 49 | { 50 | output *= 10; 51 | output += (*input - '0'); 52 | ++input; 53 | } 54 | } 55 | 56 | template 57 | void unformat_real(const char* input, const char* inputEnd, T& output) noexcept 58 | { 59 | long double f = 0; 60 | 61 | // Check for negative 62 | if (*input == '-') 63 | { 64 | f = -f; 65 | ++input; 66 | } 67 | 68 | #ifndef UNFORMAT_DISABLE_PLUS_SIGN 69 | // Ignore plus 70 | if (*input == '+') 71 | { 72 | ++input; 73 | } 74 | #endif 75 | 76 | // Parse units 77 | while (*input != '.' && input != inputEnd && *input != 'e') 78 | { 79 | f *= 10; 80 | f += (*input - '0'); 81 | ++input; 82 | } 83 | 84 | // Parse decimal 85 | if (*input == '.') 86 | { 87 | ++input; 88 | long double decimal = 1.0L; 89 | while (input != inputEnd && *input != 'e') 90 | { 91 | decimal *= 0.1L; 92 | f += (*input - '0') * decimal; 93 | ++input; 94 | } 95 | } 96 | 97 | // Parse exponent 98 | if (*input == 'e') 99 | { 100 | ++input; 101 | if (*input == '+') 102 | { 103 | ++input; // Skip + as unformat_signed_int can't handle it 104 | } 105 | int e; 106 | unformat_signed_int(input, inputEnd, e); 107 | f *= pow(10L, e); 108 | } 109 | 110 | output = static_cast(f); 111 | } 112 | } 113 | 114 | namespace ay 115 | { 116 | // Not defined on purpose for the general case. Note that you can define custom unformatters by defining new specialisations 117 | // for this function. 118 | template 119 | void unformat_arg(const char* input, const char* inputEnd, T& output) noexcept; 120 | 121 | template 122 | T unformat_arg(const char* input, const char* inputEnd) noexcept 123 | { 124 | T output; 125 | unformat_arg(input, inputEnd, output); 126 | return output; 127 | } 128 | 129 | #ifdef UNFORMAT_CPP17 130 | template 131 | T unformat_arg(std::string_view input) noexcept 132 | { 133 | return unformat_arg(input.data(), input.data() + input.length()); 134 | } 135 | #endif 136 | 137 | template 138 | T unformat_arg(std::string input) noexcept 139 | { 140 | return unformat_arg(input.c_str(), input.c_str() + input.length()); 141 | } 142 | 143 | template <> 144 | inline void unformat_arg(const char* input, const char* inputEnd, char& output) noexcept 145 | { 146 | output = input[0]; 147 | } 148 | 149 | template <> 150 | inline void unformat_arg(const char* input, const char* inputEnd, unsigned char& output) noexcept 151 | { 152 | output = input[0]; 153 | } 154 | 155 | #ifdef UNFORMAT_CPP17 156 | template <> 157 | inline void unformat_arg(const char* input, const char* inputEnd, std::string_view& output) noexcept 158 | { 159 | output = std::string_view(input, inputEnd - input); 160 | } 161 | #endif 162 | 163 | template <> 164 | inline void unformat_arg(const char* input, const char* inputEnd, std::string& output) noexcept 165 | { 166 | output.assign(input, inputEnd - input); 167 | } 168 | 169 | template <> 170 | inline void unformat_arg(const char* input, const char* inputEnd, float& output) noexcept 171 | { 172 | unformat_real(input, inputEnd, output); 173 | } 174 | 175 | template <> 176 | inline void unformat_arg(const char* input, const char* inputEnd, double& output) noexcept 177 | { 178 | unformat_real(input, inputEnd, output); 179 | } 180 | 181 | template <> 182 | inline void unformat_arg(const char* input, const char* inputEnd, short& output) noexcept 183 | { 184 | unformat_signed_int(input, inputEnd, output); 185 | } 186 | 187 | template <> 188 | inline void unformat_arg(const char* input, const char* inputEnd, int& output) noexcept 189 | { 190 | unformat_signed_int(input, inputEnd, output); 191 | } 192 | 193 | template <> 194 | inline void unformat_arg(const char* input, const char* inputEnd, long& output) noexcept 195 | { 196 | unformat_signed_int(input, inputEnd, output); 197 | } 198 | 199 | template <> 200 | inline void unformat_arg(const char* input, const char* inputEnd, long long& output) noexcept 201 | { 202 | unformat_signed_int(input, inputEnd, output); 203 | } 204 | 205 | template <> 206 | inline void unformat_arg(const char* input, const char* inputEnd, unsigned short& output) noexcept 207 | { 208 | unformat_unsigned_int(input, inputEnd, output); 209 | } 210 | 211 | template <> 212 | inline void unformat_arg(const char* input, const char* inputEnd, unsigned int& output) noexcept 213 | { 214 | unformat_unsigned_int(input, inputEnd, output); 215 | } 216 | 217 | template <> 218 | inline void unformat_arg(const char* input, const char* inputEnd, unsigned long& output) noexcept 219 | { 220 | unformat_unsigned_int(input, inputEnd, output); 221 | } 222 | 223 | template <> 224 | inline void unformat_arg(const char* input, const char* inputEnd, unsigned long long& output) noexcept 225 | { 226 | unformat_unsigned_int(input, inputEnd, output); 227 | } 228 | 229 | struct format; 230 | 231 | constexpr format make_format_non_template(const char* str, std::size_t N); 232 | 233 | struct format 234 | { 235 | constexpr format() {} 236 | 237 | template 238 | constexpr format(const char(&str)[N]) 239 | { 240 | *this = make_format_non_template(str, N); 241 | } 242 | 243 | format(const char* str) 244 | { 245 | *this = make_format_non_template(str, std::strlen(str)); 246 | } 247 | 248 | static constexpr std::size_t MAX_COUNT{ 16 }; 249 | std::size_t offsets[MAX_COUNT]{}; 250 | char endChar[MAX_COUNT]{}; 251 | std::size_t count{ 0 }; 252 | }; 253 | 254 | template 255 | constexpr format make_format(const char(&str)[N]) noexcept 256 | { 257 | return make_format_non_template(str, N); 258 | } 259 | 260 | constexpr format make_format_non_template(const char* str, std::size_t N) 261 | { 262 | format format; 263 | 264 | std::size_t from = 0; 265 | for (std::size_t to = 0; to < N - 1; to++) 266 | { 267 | if (str[to] == '{' && str[to + 1] == '}') 268 | { 269 | if (format.count >= format::MAX_COUNT) 270 | { 271 | // Stops compilation in a constexpr context 272 | throw std::exception("Max number of {} has been exceeded."); 273 | } 274 | format.offsets[format.count] = to - from; 275 | format.endChar[format.count] = str[to + 2]; // May be '\0' which is fine 276 | ++format.count; 277 | // Advance markers 278 | from = to + 2; 279 | to = from; 280 | } 281 | } 282 | 283 | return format; 284 | } 285 | 286 | // Empty function to end recursion, no more args to process 287 | inline void unformat_internal(std::size_t inputPos, const char* input, const format& format) noexcept 288 | { 289 | } 290 | 291 | template 292 | void unformat_internal(std::size_t inputPos, const char* input, const format& format, T& first, TRest&... rest) noexcept 293 | { 294 | const std::size_t argNo = format.count - sizeof...(rest) - 1; 295 | 296 | // Get the location of the first brace 297 | const auto offset = format.offsets[argNo]; 298 | 299 | // Find input string 300 | inputPos += offset; 301 | auto inputEnd = inputPos; 302 | while (input[inputEnd] != format.endChar[argNo]) 303 | { 304 | ++inputEnd; 305 | } 306 | 307 | // Process this arg 308 | unformat_arg(&input[inputPos], &input[inputEnd], first); 309 | 310 | // Process TRest 311 | unformat_internal(inputEnd, input, format, rest...); 312 | } 313 | 314 | // Parses and extracts data from 'input' given a braced styled "{}" 'format' into 'args...' 315 | // For example: 316 | // std::string name, int age; 317 | // constexpr auto format = make_format("{} is {} years old."); 318 | // unformat("Harry is 18 years old.", format, name, age); 319 | // 320 | // Then the following data is extracted: 321 | // name == "Harry" and age == 18 322 | template 323 | void unformat(const char* input, const format& format, Args&... args) noexcept 324 | { 325 | unformat_internal(0, input, format, args...); 326 | } 327 | 328 | // Parses and extracts data from 'input' given a braced styled "{}" 'format' into 'args...' 329 | // For example: 330 | // std::string name, int age; 331 | // constexpr auto format = make_format("{} is {} years old."); 332 | // unformat("Harry is 18 years old.", format, name, age); 333 | // 334 | // Then the following data is extracted: 335 | // name == "Harry" and age == 18 336 | template 337 | void unformat(const std::string& input, const format& format, Args&... args) noexcept 338 | { 339 | unformat_internal(0, input.c_str(), format, args...); 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /unformat_benchmark.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "unformat.h" 6 | 7 | const char* g_input = "Harry is 18 years old and weighs 67.8 kilograms"; 8 | const std::string g_inputString = g_input; 9 | 10 | #ifdef UNFORMAT_CPP17 11 | #include 12 | using string_type = std::string_view; 13 | #else 14 | using string_type = std::string; 15 | #endif 16 | 17 | static void Unformat(benchmark::State& state) 18 | { 19 | string_type name; 20 | int age; 21 | float weight; 22 | string_type units; 23 | for (auto _ : state) 24 | { 25 | ay::unformat(g_input, "{} is {} years old and weighs {} {}", name, age, weight, units); 26 | benchmark::DoNotOptimize(name); 27 | benchmark::DoNotOptimize(age); 28 | benchmark::DoNotOptimize(weight); 29 | benchmark::DoNotOptimize(units); 30 | } 31 | } 32 | 33 | BENCHMARK(Unformat); 34 | 35 | static void Unformat_ConstChar(benchmark::State& state) 36 | { 37 | string_type name; 38 | int age; 39 | float weight; 40 | string_type units; 41 | for (auto _ : state) 42 | { 43 | const char* format = "{} is {} years old and weighs {} {}"; 44 | ay::unformat(g_input, format, name, age, weight, units); 45 | benchmark::DoNotOptimize(name); 46 | benchmark::DoNotOptimize(age); 47 | benchmark::DoNotOptimize(weight); 48 | benchmark::DoNotOptimize(units); 49 | } 50 | } 51 | 52 | BENCHMARK(Unformat_ConstChar); 53 | 54 | static void Unformat_ConstexprMakeFormat(benchmark::State& state) 55 | { 56 | string_type name; 57 | int age; 58 | float weight; 59 | string_type units; 60 | for (auto _ : state) 61 | { 62 | constexpr auto format = ay::make_format("{} is {} years old and weighs {} {}"); 63 | ay::unformat(g_input, format, name, age, weight, units); 64 | benchmark::DoNotOptimize(name); 65 | benchmark::DoNotOptimize(age); 66 | benchmark::DoNotOptimize(weight); 67 | benchmark::DoNotOptimize(units); 68 | } 69 | } 70 | 71 | BENCHMARK(Unformat_ConstexprMakeFormat); 72 | 73 | static void StdStringStream(benchmark::State& state) 74 | { 75 | std::string name; 76 | int age; 77 | float weight; 78 | std::string units; 79 | for (auto _ : state) 80 | { 81 | std::istringstream stream(g_input); 82 | stream >> name; 83 | stream.ignore(2); 84 | stream >> age; 85 | stream.ignore(21); 86 | stream >> weight; 87 | stream >> units; 88 | benchmark::DoNotOptimize(name); 89 | benchmark::DoNotOptimize(age); 90 | benchmark::DoNotOptimize(weight); 91 | benchmark::DoNotOptimize(units); 92 | } 93 | } 94 | 95 | BENCHMARK(StdStringStream); 96 | 97 | static void StdRegex(benchmark::State& state) 98 | { 99 | std::string name; 100 | int age; 101 | float weight; 102 | std::string units; 103 | for (auto _ : state) 104 | { 105 | std::regex regex("([A-Za-z]+) is ([0-9]+) years old and weighs ([0-9\\.]+) ([A-Za-z]+)"); 106 | std::smatch matches; 107 | std::regex_search(g_inputString, matches, regex); 108 | 109 | name = matches[1]; 110 | age = std::stoi(matches[2]); 111 | weight = std::stof(matches[3]); 112 | units = matches[4]; 113 | benchmark::DoNotOptimize(name); 114 | benchmark::DoNotOptimize(age); 115 | benchmark::DoNotOptimize(weight); 116 | benchmark::DoNotOptimize(units); 117 | } 118 | } 119 | 120 | BENCHMARK(StdRegex); 121 | 122 | static void StdScanf(benchmark::State& state) 123 | { 124 | char name[16]; 125 | int age; 126 | float weight; 127 | char units[16]; 128 | for (auto _ : state) 129 | { 130 | std::sscanf(g_input, "%s is %d years old and weighs %f %s", &name, &age, &weight, &units); 131 | benchmark::DoNotOptimize(name); 132 | benchmark::DoNotOptimize(age); 133 | benchmark::DoNotOptimize(weight); 134 | benchmark::DoNotOptimize(units); 135 | } 136 | } 137 | 138 | BENCHMARK(StdScanf); 139 | 140 | BENCHMARK_MAIN(); -------------------------------------------------------------------------------- /unformat_test.cpp: -------------------------------------------------------------------------------- 1 | #include "unformat.h" 2 | #include "gmock/gmock.h" 3 | #include "gtest/gtest.h" 4 | #include 5 | #include 6 | 7 | TEST(Unformat, EmptyString) 8 | { 9 | { 10 | std::string output("NotEmpty"); 11 | ay::unformat("", "{}", output); 12 | ASSERT_EQ("", output); 13 | } 14 | 15 | { 16 | std::string output("NotEmpty"); 17 | ay::unformat(" ", " {}", output); 18 | ASSERT_EQ("", output); 19 | } 20 | 21 | { 22 | std::string output("NotEmpty"); 23 | ay::unformat(" ", "{} ", output); 24 | ASSERT_EQ("", output); 25 | } 26 | 27 | { 28 | std::string output("NotEmpty"); 29 | ay::unformat(" ", " {} ", output); 30 | ASSERT_EQ("", output); 31 | } 32 | } 33 | 34 | TEST(Unformat, Basic) 35 | { 36 | // Basic test to extract a single integer 37 | int output; 38 | ay::unformat("4", "{}", output); 39 | ASSERT_EQ(4, output); 40 | 41 | ay::unformat(" 5 ", " {} ", output); 42 | ASSERT_EQ(5, output); 43 | 44 | ay::unformat(" 6", " {}", output); 45 | ASSERT_EQ(6, output); 46 | 47 | ay::unformat("7 ", "{} ", output); 48 | ASSERT_EQ(7, output); 49 | } 50 | 51 | TEST(Unformat, TwoVars) 52 | { 53 | std::string name; 54 | int age; 55 | ay::unformat("Harry is 18 years old.", "{} is {} years old.", name, age); 56 | ASSERT_EQ(std::string("Harry"), name); 57 | ASSERT_EQ(18, age); 58 | } 59 | 60 | TEST(Unformat, ThreeVars) 61 | { 62 | std::string name; 63 | int weight; 64 | std::string supporting; 65 | ay::unformat("gpmvdo (78) -> jjixrr, zacrh, smylfq, fdvtn", "{} ({}) -> {}", name, weight, supporting); 66 | ASSERT_EQ(std::string("gpmvdo"), name); 67 | ASSERT_EQ(std::string("jjixrr, zacrh, smylfq, fdvtn"), supporting); 68 | ASSERT_EQ(78, weight); 69 | } 70 | 71 | namespace 72 | { 73 | constexpr static auto FUZZ_COUNT = 10000; 74 | 75 | template 76 | void fuzzReal() 77 | { 78 | fuzz>(); 79 | } 80 | 81 | template 82 | void fuzzInt() 83 | { 84 | fuzz>(); 85 | } 86 | 87 | template 88 | void fuzz() 89 | { 90 | std::mt19937 engine; 91 | DISTRIBUTION dist(std::numeric_limits::min(), std::numeric_limits::max()); 92 | constexpr auto format = ay::make_format("{}"); 93 | 94 | for (int i = 0; i < FUZZ_COUNT; i++) 95 | { 96 | const auto input = dist(engine); 97 | T output; 98 | 99 | std::stringstream stream; 100 | stream << input; 101 | ay::unformat(stream.str().c_str(), format, output); 102 | T streamOutput; 103 | stream >> streamOutput; 104 | ASSERT_EQ(streamOutput, output) << i << ": Test that unformat and std::stringstream are equal at default precision"; 105 | 106 | ay::unformat(std::to_string(input).c_str(), format, output); 107 | ASSERT_EQ(input, output) << i << ": Test that unformat is the exact lossless opposite of std::to_string"; 108 | } 109 | } 110 | } 111 | 112 | TEST(Unformat, FuzzFloat) 113 | { 114 | fuzzReal(); 115 | } 116 | 117 | TEST(Unformat, FuzzDouble) 118 | { 119 | fuzzReal(); 120 | } 121 | 122 | TEST(Unformat, FuzzShort) 123 | { 124 | fuzzInt(); 125 | } 126 | 127 | TEST(Unformat, FuzzUnsignedShort) 128 | { 129 | fuzzInt(); 130 | } 131 | 132 | TEST(Unformat, FuzzInt) 133 | { 134 | fuzzInt(); 135 | } 136 | 137 | TEST(Unformat, FuzzUnsignedInt) 138 | { 139 | fuzzInt(); 140 | } 141 | 142 | TEST(Unformat, FuzzLong) 143 | { 144 | fuzzInt(); 145 | } 146 | 147 | TEST(Unformat, FuzzUnsignedLong) 148 | { 149 | fuzzInt(); 150 | } 151 | 152 | TEST(Unformat, FuzzLongLong) 153 | { 154 | fuzzInt(); 155 | } 156 | 157 | TEST(Unformat, FuzzUnsignedLongLong) 158 | { 159 | fuzzInt(); 160 | } --------------------------------------------------------------------------------