diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..039aff3 --- /dev/null +++ b/.clang-format @@ -0,0 +1,31 @@ +--- +# We use clang-format to keep C++ code styling consistent +# The rules for our specific style are delineated here +AccessModifierOffset: "-4" +AlignConsecutiveMacros: "true" +# AlignConsecutiveAssignments: "true" +AlignConsecutiveDeclarations: "true" +AlignEscapedNewlines: Left +AllowShortBlocksOnASingleLine: "true" +AllowShortCaseLabelsOnASingleLine: "true" +AllowShortFunctionsOnASingleLine: All +AllowShortIfStatementsOnASingleLine: WithoutElse +AllowShortLoopsOnASingleLine: "true" +AlwaysBreakAfterReturnType: TopLevelDefinitions +AlwaysBreakTemplateDeclarations: "Yes" +BreakBeforeBraces: Stroustrup +BreakBeforeTernaryOperators: "true" +BreakConstructorInitializers: AfterColon +BreakInheritanceList: AfterColon +ColumnLimit: 120 +CompactNamespaces: "true" +FixNamespaceComments: "true" +IncludeBlocks: Regroup +IndentCaseLabels: "true" +IndentPPDirectives: BeforeHash +IndentWidth: "4" +PointerAlignment: Left +NamespaceIndentation: Inner +SortIncludes: false +SortUsingDeclarations: "true" +SpaceAfterTemplateKeyword: "false" diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..fcf0b7a --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,58 @@ +Checks: "*, + -abseil-*, + -altera-*, + -android-*, + -bugprone-*, + -cert-err58-cpp, + -concurrency-mt-unsafe, + -cppcoreguidelines-avoid-const-or-ref-data-members, + -cppcoreguidelines-avoid-do-while, + -cppcoreguidelines-avoid-goto, + -cppcoreguidelines-avoid-magic-numbers, + -cppcoreguidelines-avoid-non-const-global-variables, + -cppcoreguidelines-macro-usage, + -cppcoreguidelines-non-private-member-variables-in-classes, + -cppcoreguidelines-pro-bounds-array-to-pointer-decay, + -cppcoreguidelines-pro-bounds-constant-array-index, + -cppcoreguidelines-pro-bounds-pointer-arithmetic, + -cppcoreguidelines-pro-type-reinterpret-cast, + -cppcoreguidelines-pro-type-union-access, + -cppcoreguidelines-virtual-class-destructor, + -fuchsia-*, + -google-*, + -hicpp-avoid-goto, + -hicpp-explicit-conversions, + -hicpp-function-size, + -hicpp-no-array-decay, + -hicpp-no-assembler, + -hicpp-signed-bitwise, + -hicpp-uppercase-literal-suffix, + -llvm-*, + -llvmlibc-*, + -misc-confusable-identifiers, + -misc-no-recursion, + -misc-non-private-member-variables-in-classes, + -modernize-concat-nested-namespaces, + -modernize-use-nodiscard, + -modernize-use-trailing-return-type, + -readability-avoid-const-params-in-decls, + -readability-else-after-return, + -readability-function-cognitive-complexity, + -readability-function-size, + -readability-identifier-length, + -readability-magic-numbers, + -readability-redundant-access-specifiers, + -readability-simplify-boolean-expr, + -readability-static-accessed-through-instance, + -readability-uppercase-literal-suffix', + -zircon-*" + +CheckOptions: + - key: hicpp-special-member-functions.AllowSoleDefaultDtor + value: 1 + +WarningsAsErrors: '' + +HeaderFilterRegex: '' + +FormatStyle: none diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..0d1de9f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,6 @@ +root = true + +[*.adoc] +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/.github/workflows/archive.yml b/.github/workflows/archive.yml new file mode 100644 index 0000000..40f2e77 --- /dev/null +++ b/.github/workflows/archive.yml @@ -0,0 +1,56 @@ +# Creates & releases a minimal archive of this library's core files and directories. +# This is useful for the standard CMake module `FetchContent` and friends. +name: Release library archive + +# The files & directories we archive and the name for the archive. +# The git tag for the release is either 'current' or the relase tag like '2.1.2' +env: + archive_content: LICENSE CMakeLists.txt include + archive_name: ${{ github.event.repository.name }}.zip + archive_tag: ${{github.ref == 'refs/heads/main' && 'current' || github.ref}} + +# We may overwrite the $archive_tag so need write permissions +permissions: + contents: write + +# When is the workflow run? +on: + # Any push to the main branch that changes the content of the archive. + # TODO: Perhaps we can use the $archive_content variable here? + push: + branches: + - main + paths: + - "LICENSE" + - "CMakeLists.txt" + - "include/**" + # Any formal release + release: + types: [published] + + # You can trigger the workflow manually (handy to debug the workflow). + workflow_dispatch: + +# There is just a single job in the workflow +jobs: + archive: + name: Create and release an archive of the library's core files and directories. + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Zip the important files and directories + run: | + echo "Creating archive '$archive_name' from: '$archive_content' ..." + echo "The git ref is: '${{github.ref}}'" + echo "The release tag: '${{env.archive_tag}}'" + zip -r $archive_name $archive_content + + - name: Upload the archive to a release + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{secrets.GITHUB_TOKEN}} + file: ${{env.archive_name}} + tag: ${{env.archive_tag}} + overwrite: true + body: "Latest minimal version of the library for CMake's module `FetchContent` and friends." diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..80066da --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# Specify up the folders/files that should *never* go into the git repository. + +# The Mac Finder produces a metadata file that is noise. +.DS_Store + +# My specific Visual Code setup is only useful to me. +.vscode/ + +# Visual Studio puts artifacts into an out directory. +out/ + +# Our CMake setup puts artifacts into "build" folders. +build* +build*/ + +# Some local scratch files are kept in private directories that need not go into git. +private/ + +# The documentation website uses Quarto and we don't need to version its cache or what it builds. +.quarto/ +_site/ + +# Jupyter checkpoint directories need not go into git +.ipynb_checkpoints/. + +# Generally don't want to check Node.js modules into the remote repo -- just the recipes for installing them. +node_modules/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..efc1c41 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,43 @@ +cmake_minimum_required(VERSION 3.25) + +# Our project +project(utilities DESCRIPTION "C++ Utilities Library" LANGUAGES CXX) + +# Add a target for the "library" we are building (it is header-only, hence INTERFACE). +# Also add the usual alias for this library name. +add_library(utilities INTERFACE) +add_library(utilities::utilities ALIAS utilities) + +# We use C++20 features +target_compile_features(utilities INTERFACE cxx_std_20) + +# Where to find the project headers (e.g., how to resolve `#include "utilities/utilities.h"`). +target_include_directories(utilities INTERFACE + $ + $) + +# That's it unless we are developing the library instead of just using it! +# If we are developing the library, then we go ahead and create targets for the executables in the examples/ directory +if (PROJECT_IS_TOP_LEVEL) + + # Append our local directory of CMake modules to the default ones searched by CMake + list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") + + # Prevent in-source builds for the example programs + include(disable_in_source_builds) + disable_in_source_builds() + + # Make the compiler issue warnings for "bad" code, etc. + include(compiler_init) + compiler_init(utilities) + + # For neatness, we want all the example executables to go in build/bin/. + include(GNUInstallDirs) + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}") + + # Finally, we walk through the examples directory and build a target for each .cpp file with appropriate linkage. + # We have a CMake function that makes that traversal straightforward. + include(add_executables) + add_executables(examples utilities::utilities) + +endif() diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7db1326 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2024 Nessan Fitzmaurice + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5279081 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# README + +The `utilities` library is a small collection of C++ classes, functions, and macros. + +It is header-only, so there is nothing to compile or link. +Moreover, you can use any header file in this library on a standalone basis, as there are no interdependencies. + +## Available Facilities + +| Header File | Purpose | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `check.h` | Defines some `assert`-type macros that improve on the standard one in various ways. In particular, you can add a message explaining why a check failed. | +| `format.h` | Functionality that connects any class with a `to_string()` method to `std::format`. | +| `print.h` | Workaround for any compiler that hasn't yet implemented `std::print`. | +| `macros.h` | Defines macros often used in test and example programs.
It also defines a mechanism that lets you overload a macro based on the number of passed arguments. | +| `log.h` | Some very simple logging macros. | +| `stopwatch.h` | Defines the `utilities::stopwatch` class you can use to time blocks of code. | +| `stream.h` | Defines some functions to read lines from a file, ignoring comments and allowing for continuation lines. | +| `string.h` | Defines several useful string functions (turn them to upper-case, trim white space, etc). | +| `thousands.h` | Defines functions to imbue output streams and locales with commas. This makes it easier to read large numbers–for example, printing 23000.56 as 23,000.56. | +| `type.h` | Defines the function `utilities::type` which produces a string for a type. | +| `utilities.h` | This “include-the-lot” header pulls in all the other files above. | + +## Installation + +This library is header-only, so there is nothing to compile & link. Drop the small `utilities` header directory somewhere convenient. You can even use any single header file on a stand-alone basis. + +Alternatively, if you are using `CMake`, you can use the standard `FetchContent` module by adding a few lines to your project's `CMakeLists.txt` file: + +```cmake +include(FetchContent) +FetchContent_Declare(utilities URL https://github.com/nessan/utilities/releases/download/current/utilities.zip) +FetchContent_MakeAvailable(utilities) +``` + +This command downloads and unpacks an archive of the current version of `utilities` to your project's build folder. You can then add a dependency on `utilities::utilities`, a `CMake` alias for `utilities`. `FetchContent` will automatically ensure the build system knows where to find the downloaded header files and any needed compiler flags. + +## Documentation + +You can read the project's documentation [here](https://nessan.github.io/utilities/). \ +We used the static website generator [Quarto](https://quarto.org) to construct the documentation site. + +### Contact + +You can contact me by email [here](mailto:nzznfitz+gh@icloud.com). + +### Copyright and License + +Copyright (c) 2022-present Nessan Fitzmaurice. \ +You can use this software under the [MIT license](https://opensource.org/license/mit). diff --git a/cmake/add_executables.cmake b/cmake/add_executables.cmake new file mode 100644 index 0000000..103d1e0 --- /dev/null +++ b/cmake/add_executables.cmake @@ -0,0 +1,133 @@ +# ----------------------------------------------------------------------------- +# @brief Add targets for lots of small executables. +# @link https://nssn.gitlab.io/cmake +# +# SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice +# SPDX-License-Identifier: MIT +# ----------------------------------------------------------------------------- + +# group_target(Foo03 target) will return Foo in the `target` argument. +# This is a local helper function used by the main event below. +function(group_target target group_target) + + # NOTE: CMake's regex abiliities are fairly basic but this seems to work OK + string(REGEX REPLACE "(^[a-zA-Z]*)_*[0-9]+$" "\\1" group ${target}) + set(${group_target} ${group} PARENT_SCOPE) + +endfunction() + +# check_target("Foo" target) will return a valid target-name in the second +# argument -- typically just Foo. However, if it happens that 'Foo' is already a +# pre-existing target then you will get back something like Foo1, Foo2 etc. +# +# Note this function uses the CMake in/out idiom so the first variable is passed +# as a value (e.g. "Foo" or ${target}) while the second is just a variable name +# which may or may not exist at the time of the call. Call might look like +# `check_target("Foo" target)` or `check_target("$tgt" target)` +# +# This is a local helper function used by the main event below but might be +# useful in other contexts. +function(check_target in out) + + # Start by assuming that the given target is not conflicted + set(trial ${in}) + + # Counter that we will use to create new potential target names if there is a conflict + set(n_min "2") + set(n_max "9") + set(n ${n_min}) + + # As long as there is a conflict append -n to the target without going crazy with the size of n .... + while(TARGET ${trial}) + set(trial ${in}-${n}) + math(EXPR n "${n} + 1") + if(n GREATER n_max) + message(FATAL "Cannot create an unconflicted target name for ${in}") + endif() + endwhile() + + # Set the unconflicted target name + set(${out} ${trial} PARENT_SCOPE) + +endfunction() + + +# add_executables(examples, ...) is the main event as descibed in the docs. +function(add_executables dir) + + # Trivial check ... + if(NOT IS_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/${dir}) + message(WARNING "Argument '${dir}' isn't a directory! (Spelling error?)") + return() + endif() + + # Grab all the source files from the directory in question (only handle C++/C as yet) + file(GLOB sources LIST_DIRECTORIES false CONFIGURE_DEPENDS ${dir}/*.cpp ${dir}/*.cc ${dir}/*.c) + set(all_targets "") + + # Add a target for each file + foreach(source_file ${sources}) + + # File "Foo03.cpp" is expected to yield target "Foo03" in the target variable + get_filename_component(target ${source_file} NAME_WLE) + + # Might have a target naming conflict (test program Foo linking to library Foo) + check_target(${target} clean_target) + + # We link any libraries passed at the end of the function invocation. + add_executable(${clean_target} EXCLUDE_FROM_ALL ${source_file}) + target_link_libraries(${clean_target} ${ARGN}) + + # Add the new target to the list of all the targets + list(APPEND all_targets ${clean_target}) + + endforeach() + + # Assuming there are targets to process we now create a target "dir" that expands to all the targets + # and also group targets so that e.g. Bar will expand to Bar01 Bar02 and Foo to Foo01 Foo2 Foo03 etc. + list(LENGTH all_targets all_targets_len) + if(${all_targets_len} GREATER 0) + + # Want to have everything in nice order e.g. Bar0, Bar01, Bar03, Foo01, Foo02, Starter, ... + list(SORT all_targets) + + # We will keep track of a group with a name like "Bar" and members "Bar01,Bar02,..." + set(group_target "") # Initial value + set(group_list "") # Initial value + + # Go through the targets and extract the groups and their members + foreach(exec ${all_targets}) + + # Given a name like Foo01 or Foo2 or Foo_12 the following regex will extract the string Foo + group_target(${exec} exec_group) + + # OK we got our "Foo". Do we need to close out the current group and start a new one? + if(NOT ${exec_group} STREQUAL group_target) + + # How many targets are in the current group (say it's called Bar)? + list(LENGTH group_list group_len) + + # Create a target for Bar if that is worth our while (i.e. more than one sub-target) + if(${group_len} GREATER 1) + check_target(${group_target} clean_group_target) + add_custom_target(${clean_group_target} DEPENDS ${group_list}) + endif() + + # Start the new group with no members in its corresponding list as yet + set(group_target ${exec_group}) + set(group_list "") + + endif() + + # Have an appropriate group to add this particular target to + list(APPEND group_list ${exec}) + + endforeach() + + # Finally we create an overall cumulative target 'dir' that expands to Foo01, Foo02, Bar01, Bar02, Bar03 etc. + check_target(${dir} clean_dir_target) + add_custom_target(${clean_dir_target} DEPENDS ${all_targets}) + + endif() + +endfunction() diff --git a/cmake/compiler_init.cmake b/cmake/compiler_init.cmake new file mode 100644 index 0000000..a4be1fd --- /dev/null +++ b/cmake/compiler_init.cmake @@ -0,0 +1,152 @@ +# ----------------------------------------------------------------------------- +# @brief Set some commonly used compiler warning flags etc. +# @link https://nssn.gitlab.io/cmake +# +# SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice +# SPDX-License-Identifier: MIT +# ----------------------------------------------------------------------------- + +# Get CMake to export all the compile commands in a JSON file. That is useable +# by editors such as VSCode to determine header locations etc. +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +# Get CMake to ask the comiler to add some color to our lives! +set(CMAKE_COLOR_DIAGNOSTICS ON) + +# By default gets the compiler performs extra checks. +option(WARNINGS_ARE_PICKY "Make the compiler extra picky!" ON) + +# By default the compiler will treat any warning as a hard error and stop compilation. +option(WARNINGS_ARE_ERRORS "Make the compiler barf completely if it encounters any warning!" ON) + +# The main event ... +function(compiler_init target_name) + + # Force Microsoft Visual Studio Code to use its newer cross-platform compatible preprocessor + set(MSVC_FLAGS /Zc:preprocessor /utf-8) + + # Add baseline warnings for Microsoft Visual Studio Code + set(MSVC_FLAGS + ${MSVC_FLAGS} + /w14242 # 'identifier': conversion from 'type1' to 'type1', possible loss of data + /w14254 # 'operator': conversion from 'type1:field_bits' to 'type2:field_bits', possible loss of data + /w14263 # 'function': member function does not override any base class virtual member function + /w14265 # 'classname': class has virtual functions, but destructor is not virtual + /w14287 # 'operator': unsigned/negative constant mismatch + /we4289 # 'variable': loop control variable declared in the for-loop is used outside the for-loop scope + /w14296 # 'operator': expression is always 'boolean_value' + /w14311 # 'variable': pointer truncation from 'type1' to 'type2' + /w14545 # expression before comma evaluates to a function which is missing an argument list + /w14546 # function call before comma missing argument list + /w14547 # 'operator': operator before comma has no effect; expected operator with side-effect + /w14549 # 'operator': operator before comma has no effect; did you intend 'operator'? + /w14555 # expression has no effect; expected expression with side- effect + /w14619 # pragma warning: there is no warning number 'number' + /w14640 # Enable warning on thread un-safe static member initialization + /w14826 # Conversion from 'type1' to 'type_2' is sign-extended. This may cause unexpected runtime behavior. + /w14905 # wide string literal cast to 'LPSTR' + /w14906 # string literal cast to 'LPWSTR' + /w14928 # illegal copy-initialization; more than one user-defined conversion has been implicitly applied + /permissive- # standards conformance mode for MSVC compiler. + ) + + # Baseline enabled warnings that are the same for clang and gcc + set(CLANG_FLAGS + -Wcast-align + -Wconversion + -Wdeprecated + -Wdouble-promotion + -Wformat=2 + -Wnon-virtual-dtor + -Wnull-dereference + -Wold-style-cast + -Woverloaded-virtual + -Wshadow + -Wsign-conversion + -Wundef + -Wzero-as-null-pointer-constant + -Wno-unused-function # We often have non-inline functions that aren't used in our test/example programs + ) + + # gcc has much the same interface as clang -- we can just add some more baseline items + set(GCC_FLAGS + ${CLANG_FLAGS} + -Wcast-qual + -Wctor-dtor-privacy + -Wdisabled-optimization + -Wduplicated-branches + -Wduplicated-cond + -Winvalid-pch + -Wlogical-op + -Wmisleading-indentation + -Wmissing-include-dirs + -Wno-ctor-dtor-privacy + -Wno-dangling-else + -Wno-format-nonliteral + -Wno-unused-local-typedefs + -Wpointer-arith + -Wredundant-decls + -Wshift-overflow=2 + -Wsized-deallocation + -Wtrampolines + -Wuseless-cast + -Wvector-operation-performance + -Wwrite-strings + ) + + # Disable some GCC buggy checks .... + # GCC at least as of 12.2 has a bug where the -Wrestrict flag detects some false positives in optimized mode. + set(GCC_FLAGS ${GCC_FLAGS} -Wno-restrict) + + # GCC at least as of 13.1 has a bug where the -Wnull-dereference flag detects some false positives. + set(GCC_FLAGS ${GCC_FLAGS} -Wno-null-dereference) + + # Up the level of inspection done by the compiler if asked to do so + if (WARNINGS_ARE_PICKY) + set(CLANG_FLAGS ${CLANG_FLAGS} -Wall -Wextra) + set(GCC_FLAGS ${GCC_FLAGS} -Wall -Wextra) + set(MSVC_FLAGS ${MSVC_FLAGS} /W4) + endif() + + # Make the compiler treat warnings as errors (i.e. stop) if asked to do so. Forces developer to fix them. + if(WARNINGS_ARE_ERRORS) + set(CLANG_FLAGS ${CLANG_FLAGS} -Werror) + set(GCC_FLAGS ${GCC_FLAGS} -Werror) + set(MSVC_FLAGS ${MSVC_FLAGS} /WX) + endif() + + # Pick the correct set of flags for the compiler we are using + if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + set(compiler_flags ${MSVC_FLAGS}) + elseif(CMAKE_CXX_COMPILER_ID MATCHES ".*Clang") + set(compiler_flags ${CLANG_FLAGS}) + elseif(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(compiler_flags ${GCC_FLAGS}) + else() + message(WARNING "No compiler flags were set for unknown '${CMAKE_CXX_COMPILER_ID}' compiler.") + endif() + + # Are we looking at a library type target? + set(target_is_a "") + get_target_property(target_is_a ${target_name} TYPE) + + # Don't generally want any flags to propagate except for header-only targets. + set(target_type PRIVATE) + if(${target_is_a} STREQUAL "INTERFACE_LIBRARY") + set(target_type INTERFACE) + endif() + + # Debug builds get the DEBUG flag set otherwise we set the NDEBUG flag + target_compile_definitions(${target_name} ${target_type} $, DEBUG=1, NDEBUG=1>) + + # Set the other compilation flags as laid out above. + target_compile_options(${target_name} ${target_type} ${compiler_flags}) + + # GCC at least as of 13.2 does not interact well with the new linker from Xcode-15 so use the classic version instead. + # We apply the fix for all GCC/Xcode links here for now and issue a message to let the user know ... + if(APPLE AND CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + message("Reverting to using the classic Xcode ld linker for gcc on MacOS!") + add_link_options("-Wl,-ld_classic") + endif() + +endfunction() diff --git a/cmake/disable_in_source_builds.cmake b/cmake/disable_in_source_builds.cmake new file mode 100644 index 0000000..df44948 --- /dev/null +++ b/cmake/disable_in_source_builds.cmake @@ -0,0 +1,21 @@ +# ----------------------------------------------------------------------------- +# @brief Dsable in-source builds. A classic! +# @link https://nssn.gitlab.io/cmake +# +# SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice +# SPDX-License-Identifier: MIT +# ----------------------------------------------------------------------------- +function(disable_in_source_builds) + + if(PROJECT_SOURCE_DIR STREQUAL PROJECT_BINARY_DIR) + message(FATAL_ERROR + "You are trying to compile things in the same location that the source files live in. + This is considered 'unhygienic' so is not allowed by this project: ${PROJECT_NAME}! + Please create a subfolder (e.g. `mkdir build; cd build`) and use `cmake ..` from there. + + NOTE: cmake will now (unfortunately) create the file CMakeCache.txt and the directory CMakeFiles/ + in the ${PROJECT_SOURCE_DIR}. So as well as creating the build directory etc. you must delete both + of those or cmake will refuse to work.") + endif() + +endfunction() \ No newline at end of file diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..075b254 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1 @@ +/.quarto/ diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..76e336a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,12 @@ +# README + +This directory holds content and code for the library's [documentation site]. + +We use the static website generator [Quarto] to build the site and write the content in their version of [Markdown]. +The Markdown files are in the `content` directory and have a `.qmd` extension. + + + +[documentation site]: https://nessan.github.io/utilities +[Quarto]: https://quarto.org +[Markdown]: https://quarto.org/content/authoring/markdown-basics.html diff --git a/docs/_quarto.yml b/docs/_quarto.yml new file mode 100644 index 0000000..f080fdf --- /dev/null +++ b/docs/_quarto.yml @@ -0,0 +1,125 @@ +# Metadata for this documentation project. +# See https://quarto.orgcontent/reference/projects/websites.html +project: + type: website + output-dir: _site +format: + html: + toc: true + toc-expand: true + code-copy: true + code-overflow: scroll + grid: + sidebar-width: 250px + body-width: 800px + margin-width: 250px + gutter-width: 1.5em + theme: + - cosmo + - assets/css/theme.scss +website: + title: "C++ Utilities Library" + image: "assets/images/logo.png" + favicon: "assets/images/logo.png" + open-graph: true + twitter-card: true + google-analytics: G-1KYLJGVB85 + site-url: "https://nessan.github.io/utilities" + repo-url: "https://github.com/nessan/utilities" + repo-subdir: docs + issue-url: "https://github.com/nessan/utilities/issues/new/choose" + repo-actions: [edit, issue] + page-navigation: true + bread-crumbs: false + back-to-top-navigation: true + page-footer: + left: | + Copyright (c) Nessan Fitzmaurice + center: | + Project is under the MIT License + right: + - icon: github + href: "https://github.com/nessan/utilities" + aria-label: GitHub Repo + - icon: twitter + href: https://twitter.com/nezzan + aria-label: Twitter + navbar: + background: dark + logo: "assets/images/logo.png" + logo-alt: "The utilities library logo" + title: utilities + search: true + pinned: true + collapse-below: lg + left: + - text: "Home" + file: content/index.qmd + - text: "Format" + file: content/format.qmd + - text: "Checks" + file: content/check.qmd + - text: "Logging" + file: content/log.qmd + - text: "Stopwatch" + file: content/stopwatch.qmd + - text: "Strings" + file: content/string.qmd + - text: "Streams" + file: content/stream.qmd + - text: "Commas" + file: content/thousands.qmd + - text: "Macros" + file: content/macros.qmd + - text: "Types" + file: content/type.qmd + - text: "More" + menu: + - text: "Project Repo" + icon: "github" + href: "https://github.com/nessan/utilities/issues" + - text: "Report a Bug" + icon: "bug" + href: "https://github.com/nessan/utilities/issues" + - text: "Ask a Question" + icon: "chat-right-text" + href: "https://github.com/nessan/utilities/discussions" + tools: + - icon: github + href: "https://github.com/nessan/utilities" + text: GitHub repo + - icon: twitter + href: https://twitter.com/nezzan + aria-label: Twitter + sidebar: + style: floating + type: light + background: light + align: left + collapse-level: 2 + contents: + - text: "Library Overview" + file: content/index.qmd + - text: "Formatted Output" + file: content/format.qmd + - text: "Print Facilities" + file: content/print.qmd + - text: "Checks & Assertions" + file: content/check.qmd + - text: "Logging" + file: content/log.qmd + - text: "Stopwatch" + file: content/stopwatch.qmd + - text: "String Functions" + file: content/string.qmd + - text: "Stream Functions" + file: content/stream.qmd + - text: "Readable Numbers" + file: content/thousands.qmd + - text: "Useful Macros" + file: content/macros.qmd + - text: "Names for Types" + file: content/type.qmd +editor: + markdown: + canonical: true diff --git a/docs/_variables.yml b/docs/_variables.yml new file mode 100644 index 0000000..66e9461 --- /dev/null +++ b/docs/_variables.yml @@ -0,0 +1,3 @@ +cpp: "C++" +cpp20: "C++20" +cpp23: "C++23" \ No newline at end of file diff --git a/docs/assets/css/theme.scss b/docs/assets/css/theme.scss new file mode 100644 index 0000000..fc37056 --- /dev/null +++ b/docs/assets/css/theme.scss @@ -0,0 +1,57 @@ +/*-- scss:defaults --*/ + +// ------------------------------------------------------------------------------------------------ +// Fonts: +// ------------------------------------------------------------------------------------------------ + +// Import google fonts +@import url("https://fonts.googleapis.com/css2?family=Roboto:ital,wght@0,100;0,300;0,400;0,500;0,700;0,900;1,100;1,300;1,400;1,500;1,700;1,900&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono:ital,wght@0,100..700;1,100..700&display=swap"); + +// Set the main font and the mono-spaced one +$font-family-sans-serif: "Roboto", sans-serif !default; +$font-family-monospace: "Roboto Mono", monospace !default; + +// Go with lighter fonts for all headings etc. +$headings-font-weight: 400 !default; +:is(h1, h2, h3, h4, h5, h6) { + font-weight: 400; +} + +code { + font-size: 0.95em; +} + +table { + font-size: 0.9em; +} + +table code { + font-size: 14px; +} + +// ------------------------------------------------------------------------------------------------ +// Our own classes ... +// ------------------------------------------------------------------------------------------------ + +// Block titles -- the .bt class: +// Markdown: "[Example: blah blah blah]{.bt}". +// HTML: "

Example: blah blah blah:

" + +// Basic styling for block titles +.bt { + color: blue; + font-size: 0.9em; + font-weight: normal; + display: block; +} + +// Block titles preceding code-blocks should be tighter to the code. +// Selector picks any p with a .bt child that is then followed by a .sourceCode element. +p:has(> .bt):has(+ .sourceCode) { + margin-bottom: -1em; +} + +p:has(> .bt):has(+ dl) { + margin-bottom: 0em; +} diff --git a/docs/assets/images/logo.png b/docs/assets/images/logo.png new file mode 100644 index 0000000..79002c8 Binary files /dev/null and b/docs/assets/images/logo.png differ diff --git a/docs/content/_common.qmd b/docs/content/_common.qmd new file mode 100644 index 0000000..53bee92 --- /dev/null +++ b/docs/content/_common.qmd @@ -0,0 +1,34 @@ + + + +[MIT License]: https://opensource.org/license/mit +[Pandoc]: https://pandoc.org +[Quarto]: https://quarto.org +[repo]: https://github.com/nessan/utilities +[docs]: https://nessan.github.io/utilities +[email]: mailto:nzznfitz+gh@icloud.com + + +[`assert`]: https://www.cplusplus.com/reference/cassert/assert/ +[concept]: https://en.cppreference.com/w/cpp/language/constraints +[ranges]: https://en.cppreference.com/w/cpp/ranges +[`std::format`]: https://en.cppreference.com/w/cpp/utility/format/format +[`std::formatter`]: https://en.cppreference.com/w/cpp/utility/format/formatter +[`std::logic_error`]: https://en.cppreference.com/w/cpp/error/ +[`std::nullopt`]: https://en.cppreference.com/w/cpp/utility/optional/nullopt +[`std::optional`]: https://en.cppreference.com/w/cpp/utility/optional +[`std::print`]: https://en.cppreference.com/w/cpp/io/print +[`std::vector`]: https://en.cppreference.com/w/cpp/container/vector +[`what`]: https://en.cppreference.com/w/cpp/error/exception/what + + +[`check.h`]: /content/check.qmd +[`format.h`]: /content/format.qmd +[`log.h`]: /content/log.qmd +[`macros.h`]: /content/macros.qmd +[`print.h`]: /content/print.qmd +[`stopwatch.h`]: /content/stopwatch.qmd +[`stream.h`]: /content/stream.qmd +[`string.h`]: /content/string.qmd +[`thousands.h`]: /content/thousands.qmd +[`type.h`]: /content/type.qmd diff --git a/docs/content/check.qmd b/docs/content/check.qmd new file mode 100644 index 0000000..e8e462c --- /dev/null +++ b/docs/content/check.qmd @@ -0,0 +1,126 @@ +{{< include /content/_common.qmd >}} +--- +title: Check & Assertions +--- + +## Introduction + +The `` header has three replacements for the standard [`assert`] macro --- they all allow for an additional string output that you can use to print the values of the variables that triggered any failure. + +```cpp +check(condition, message) // <1> +debug_check(condition, message) // <2> +always_check(condition, message) // <3> +``` +1. Checks of this type are verified **unless** the `NDEBUG` flag is set at compile time. This version is closest in spirit to the standard [`assert`] macro. +2. Checks of this type are **only** verified if the `DEBUG` flag is set at compile time. +3. Checks of this type are **always** verified and cannot be turned off with a compiler flag. + +Assuming the checks are "on", in all cases, if `condition` evaluates to `false`, a `check_error` is thrown. + +A `check_error` is a custom exception object that inherits from [`std::logic_error`] +```cpp +class check_error : public std::logic_error { ... } +``` +The exception message always includes the location of the failure and an extra dynamic payload typically used to print the values of the variables that triggered the failure. +The payload can be anything that can be formatted using the facilities in [`std::format`]. + +Uncaught exceptions cause the program to abort, printing the [`what`] string from the exception. + +::: {.callout-note} +# Macro-land +We are in macro land here, so there are no namespaces. +Typically, macros have names in caps, but the standard `assert` does not follow that custom, so neither do these `check` macros. +::: + +::: {.callout-tip} +# Microsoft compiler +Microsoft's old traditional preprocessor is not happy with these macros, but their newer cross-platform compatible one is fine. +Add the `/Zc:preprocessor` flag to use that upgrade at compile time. +Our `CMake` module `compiler_init` does that automatically for you. +::: + + +## Why three macro forms? + +### `debug_check` + +In the development cycle, it can be helpful to range-check indices and so on. +However, those checks are expensive and can slow down numerical code by orders of magnitude. +Therefore, we don't want there to be any chance that those verifications are accidentally left "on" in the production code. +The `debug_check(...)` form covers this type of verification. +Turning on these checks requires the programmer to take a specific action --- namely, she must set the `DEBUG` flag during compile time. + +For example, here is a pre-condition from a hypothetical `dot(Vector u, Vector v)` function: +```cpp +debug_check(u.size() == v.size(), "Vector sizes {} and {} DO NOT match!", u.size(), v.size()); +``` +This code checks that the two vector arguments have equal length --- a necessary constraint for the dot product operation to make sense. +This code throws an exception if the requirement is not satisfied, with an informative message that includes the size of the two vectors. +Uncaught exceptions aborts the program with a helpful message that includes the location of the problem. + +The check here is **off** by default, and you need to do something special (i.e., define the `DEBUG` flag at compile time) to enable it. +The idea is that production code may do many of these dot products, and we do not generally want to pay for the check. +However, enabling these sorts of checks may be very useful during development. + +The `debug_check(...)` macro expands to nothing **unless** you set the `DEBUG` flag at compile time. + +### `check` + +On the other hand, some checks are pretty cheap, especially when you compare the cost to the actual work done by the function. +The `check(...)` form is helpful for those cheaper verifications. + +For example, a pre-condition for a matrix inversion method is that the input matrix must be square. +Here is how you might do that check in an `invert(const Matrix& M)` function: +```cpp +check(M.is_square(), "Cannot invert a {} x {} NON-square matrix!", M.rows(), M.cols()); +``` +We can only invert square matrices. +The `M.is_square()` call checks that condition and, on failure, throws an exception with a helpful message. + +This particular check is always on by default, and the programmer needs to do something special (i.e., define the `NDEBUG` flag at compile time) to deactivate it. + +The `check(...)` macro expands to nothing **only if** you set the `NDEBUG` flag at compile time --- the behavior is the same as the standard [`assert`] macro but allows for adding a formatted error message. + +### `always_check` + +There may be checks you never want to be turned off. +The final form `always_check(...)` accomplishes those tasks --- it is unaffected by compiler flags. + +For instance, in that last example, the check cost is very slight compared to the work done by the `invert(...)` method, so leaving it on even in production code is probably not a problem. +You might well want to use the `always_check(...)` version so the check never gets disabled: +```cpp +always_check(M.is_square(), "Cannot invert a {} x {} NON-square matrix!", M.rows(), M.cols()); +``` + +The decision to use one of these forms vs. another is predicated on the cost of doing the check versus the work done by the method in question. +A primary use case for `debug_check` is to do things like bounds checking on indices -- from experience, this is vital during development. +However, bounds-checking every index operation incurs a considerable performance penalty and can slow down numerical code by orders of magnitude. +So it makes sense to have the checks in place for development but to ensure they are never there in release builds. + +[Example]{.bt} +```cpp +#include +int subtract(int x, int y) +{ + always_check(x == y, "x = {}, y = {}", x, y); + return y - x; +} +int main() +{ + return subtract(10, 11); +} +``` + +[Output]{.bt} +```sh +libc++abi: terminating due to uncaught exception of type check_error: +[ERROR] In function 'subtract' (scratch01.cpp, line 4): +Statement 'x == y' is NOT true: x = 10, y = 11 +``` +The program will then abort. + +### See Also +[`format.h`] \ +[`log.h`] \ +[`assert`] diff --git a/docs/content/format.qmd b/docs/content/format.qmd new file mode 100644 index 0000000..0532256 --- /dev/null +++ b/docs/content/format.qmd @@ -0,0 +1,80 @@ +{{< include /content/_common.qmd >}} +--- +title: Formatted Output +--- + +## Introduction + +{{< var cpp20 >}} introduced the formatting library [`std::format`]. +That library makes it easy to create strings with interpolated values and has facilities that allow you to add your types as values in those strings. + +The `` header augments that facility. + +Many classes already implement a `std::string to_string() const` method that returns a string representation of an instance --- we can use that to push values into `std::format`. + +We have a [concept] that captures all types with an appropriate `to_string()` method. +```cpp +template +concept has_to_string_method = requires(const T& x) { + { x.to_string() } -> std::convertible_to; +}; +``` +We also supply a [`std::formatter`] that automatically connects any `has_string_method` class to the standard formatting library. + +[Example]{.bt} +```cpp +#include +#include + +struct Whatever { + std::string to_string() const { return "Whatever!"; } +}; + +int main() +{ + Whatever w; + std::cout << std::format("Struct with a to_string() method: '{}'\n", w); + return 0; +} +``` +[Output]{.bt} +```sh +Struct with a to_string() method: 'Whatever!' +``` + +## Ranges workaround + +{{< var cpp23 >}} will have facilities to allow [`std::format`] to work with [ranges], which will make it easier to create formatted strings with interpolated values from arrays, vectors, lists, etc. +If your compiler does not yet support this type of interpolation, `` supplies a workaround. + +[Example]{.bt} +```cpp +#include +#include +#include +int main() +{ +#ifdef __cpp_lib_format_ranges // <1> + std::cout << "The compiler natively supports formatting `ranges`!\n"; +#else + std::cout << "I will format `ranges` using the `` library!\n"; +#endif + + std::vector v = {1.123123, 2.1235, 3.555555}; + std::cout << std::format("Unformatted vector: {}\n", v); + std::cout << std::format("Formatted vector: {::3.2f}\n", v); +} +``` +1. `__cpp_lib_format_ranges` is a standard preprocessor flag indicating whether your compiler can format ranges. + +[Output]{.bt} +```sh +I will format `ranges` using the `` library! +Unformatted vector: [1.123123, 2.1235, 3.555555] +Formatted vector: [1.12, 2.12, 3.56] +``` + +### Sew Also +[`print.h`] \ +[`std::format`] \ +[`std::formatter`] \ No newline at end of file diff --git a/docs/content/index.qmd b/docs/content/index.qmd new file mode 100644 index 0000000..64c6d97 --- /dev/null +++ b/docs/content/index.qmd @@ -0,0 +1,73 @@ +{{< include /content/_common.qmd >}} +--- +title: The `utilities` Library +--- + +## Project Overview + +The `utilities` library is a small collection of {{< var cpp >}} classes, functions, and macros. +It is header-only, so there is nothing to compile or link. + +::: {.callout-tip} +# The header files are stand-alone +You can use any header file in this library by itself --- there are no interdependencies. +::: + +## Available Facilities + +Header File | Purpose +----------- | ------- +[`check.h`] | Defines some [`assert`] type macros that improve on the standard one in various ways. In particular, you can add a message explaining why a check failed. +[`format.h`] | Functionality that connects any class with a `to_string()` method to [`std::format`]. +[`print.h`] | Workaround for any compiler that hasn't yet implemented [`std::print`]. +[`macros.h`] | Defines macros often used in test and example programs.
It also defines a mechanism that lets you overload a macro based on the number of passed arguments. +[`log.h`] | Some very simple logging macros. +[`stopwatch.h`] | Defines the `utilities::stopwatch` class you can use to time blocks of code. +[`stream.h`] | Defines some functions to read lines from a file, ignoring comments and allowing for continuation lines. +[`string.h`] | Defines several useful string functions (e.g., turning strings to uppercase, trimming white space, etc.). +[`thousands.h`] | Defines functions to imbue output streams and locales with commas that make it easier to read large numbers --- for example, printing 23000.56 as 23,000.56. +[`type.h`] | Defines the function `utilities::type` which produces a string for a type. +`utilities.h` | This "include-the-lot" header pulls in all the other files above. + +: {.bordered .striped .hover .responsive tbl-colwidths="[20,80]"} + +::: {.callout-tip} +# Microsoft compiler +Microsoft's old traditional preprocessor is not happy with some of our macros, but their newer cross-platform compatible one is fine. +Add the `/Zc:preprocessor` flag to use that upgrade at compile time. +Our `CMake` module `compiler_init` does that automatically for you. +::: + +## Installation + +This library is header-only, so there is nothing to compile & link. +Drop the small `utilities` header directory somewhere convenient. +You can even use any single header file on a stand-alone basis. + +Alternatively, if you are using `CMake`, you can use the standard `FetchContent` module by adding a few lines to your project's `CMakeLists.txt` file: + +```cmake +include(FetchContent) +FetchContent_Declare(utilities URL https://github.com/nessan/utilities/releases/download/current/utilities.zip) +FetchContent_MakeAvailable(utilities) +``` + +This command downloads and unpacks an archive of the current version of `utilities` to your project's build folder. +You can then add a dependency on `utilities::utilities`, a `CMake` alias for `utilities`. +`FetchContent` will automatically ensure the build system knows where to find the downloaded header files and any needed compiler flags. + +## Documentation + +The project's source code repository is [here][repo]. \ +You can read the project's documentation [here][docs]. \ +We used the static website generator [Quarto] to construct the documentation site. + + +### Contact + +You can contact me by email [here][email]. + +### Copyright and License + +Copyright (c) 2022-present Nessan Fitzmaurice. \ +You can use this software under the [MIT license]. \ No newline at end of file diff --git a/docs/content/log.qmd b/docs/content/log.qmd new file mode 100644 index 0000000..bc97974 --- /dev/null +++ b/docs/content/log.qmd @@ -0,0 +1,81 @@ +{{< include /content/_common.qmd >}} +--- +title: Logging +--- + +## Introduction + +The header `` supplies a couple of macros for _simple_ logging and debug messages. +```cpp +LOG(...) // <1> +DBG(...) // <2> +``` +1. These messages are dispatched **unless** the `NO_LOGS` flag is set at compile time. +2. These messages are dispatched **only if** the `DEBUG` flag is set at compile time. + +These macros create and dispatch `utilities::message` objects, which hold the user-supplied message and location information specifying the message's creation location. +The `LOG` and `DBG` macros automatically pass the correct source code location information to the message. + +The message payload can be anything that works for the facilities in [`std::format`]. +If there is no payload, the default message handler prints the source code location. + +::: {.callout-tip} +# Microsoft compiler +Microsoft's old traditional preprocessor is not happy with these macros, but their newer cross-platform compatible one is fine. +Add the `/Zc:preprocessor` flag to use that upgrade at compile time. +Our `CMake` module `compiler_init` does that automatically for you. +::: + +## Message Handling + +The macros create and immediately dispatch messages to a message _handler_. +The default handler prints the message and the location information to a default stream `std::cout`. +You can change that default stream, for example: +```cpp +log_file = std::ofstream("log.txt"); +utilities::message::stream = &log_file; +``` + +For fancier usage, you can interpose your message handler +```cpp +void my_handler(const utilities::message& msg) { ... } +utilities::message::handler() = my_handler; +``` +Your handler can do whatever it likes with the messages it receives. +You can revert to the default handler by calling the class method `utilities::message::use_default_handler()`. + +::: {.callout-note} +# Simplicity rules +Everything here is deliberately rudimentary, and there is no consideration for rotating log files, etc., or any real consideration for streaming efficiencies. +The primary use case for the macros above and the underlying `utilities::message` class is to _easily_ print useful messages during the development cycle. +The emphasis is on ease of use as opposed to great generality or, indeed, speed. +During the development cycle, the typical output is going to be pretty small anyway. +::: + +[Example (source file named `example.cpp`)]{.bt} +```cpp +#include +int add(int x, int y) +{ + DBG("The DEBUG flag was set at compile time."); + LOG("x = {}, y = {}", x, y); + return x + y; +} + +int main() +{ + LOG(); // <1> + return add(10, 11); +} +``` +1. A call to `LOG` without any message will print the source location. + +[Output]{.bt} +```sh +[LOG] function 'main' (example.cpp, line 11) +[DBG] function 'add' (example.cpp, line 4): The DEBUG flag was set at compile time. +[LOG] function 'add' (example.cpp, line 5): x = 10, y = 11 +``` + +### See Also +[`check.h`] diff --git a/docs/content/macros.qmd b/docs/content/macros.qmd new file mode 100644 index 0000000..f04f512 --- /dev/null +++ b/docs/content/macros.qmd @@ -0,0 +1,147 @@ +{{< include /content/_common.qmd >}} +--- +title: Macros +--- + +## Introduction + +The header `` supplies some useful macros. + +These include the `OVERLOAD` macro, which implements a well-known trick to "overload" any macro based on the number of passed arguments. +See [overloading] below. + +::: {.callout-tip} +# Microsoft compiler +Microsoft's old traditional preprocessor is unhappy with `OVERLOAD`, but their newer cross-platform compatible one is fine. +Add the `/Zc:preprocessor` flag to use that upgrade at compile time. +Our `CMake` module `compiler_init` does that automatically for you. +::: + +## Compiler String + +```cpp +COMPILER_NAME +``` +Expands to a string that encodes the name of the compiler and its version. +Test code can use the macro to annotate results. +We only support the usual three compiler suspects, `gcc`, `clang`, and `MSVC`, but adding more [compilers] is not difficult. + +One of the examples below uses this macro. + +## Macro Expansion + +```cpp +STRINGIZE(foo) // <1> +CONCAT(foo, bar) // <2> +``` +1. Turn the argument into a string. +2. Concatenates the two arguments. + +These two classic macros work even if the arguments you pass in are themselves macros by fully expanding all arguments. + +## Overloading + +```cpp +OVERLOAD(macro, ...) +``` +You can "overload" a macro based on the number of passed arguments. + +For example, if you have overloaded `FOO` macro: +```cpp +#define FOO(...) OVERLOAD(FOO, __VA_ARGS__) +``` +The consumer of the macro can call it as `FOO()`, or `FOO(a)`, or `FOO(a,b)`. + +All calls get automatically forwarded to the correct concrete macro. +For `FOO`, these must be named `FOO0`, `FOO1`, `FOO2`, etc. +The macro writer provides whichever of these makes sense, but the consumer of the macro can use them through the more straightforward call. + +[Example]{.bt} +```cpp +#include +#include + +#define COUT(...) OVERLOAD(COUT, __VA_ARGS__) // <1> + +#define COUT0(x) std::cout << "Zero argument version: " << '\n' // <2> +#define COUT1(x) std::cout << "One argument version: " << x << '\n' +#define COUT2(x, y) std::cout << "Two arguments version: " << x << ", " << y << '\n' +#define COUT3(x, y, z) std::cout << "Three arguments version: " << x << ", " << y << ", " << z << '\n' + +int main() +{ + std::cout << "Compiler: " << COMPILER_NAME << '\n'; + + // NOTE: We only ever call COUT, but we expect to get the correct concrete version. + COUT(); + COUT("x"); + COUT("x", 2); + COUT("x", 2, 'z'); + return 0; +} +``` +1. We overload a trivial macro depending on the number of passed arguments. +2. The one, two, and three argument versions of `COUT`. + +[Output]{.bt} +```sh +Compiler: clang 17.0.6 +Zero argument version: +One argument version: x +Two arguments version: x, 2 +Three arguments version: x, 2, z +``` + +## Semantic Version Strings +```cpp +VERSION_STRING(major) <1> +VERSION_STRING(major, minor) <2> +VERSION_STRING(major, minor, patch) <3> +``` +1. Expands to "major". +2. Expands to "major.minor". +3. Expands to "major.minor.patch". + +`VERSION_STRING` is an _overloaded_ macro. +For example, `VERSION_STRING(3, 1, 0)` expands to the string `"3.1.0"`. + +## Print Code Lines and Results + +```cpp +RUN(code) <1> +RUN(code, out) <2> +RUN(code, out1, out2) <3> +``` +1. Prints a line of code to the screen and then runs it. +2. Prints a line of code to the screen, runs it, then prints the value from `out`. +3. Prints a line of code to the screen, runs it, then prints the values `out1` and `out2`. + +This _overloaded_ macro prints what will be executed in a test program, optionally followed by some results. +Best illustrated with an example. + +[Example]{.bt} +```cpp +#include +#include +#include +#include +#include + +int main() +{ + RUN(std::string s1); + RUN(double x = 123456789.123456789); + RUN(std::format_to(std::back_inserter(s1), "x = {:12.5f}", x), s1); +} +``` + +[Output]{.bt} +```sh +[CODE] std::string s1 +[CODE] double x = 123456789.123456789 +[CODE] fmt::format_to(std::back_inserter(s1), "x = {:12.5f}", x) +[RESULT] s1: x = 123456789.12346 +``` + + +[compilers]: https://github.com/cpredef/predef/blob/master/Compilers.md[compiler definitions] diff --git a/docs/content/print.qmd b/docs/content/print.qmd new file mode 100644 index 0000000..0f4f76c --- /dev/null +++ b/docs/content/print.qmd @@ -0,0 +1,39 @@ +{{< include /content/_common.qmd >}} +--- +title: Printing +--- + +## Introduction + +{{< var cpp23 >}} introduces [`std::print`] as an addition to the standard formatting library. + +If your compiler does not yet support [`std::print`], the `` header file supplies a workaround. + +[Example]{.bt} +```cpp +#include +#include +#include + +int main() +{ +#ifdef __cpp_lib_print + std::cout << "The compiler has std::print!\n"; +#else + std::cout << "I will use the std::print workaround!\n"; +#endif + std::vector v = {1.123123, 2.1235, 3.555555}; + std::print("Unformatted vector: {}\n", v); + std::print("Formatted vector: {::3.2f}\n", v);W +} +``` + +[Output]{.bt} +```sh +I will use the std::print workaround! +Unformatted vector: [1.123123, 2.1235, 3.555555] +Formatted vector: [1.12, 2.12, 3.56] +``` + +### See Also +[`format.h`] \ No newline at end of file diff --git a/docs/content/stopwatch.qmd b/docs/content/stopwatch.qmd new file mode 100644 index 0000000..91e0360 --- /dev/null +++ b/docs/content/stopwatch.qmd @@ -0,0 +1,181 @@ +{{< include /content/_common.qmd >}} +--- +title: Stopwatch Class +--- + +## Introduction + +The `` header defines `utilities::stopwatch`, a simple stopwatch class to measure execution times. + +You might use it like this: +```cpp +utilities::stopwatch sw; +do_work(); +std::cout << sw << '\n'; +``` +The output will be the time taken in seconds for the `do_work()` call to run. + +## Declaration + +```cpp +template +class utilities::stopwatch; +``` +The header-only class is templatized over the specific clock choice, likely one of the clocks from [`std::chrono`]. + +::: {.callout-tip} +# The default clock +Our default clock is [`std::::chrono::high_resolution_clock`]. +Discussions on sites like [Stack Overflow] instead favor [`std::chrono::steady_clock`]. +That clock is guaranteed to be monotonic and unaffected by changing the system time, so it is suitable for long-running processes. +However, in practice, we primarily examine much shorter code blocks and value accuracy over "steadiness." +We also note that, in any case, the two clocks are often identical! +::: + +If the clock choice matters, you can use one of the following type aliases: +```cpp +utilities::precise_stopwatch = utilities::stopwatch; +utilities::steady_stopwatch = utilities::stopwatch; +utilities::system_stopwatch = utilities::stopwatch; +``` + +We always store elapsed times as a `double` number of seconds --- this is also contrary to advice that advocates the use of [`std::chrono::duration`]. + +The primary goal for `utilities::stopwatch` is ease of use. +Sticking to seconds as the standard unit makes everything consistent. +Besides, a double number of seconds gives you 15 or 16 places of accuracy, which is enough for any conceivable application. + +::: {.callout-tip} +# This is not a profiler +Use the `utilities::stopwatch` class for cheap and cheerful performance measurement. +It is not a replacement for the many more complete but more complex code profiling tools. +::: + +## Construction + +```cpp +utilities::stopwatch(const std::string& name = ""); +``` +Creates a stopwatch and sets its _zero time_ to now. +You can give it a name, which is helpful if multiple stopwatches run in one executable. + +## Timing Methods + +The stopwatch class is kept deliberately simple. + +At its core, it measures the elapsed time in seconds from a _zero time_ set on creation or by calling the stopwatch's `reset()` method. +It also supports the idea of _clicking_ a stopwatch to record a _split time_--the time in seconds from the zero point to the click event. +Finally, it records a _lap_ time, which is the time between the last two stopwatch clicks. +However, it has no memory further back than that. + +```cpp +constexpr void reset(); // <1> +constexpr double elapsed() const; // <2> +constexpr double click(); // <3> +constexpr double split() const; // <4> +constexpr double lap() const; // <5> +``` +1. Clears out any recorded split time and resets the _zero time_ to now. +2. Returns the number of seconds from the _zero_time_ to now. +3. Creates a _split_ by recording the elapsed time in seconds from the _zero time_ to the `click()` call and returns that time. +4. Returns the last recorded split time in seconds. +5. Returns the time in seconds between the last two splits --- i.e., between the previous two click events. + +## Other Methods + +```cpp +const std::string& name() const; // <1> +std::string& name(); +``` +1. Read-only and read-write access to the stopwatch name field. + +## Output Functions + +```cpp +template +std::ostream & +operator<<(std::ostream&, const utilities::stopwatch&); // <1> + +template +struct std::formatter>; // <2> +``` +1. The usual output operator for a stopwatch. +2. Adds stopwatch support to [`std::format`]. + +::: {.callout-note} +# Elapsed time +These functions output the stopwatch's elapsed time only. +The output will look like "3.2s" without a name. +The output will look like "name: 3.2s" with a name. +::: + +## Other Functions + +```cpp +template +constexpr double +to_seconds(const std::chrono::duration); // <1> +``` +1. This converts a [`std::chrono::duration`] to a `double` number of seconds. + + +[Example: How efficient is it to put a thread to sleep?]{.bt} +```cpp +#include +#include +#include + +int main() +{ + using namespace std::literals; + utilities::stopwatch sw; + + for (auto sleep_duration = 0ms; sleep_duration <= 2s; sleep_duration += 200ms) { + + sw.click(); // <1> + std::this_thread::sleep_for(sleep_duration); // <2> + sw.click(); // <3> + + double sleep_ms = 1000 * utilities::to_seconds(sleep_duration); // <4> + double actual_ms = 1000 * sw.lap(); // <5> + double diff = actual_ms - sleep_ms; + double percent = sleep_ms != 0 ? 100 * diff / sleep_ms : 0; + + std::cout << std::format("Requested sleep for {:8.2f}ms, measured wait was {:8.2f}ms => overhead {:.2f}ms ({:.2f}%)\n", + sleep_ms, actual_ms, diff, percent); + } + std::cout << "Total elapsed time: " << sw << '\n'; + return 0; +} +``` +1. Create a split. +2. Sleep for a set number of milliseconds. +3. Create a second split and, hence, a lap. +4. Convert the sleep duration to a double number of seconds. +5. Get the lap time for that last call to `sleep_for(...)`. + +[Output]{.bt} +```sh +Requested sleep for 0.00ms, measured wait was 0.00ms => overhead 0.00ms (0.00%) +Requested sleep for 200.00ms, measured wait was 202.96ms => overhead 2.96ms (1.48%) +Requested sleep for 400.00ms, measured wait was 401.59ms => overhead 1.59ms (0.40%) +Requested sleep for 600.00ms, measured wait was 604.82ms => overhead 4.82ms (0.80%) +Requested sleep for 800.00ms, measured wait was 804.06ms => overhead 4.06ms (0.51%) +Requested sleep for 1000.00ms, measured wait was 1001.97ms => overhead 1.97ms (0.20%) +Requested sleep for 1200.00ms, measured wait was 1202.99ms => overhead 2.99ms (0.25%) +Requested sleep for 1400.00ms, measured wait was 1405.10ms => overhead 5.10ms (0.36%) +Requested sleep for 1600.00ms, measured wait was 1603.67ms => overhead 3.67ms (0.23%) +Requested sleep for 1800.00ms, measured wait was 1803.70ms => overhead 3.70ms (0.21%) +Requested sleep for 2000.00ms, measured wait was 2004.82ms => overhead 4.82ms (0.24%) +Total elapsed time: 11.036255763s +``` + +### See Also +[`std::chrono`] + + +[`std::chrono`]: https://en.cppreference.com/w/cpp/header/chrono +[`std::::chrono::high_resolution_clock`]: https://en.cppreference.com/w/cpp/chrono/high_resolution_clock +[`std::chrono::steady_clock`]: https://en.cppreference.com/w/cpp/chrono/steady_clock +[`std::chrono::duration`]: https://en.cppreference.com/w/cpp/chrono/duration +[Stack Overflow]: https://stackoverflow.com \ No newline at end of file diff --git a/docs/content/stream.qmd b/docs/content/stream.qmd new file mode 100644 index 0000000..0e296dd --- /dev/null +++ b/docs/content/stream.qmd @@ -0,0 +1,53 @@ +{{< include /content/_common.qmd >}} +--- +title: Stream Functions +--- + +## Introduction + +The `` header supplies some utility functions that work on streams. + +## Reading from a Stream + +```cpp +std::string +utilities::read_line(std::istream &s, + std::string_view comment_begin = "#"); // <1> +std::size_t +utilities::read_line(std::istream &s, std::string &line, + std::string_view comment_begin = "#"); // <2> +``` +1. Reads a '`line`' from a stream `s` and returns that as a `std::string`. +2. Overwrites the '`line`' argument with the contents from a stream `s`. +Returns the number of characters placed into `line`. + +These functions differ from [`std::getline`] in a few ways: + +1. They ignore blank lines in the input stream. +2. They allow for long lines by assuming that lines that end with a "\\" continue to the next. +3. They strip out comment lines and trailing comments. + +Comment lines begin with "#" by default. + +## Related Functions + +```cpp +std::istream & rewind(std::istream &is); // <1> + +std::size_t +line_count(std::istream &is, + std::string_view comment_begin = "#"); // <2> +``` +1. This function rewinds an input stream to the start. +2. This function returns the number of non-comment lines in the stream. + +If the comment start string is empty, `line_count` uses [`std::getline`] to read the lines. +Otherwise, we use our `read_line(...)` function, so comment lines are excluded, etc. + +::: {.callout-note} +# Files only +These two functions only work with file streams, etc. +::: + + +[`std::getline`]: https://en.cppreference.com/w/cpp/string/basic_string/getline \ No newline at end of file diff --git a/docs/content/string.qmd b/docs/content/string.qmd new file mode 100644 index 0000000..5b6cf21 --- /dev/null +++ b/docs/content/string.qmd @@ -0,0 +1,250 @@ +{{< include /content/_common.qmd >}} +--- +title: String Functions +--- + +## Introduction + +The header `` supplies several utility functions that work on strings. + +Many of the functions come in two flavors. +One version alters the input string in-place, while the other returns a new string that is a copy of the input appropriately converted, leaving the original untouched. + +For example, `utilities::upper_case(str)` converts `str` to upper-case in place. +On the other hand, `utilities::upper_cased(str)` returns a fresh string that is a copy of `str` converted to upper-case. +As you will see below, this is the typical naming style used. + +There are other functions where this distinction is unnecessary, such as `utilities::starts_with(...)`. + +## Case Conversions + +```cpp +void utilities::upper_case(std::string&); // <1> +void utilities::lower_case(std::string&); // <2> + +std::string utilities::upper_cased(std::string_view); // <3> +std::string utilities::lower_cased(std::string_view); // <4> +``` +1. Converts a string to uppercase. +2. Converts a string to lowercase. +3. Returns a new string, an uppercase copy of the input string. +4. Returns a new string, a lowercase copy of the input string. + +::: {.callout-note} +# Limitation +Our case conversions rely on the [`std::tolower`] and [`std::toupper`] functions, which only work for simple character types. +::: + +## Trimming Spaces + +```cpp +void utilities::trim_left(std::string&); // <1> +void utilities::trim_right(std::string&); // <2> +void utilities::trim(std::string&); // <3> + +std::string utilities::trimmed_left(std::string_view); // <4> +std::string utilities::trimmed_right(std::string_view); // <5> +std::string utilities::trim(medstd::string_view); // <6> +``` +1. Remove any leading whitespace from the input string. +2. Remove any trailing whitespace from the input string. +3. Remove leading and trailing whitespace from the input string. +4. Returns a new string, a left-trimmed copy of the input string. +5. Returns a new string, a right-trimmed copy of the input string. +6. Returns a new string that is a trimmed copy of the input string on both sides. + +::: {.callout-note} +# Limitation +Our case conversions rely on the [`std::isspace`] function to identify whitespace characters. +::: + +## Replacing Substrings + +```cpp +void +utilities::replace_left(std::string &str, + std::string_view target, + std::string_view replacement); // <1> +void +utilities::replace_right(std::string &str, + std::string_view target, + std::string_view replacement); // <2> +void +utilities::replace(std::string &str, + std::string_view target, + std::string_view replacement); // <3> +std::string +utilities::replaced_left(std::string_view str, + std::string_view target, + std::string_view replacement); // <4> +std::string +utilities::replaced_right(std::string_view str, + std::string_view target, + std::string_view replacement);// <5> +std::string +utilities::replaced(std::string_view str, + std::string_view target, + std::string_view replacement); // <6> +``` +1. Replace the first occurrence of `target` in `str` with `replacement`. +2. Replace the final occurrence of `target` in `str` with `replacement`. +3. Replace all occurrences of `target` in `str` with `replacement`. +4. Returns a new string, a copy of `str` with the first occurrence of `target` changed to `replacement`. +5. Returns a new string, a copy of `str` with the final occurrence of `target` changed to `replacement`. +6. Returns a new string, a copy of `str` with all occurrences of `target` changed to `replacement`. + +We also have functions to replace all contiguous white space sequences in a string: +```cpp +void +utilities::replace_space(std::string &str, + const std::string &with = " ", + bool also_trim = true); // <1> +std::string +utilities::condense(std::string_view str, + bool also_trim = true); // <2> +std::string +utilities::replaced_space(std::string_view &str, + const std::string &with = " ", + bool also_trim = true); // <3> +std::string +utilities::condensed(std::string_view str, + bool also_trim = true); // <4> +``` +1. Replaces all contiguous white space sequences in a string with a single white space character or, optionally, something else. +By default, the string is also trimmed of white space on both the left and right. +1. Replaces all contiguous white space sequences in a string with a single white space character. +By default, the string is also trimmed of white space on both the left and right. +1. Returns a new string, a copy of `str` with all contiguous white space sequences replaced with a single white space character or, optionally, something else. +By default, the output string is also trimmed of white space on both the left and right. +1. Returns a new string, a copy of `str` with all contiguous white space sequences replaced with a single white space character. +By default, the output string is also trimmed of white space on both the left and right. + +## Erasing Substrings + +```cpp +void +utilities::erase_left(std::string &str, + std::string_view target); // <1> +void +utilities::erase_right(std::string &str, + std::string_view target); // <2> +void +utilities::erase(std::string &str, + std::string_view target); // <3> +std::string +utilities::erased_left(std::string_view str, + std::string_view target); // <4> +std::string +utilities::erased_right(std::string_view str, + std::string_view target); // <5> +std::string +utilities::erased(std::string_view str, + std::string_view target); // <6> +``` +1. Erases the first occurrence of the `target` substring in `str`. +2. Erases the final occurrence of the `target` substring in `str`. +3. Erases all occurrences of the `target` substring in `str`. +4. Returns a new string, a copy of `str` with the first occurrence of `target` erased. +5. Returns a new string, a copy of `str` with the final occurrence of `target` erased. +6. Returns a new string, a copy of `str` with all occurrences of `target` erased. + +## "Standardizing" Strings + +We often need to parse free-form input while looking for a keyword or phrase. +Having a facility that converts strings to some standard form is helpful. + +```cpp +void utilities::remove_surrounds(std::string&); // <1> +void utilities::standardize(std::string&); // <2> + +std::string utilities::removed_surrounds(std::string_view); // <3> +std::string utilities::standardized(std::string_view); // <4> +``` +1. Strips any "surrounds" from the input string. \ +For example, the string "(text)" becomes "text". +Multiples also work so "{{{text}}}" becomes "text". +Only correctly balanced surrounds are ever removed. +1. Standardize the input string --- see below +2. Returns a new string, a copy of the input with any "surrounds" removed. +3. Returns a new string, a standardized copy of the input. + +The `standardize` functions give you a string stripped of extraneous brackets, etc. +Moreover, the single space character will replace all interior white space, and all leading and trailing whitespace will be removed. +So a string like "< Ace of Clubs >" will become "ACE OF CLUBS". + +It is a lot easier to parse standardized strings. + +## Searching + +```cpp +bool utilities::starts_with(std::string_view str, std::string_view prefix); // <1> +bool utilities::ends_with(std::string_view str, std::string_view prefix); // <2> +``` +1. Returns `true` if `str` starts with `prefix`. +2. Returns `true` if `str` ends with `suffix`. + +## Tokenizing + +We often want to convert a stream of text into tokens. +Here are some functions to help with that: + +```cpp +template +constexpr void +for_each_token(InIter input_begin, InIter input_end, + FwdIter delims_begin, FwdIter delims_end, Func token_func); // <1> + +template +constexpr void +tokenize(std::string_view input, Container_t &output_container, + std::string_view delimiters = "\t,;: ", bool skip = true); // <2> + +std::vector +split(std::string_view input, + std::string_view delimiters = "\t,;: ", bool skip = true); // <3> +``` +1. Given iterators that bracket the input text and others that bracket the possible token delimiters, this method processes the text and passes each token to a user-supplied function. +2. Tokenizes the input text string and places the tokens into `output_container`. +3. Tokenizes the input text string and returns the tokens as a `std::vector` of strings. + +We have based the `for_each_token` function on the excellent discussion [here](https://tristanbrindle.com/posts/a-quicker-study-on-tokenising/). + +### Function Arguments + +Argument | Description +-------- | ------- +`input_begin` | To tokenize the string stored in `text` `input_begin` should be `std::cbegin(text)`. +`input_end` | To tokenize the string stored in `text` `input_end` should be `std::cend(text)`. +`delims_begin`| If the possible delimiters for the tokens are in the string `delims`, which might be `"\t,;: "`, then `delims_begin` should be `std::cbegin(delims)`. +`delims_end` | If the possible delimiters for the tokens are in the string `delims`, which might be `"\t,;: "`, then `delims_end` should be `std::cend(delims)`. +`token_func` | This will be called for each token: `token_func(token.cbegin(), token.cend())`. +`output_container` | This container needs to be dynamically resizable and support the `emplace_back(token.cbegin(), token.cend())`. +`skip` | If true, we ignore empty tokens (e.g., two spaces in a row). +`delimiters` | These are the characters that should delimit our tokens. Tokens break on white space, commas, semi-colons, and colons by default. +: {.bordered .striped .hover .responsive tbl-colwidths="[20,80]"} + +## Extracting Values + +We also have a function that attempts to parse a value from a string. +```cpp +template +constexpr std::optional possible(std::string_view str); // <1> +``` +1. Tries to read a value of a particular type from a string. + +This function uses the [`std::from_chars`] function to retrieve a possible simple type from a string. +It returns a [`std::nullopt`] if it fails to parse the input. + +[Example]{.bt} +```cpp +auto x = possible(str); +if(x) std::cout << str << ": parsed as the double value " << x << '\n'; +``` +This function tries to fill `x` with a `double` value read from a string and, if successful, print it on `std::cout`. + + +[`std::tolower`]: https://en.cppreference.com/w/cpp/string/byte/tolower +[`std::toupper`]: https://en.cppreference.com/w/cpp/string/byte/toupper +[`std::isspace`]: https://en.cppreference.com/w/cpp/string/byte/isspace +[`std::from_chars`]: https://en.cppreference.com/w/cpp/utility/from_chars +[`std::nullopt`]: https://en.cppreference.com/w/cpp/utility/optional/nullopt \ No newline at end of file diff --git a/docs/content/thousands.qmd b/docs/content/thousands.qmd new file mode 100644 index 0000000..679b5b7 --- /dev/null +++ b/docs/content/thousands.qmd @@ -0,0 +1,84 @@ +{{< include /content/_common.qmd >}} +--- +title: Pretty Print Large Numbers +--- + +## Introduction + +The `` header provides functions that help you print large numbers in a readable format by forcing a stream or locale to insert appropriate commas, e.g., having 23456.7 print as 23,456.7. + +```cpp +utilities::imbue_stream_with_commas(std::ios_base &s = std::cout, bool on = true); // <1> +utilities::imbue_global_with_commas(bool on = true); // <2> +utilities::pretty_print_thousands(bool on = true); // <3> +``` +1. Turns commas on or off for a particular stream. +2. Turns commas on or off for the global locale. +3. Turns commas on or off for the standard streams `std::cout`, `std::cerr`, `std::clog`, and the global locale. + +Ideally, one should be able to rely on facilities in [`std::locale`] to format large numbers per local custom. +For example, using [`std::format`], you might print a large number like this: +```cpp +std::cout << std::format("x = {:L}\n", 23456.7); +``` +The `L` specifier tells the text formatting library to invoke an appropriate facet from the default locale. +Then, you might expect to see `23,456.7` printed on the screen for many locations. + +In practice, the {{< var cpp >}} standard libraries seem to have poor support for resolving locale information. + +Your computer correctly supports locales at the operating system level. +After all, computer manufacturers sell their machines to customers worldwide, and those customers expect to see dates, etc., presented locally. +However, the default locale used in many {{< var cpp >}} implementations is not the default one used by the operating system. +Instead, it is often a complete blank, so rather than seeing `23,456.7` you will get the less readable `23456.7` + +The various `utilities::imbue_xxx_with_commas` functions are a little hack that corrects the deficit. +If you call the final version above `pretty_print_thousands()`, we inject a comma-producing facet into the default locale, and printing large numbers will work as expected. +Calling `pretty_print_thousands(false);` will restore the default locale to its original state. + +::: {.callout-warning} +# Commas only +For now, the only punctuation we support with these functions is the comma. +You should be aware that many countries treat the comma as a decimal point. +::: + +[Example]{.bt} +```cpp +#include +#include + +int main() +{ + double x = 123456789.123456789; + + std::cout << std::format("Using the default locale:\n"); + std::cout << std::format("No locale specifier: {:12.5f}\n", x); + std::cout << std::format("With locale specifier: {:12.5Lf}\n\n", x); // <1> + + utilities::pretty_print_thousands(); + std::cout << std::format("After adding a commas facet:\n"); + std::cout << std::format("No locale specifier: {:12.5f}\n", x); + std::cout << std::format("With locale specifier: {:12.5Lf}\n", x); // <2> +} +``` +1. We might expect to see a readable `x` here but typically will not. +2. We will see a readable `x` here for sure. + +[Output]{.bt} + +```sh +Using the default locale: +No locale specifier: 123456789.12346 +With locale specifier: 123456789.12346 + +After adding a commas facet: +No locale specifier: 123456789.12346 +With locale specifier: 123,456,789.12346 +``` + +### See Also +[`std::format`] \ +[`std::locale`] + + +[`std::locale`]: https://en.cppreference.com/w/cpp/locale/locale +[`std::format`]: https://en.cppreference.com/w/cpp/utility/format/format \ No newline at end of file diff --git a/docs/content/type.qmd b/docs/content/type.qmd new file mode 100644 index 0000000..02c9269 --- /dev/null +++ b/docs/content/type.qmd @@ -0,0 +1,106 @@ +{{< include /content/_common.qmd >}} +--- +title: The Type of an Object +--- + +## Introduction + +The `` heeder defines functions that return a string representing an object's "type" as the compiler/preprocessor sees. + +```cpp +template +constexpr std::string_view utilities::type(); // <1> + +template +constexpr std::string_view utilities::type(const T&); // <2> +``` +1. Returns a string for a type, e.g., `utilities::type` will return `"int"`. +2. Returns a string for the type of a specific object, so if `x` is an `int`, then `utilities::type(x)` will return `"int"`. + +There is no portable way to get a readable string representation of a type in {cpp}, though clearly, the compiler has the information. + +You can use [`std::typeid`] to retrieve the get [`std::type_info`] that has a `name` field. +However, that `name` field is not standardized across compilers, and in some cases, it contains the mangled name, which is not easily readable +Moreover, [`std::typeid`] is not a compile-time call, so no use for template meta-programming. + +We can instead use the compiler's preprocessor. +It generally has some predefined macro that produces a clean-looking signature string when invoked in any function. +The name of the macro we want isn't standardized but `clang` and `GCC` both use `__PRETTY_FUNCTION__` while Microsoft uses `__FUNCSIG__`. +How the macro expands isn't standardized but will be consistent with any compiler. + +The macro string is available at compile time as its expansion is part of the pre-compilation phase. +Using other `constexpr` functions, you can parse the string to get a printable name for any type at compile time. + +::: {.callout-note} +# Not standardized +The type name will be perfectly readable but not identical across compilers. +::: + +[Example]{.bt} +```cpp +#include +#include +#include +#include + +int main() +{ + utilities::stopwatch sw_default; // <1> + utilities::precise_stopwatch sw_precise; + utilities::steady_stopwatch sw_steady; + utilities::system_stopwatch sw_system; + + std::cout << "Compiler: " << COMPILER_NAME << '\n'; // <2> + + std::print("sw_default has type '{}'\n", utilities::type(sw_default)); + std::print("sw_precise has type '{}'\n", utilities::type(sw_precise)); + std::print("sw_steady has type '{}'\n", utilities::type(sw_steady)); + std::print("sw_system has type '{}'\n", utilities::type(sw_system)); +} +``` +1. See [`stopwatch.h`] for details. +2. See [`macros.h`] for details. + +[Output from GCC]{.bt} +```sh +Compiler: gcc 13.2.0 +sw_default has type 'stopwatch' +sw_precise has type 'stopwatch' +sw_steady has type 'stopwatch' +sw_system has type 'stopwatch' +``` +It seems that `libstdc++`, the standard library for `gcc`, only has one clock, namely `std::chrono::system_clock`. +The other clocks in its `std::chrono` must all be aliases for that one. + +[Output from MSVC]{.bt} +```sh +Compiler: MSC 193131104 +sw_default has type 'class utilities::stopwatch' +sw_precise has type 'class utilities::stopwatch' +sw_steady has type 'class utilities::stopwatch' +sw_system has type 'class utilities::stopwatch' +``` +This version of Microsoft Visual Studio Code also uses a single clock, `std::chrono::steady_clock` for our system. + +[Output from clang]{.bt} +```sh +Compiler: clang 17.0.6 +sw_default has type 'utilities::stopwatch<>' +sw_precise has type 'utilities::stopwatch<>' +sw_steady has type 'utilities::stopwatch<>' +sw_system has type 'utilities::stopwatch' +``` +The specific clock type is not printed for the first three objects. + +We have observed that while `clang` uses the same `__PRETTY_FUNCTION__` macro name as `gcc`, its implementation is different, and it never outputs template arguments that match a default. + +For the first three objects above, `clang` outputs `utilities::stopwatch<>` without any reference to the underlying clock. +We conclude that all three use the default specified in [`stopwatch.h`], `std::chrono::high_resolution_clock`. +For this compiler, then `std::chrono:steady_clock` is the same as`std::chrono::high_resolution_clock`. + +However, the type name for the final `sw_system` object references a different `std::chrono::system_clock`. +The standard library `libc++` for `clang` seems to be able to access two different clocks (or at least two that it thinks are different). + + +[`std::typeid`]: https://en.cppreference.com/w/cpp/language/typeid +[`std::type_info`]: https://en.cppreference.com/w/cpp/types/type_info diff --git a/docs/index.qmd b/docs/index.qmd new file mode 100644 index 0000000..9260379 --- /dev/null +++ b/docs/index.qmd @@ -0,0 +1,2 @@ + +{{< include /content/index.qmd >}} \ No newline at end of file diff --git a/examples/check01.cpp b/examples/check01.cpp new file mode 100644 index 0000000..bff5a5e --- /dev/null +++ b/examples/check01.cpp @@ -0,0 +1,16 @@ +/// @brief Throw an uncaught utilities::check_error and see what the message looks like. +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/check.h" + +int +subtract(int x, int y) +{ + check(x == y, "x = {}, y = {}", x, y); + return y - x; +} + +int +main() +{ + return subtract(10, 11); +} \ No newline at end of file diff --git a/examples/check02.cpp b/examples/check02.cpp new file mode 100644 index 0000000..12ca2f7 --- /dev/null +++ b/examples/check02.cpp @@ -0,0 +1,24 @@ +/// @brief Throw and catch a utilities::check_error to see what the message looks like. +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/check.h" +#include + +int +subtract(int x, int y) +{ + always_check(x == y, "x = {}, y = {}", x, y); + return y - x; +} + +int +main() +{ + try { + subtract(10, 11); + } + catch (const std::exception& e) { + std::cout << "Caught an exception:"; + std::cout << e.what(); + } + return 0; +} \ No newline at end of file diff --git a/examples/format01.cpp b/examples/format01.cpp new file mode 100644 index 0000000..34f0e7c --- /dev/null +++ b/examples/format01.cpp @@ -0,0 +1,27 @@ +/// @brief Exercise std::format a little +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/format.h" +#include "utilities/print.h" + +// Here is a completely trivial class with a `to_string()` method. +struct Whatever { + std::string to_string() const { return "Whatever!"; } +}; + +int +main() +{ + std::print("As int: {}, {}, {}, {}\n", -55, -66, -77, -88); + std::print("As hex: {:#x}, {:#x}, {:#x}, {:#x}\n", -55, -66, -77, -88); + std::print("As HEX: {:#X}, {:#X}, {:#X}, {:#X}\n", -55, -66, -77, -88); + + bool a = true; + bool b = false; + std::print("Booleans by default: {} and {}\n", a, b); + std::print("Booleans as strings: {:s} and {:s}\n", a, b); + std::print("Booleans as integers: {:d} and {:d}\n", a, b); + + std::print("Struct with a to_string() method: '{}'\n", Whatever{}); + + return 0; +} \ No newline at end of file diff --git a/examples/format02.cpp b/examples/format02.cpp new file mode 100644 index 0000000..9075820 --- /dev/null +++ b/examples/format02.cpp @@ -0,0 +1,17 @@ +/// @brief Exercise std::format. +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/format.h" +#include "utilities/macros.h" +#include +#include +#include + +int +main() +{ + RUN(std::string s1); + RUN(double x = 123456789.123456789); + RUN(std::format_to(std::back_inserter(s1), "x = {:12.5f}", x), s1); + + return 0; +} diff --git a/examples/format03.cpp b/examples/format03.cpp new file mode 100644 index 0000000..05237cd --- /dev/null +++ b/examples/format03.cpp @@ -0,0 +1,107 @@ +/// @brief Exercise std::format a little. +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/format.h" +#include "utilities/macros.h" +#include +#include + +int +main() +{ + // clang-format off + auto s = std::format( + "A " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "really really really really really really really really really " + "long string"); + + // clang-format on + std::cout << "String s:\n" + << s << "\n" + << "The string has length: " << s.length() << '\n'; + + return 0; +} \ No newline at end of file diff --git a/examples/format04.cpp b/examples/format04.cpp new file mode 100644 index 0000000..10f4899 --- /dev/null +++ b/examples/format04.cpp @@ -0,0 +1,75 @@ +/// @brief Run though some formatting tests - make sure that strings come out as expected +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/format.h" +#include +#include + +#define CHECK_EQUAL(code, expect) \ + code; \ + if (str != expect) { \ + std::cout << "Test failed, line " << __LINE__ << "\n"; \ + std::cout << "str = '" << str << "' instead of expected '" << expect << "'\n"; \ + std::cout << "FAILURE FROM> " << #code << '\n'; \ + ++n_failures; \ + } \ + ++n_tests; + +int +main() +{ + std::string str; + std::size_t n_tests = 0; + std::size_t n_failures = 0; + + // Test the strings we get by parsing various basic formats + CHECK_EQUAL(str = std::format("{:s}", "asdf"), "asdf") + CHECK_EQUAL(str = std::format("{:d}", 1234), "1234") + CHECK_EQUAL(str = std::format("{:d}", -5678), "-5678") + CHECK_EQUAL(str = std::format("{:o}", 012), "12") + CHECK_EQUAL(str = std::format("{:d}", 123456u), "123456") + CHECK_EQUAL(str = std::format("{:x}", 0xdeadbeef), "deadbeef") + CHECK_EQUAL(str = std::format("{:X}", 0xDEADBEEF), "DEADBEEF") + CHECK_EQUAL(str = std::format("{:e}", 1.23456e10), "1.234560e+10") + CHECK_EQUAL(str = std::format("{:E}", -1.23456E10), "-1.234560E+10") + CHECK_EQUAL(str = std::format("{:f}", -9.8765), "-9.876500") + CHECK_EQUAL(str = std::format("{:F}", 9.8765), "9.876500") + CHECK_EQUAL(str = std::format("{:g}", 10.0), "10") + CHECK_EQUAL(str = std::format("{:G}", 100.0), "100") + CHECK_EQUAL(str = std::format("{:c}", 65), "A") + CHECK_EQUAL(str = std::format("{:s}", "asdf_123098"), "asdf_123098") + + // Test the strings we get by parsing booleans as ints or strings + CHECK_EQUAL(str = std::format("{:s}", true), "true") + CHECK_EQUAL(str = std::format("{:d}", true), "1") + + // Test precision & width + CHECK_EQUAL(str = std::format("{:10d}", -10), " -10") + CHECK_EQUAL(str = std::format("{:04d}", 10), "0010") + CHECK_EQUAL(str = std::format("{:10.4f}", 1234.1234567890), " 1234.1235") + CHECK_EQUAL(str = std::format("{:.0f}", 10.49), "10") + CHECK_EQUAL(str = std::format("{:.0f}", 10.51), "11") + CHECK_EQUAL(str = std::format("{:.2s}", "asdf"), "as") + + // Test "flags" + CHECK_EQUAL(str = std::format("{:#x}", 0x271828), "0x271828") + CHECK_EQUAL(str = std::format("{:#o}", 0x271828), "011614050") + CHECK_EQUAL(str = std::format("{:#f}", 3.0), "3.000000") + CHECK_EQUAL(str = std::format("{:010d}", 100), "0000000100") + CHECK_EQUAL(str = std::format("{:010d}", -10), "-000000010") + CHECK_EQUAL(str = std::format("{:#010X}", 0xBEEF), "0X0000BEEF") + CHECK_EQUAL(str = std::format("{:+10d}", 10), " +10") + CHECK_EQUAL(str = std::format("{:-10d}", -10), " -10") + CHECK_EQUAL(str = std::format("{:10d}", -10), " -10") + CHECK_EQUAL(str = std::format("{:<10d}", 10), "10 ") + CHECK_EQUAL(str = std::format("{:<10d}", -10), "-10 ") + + // Complicated string + auto X = static_cast('X'); + CHECK_EQUAL(str = std::format("{:0.10f} - {:04d} - {:+g} - {:s} - {:#X} - {:c}", 1.234, 42, 3.13, + "some string or other", 0XDEAD, X), + "1.2340000000 - 0042 - +3.13 - some string or other - 0XDEAD - X") + + std::cout << "Number of tests = " << n_tests << ", failures = " << n_failures << ".\n"; + + return 0; +} diff --git a/examples/format05.cpp b/examples/format05.cpp new file mode 100644 index 0000000..5013137 --- /dev/null +++ b/examples/format05.cpp @@ -0,0 +1,13 @@ +/// @brief Print the elements from a range. +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/format.h" +#include "utilities/print.h" +#include + +int +main() +{ + std::vector v = {1.123123, 2.1235, 3.555555}; + std::print("Unformatted vector: {}\n", v); + std::print("Formatted vector: {::3.2f}\n", v); +} \ No newline at end of file diff --git a/examples/locale01.cpp b/examples/locale01.cpp new file mode 100644 index 0000000..2264e8b --- /dev/null +++ b/examples/locale01.cpp @@ -0,0 +1,18 @@ +/// @brief Checking support for std::locale in the standard libraries -- GCC is notoriously poor! +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/utilities.h" +#include +#include + +int +main() +{ + + std::locale loc; + std::print("Compiler {}: Default constructed locale is named '{}'\n", COMPILER_NAME, loc.name()); + + int x = 1'000'000; + std::cout << "Printing 1000000 to std::cout yields: " << x << '\n'; + + return 0; +} \ No newline at end of file diff --git a/examples/locale02.cpp b/examples/locale02.cpp new file mode 100644 index 0000000..74f777e --- /dev/null +++ b/examples/locale02.cpp @@ -0,0 +1,37 @@ +/// @brief Checking support for std::locale in the standard libraries -- GCC is notoriously poor! +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/utilities.h" +#include + +int +main() +{ + unsigned x_val = 1'000'000; + std::string x_str = "1000000"; + + std::print("Using the default locale.\n"); + std::cout << "Sending " << x_str << " to std::cout yields: " << x_val << '\n'; + std::print("std::print({}) with :L specifier yields: {:L}\n", x_str, x_val); + std::print("std::print({}) w/o :L specifier yields: {}\n\n", x_str, x_val); + + std::print("Turning commas on for std::cout ...\n"); + RUN(utilities::imbue_stream_with_commas(std::cout)); + std::cout << "Sending " << x_str << " to std::cout yields: " << x_val << '\n'; + std::print("std::print({}) with :L specifier yields: {:L}\n", x_str, x_val); + std::print("std::print({}) w/o :L specifier yields: {}\n\n", x_str, x_val); + + std::print("Also turning commas on for the global locale ...\n"); + RUN(utilities::imbue_global_with_commas(true)); + std::cout << "Sending " << x_str << " to std::cout yields: " << x_val << '\n'; + std::print("std::print({}) with :L specifier yields: {:L}\n", x_str, x_val); + std::print("std::print({}) w/o :L specifier yields: {}\n\n", x_str, x_val); + + std::print("Turning commas off ...\n"); + RUN(utilities::imbue_stream_with_commas(std::cout, false)); + RUN(utilities::imbue_global_with_commas(false)); + std::cout << "Sending " << x_str << " to std::cout yields: " << x_val << '\n'; + std::print("std::print({}) with :L specifier yields: {:L}\n", x_str, x_val); + std::print("std::print({}) w/o :L specifier yields: {}\n\n", x_str, x_val); + + return 0; +} \ No newline at end of file diff --git a/examples/locale03.cpp b/examples/locale03.cpp new file mode 100644 index 0000000..1870e0c --- /dev/null +++ b/examples/locale03.cpp @@ -0,0 +1,30 @@ +/// @brief Checking support for std::locale in the standard libraries -- GCC is notoriously poor! +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/utilities.h" +#include + +int +main() +{ + unsigned x_val = 1'000'000; + std::string x_str = "1000000"; + + std::print("Using the default locale.\n"); + std::cout << "Sending " << x_str << " to std::cout yields: " << x_val << '\n'; + std::print("std::print({}) with :L specifier yields: {:L}\n", x_str, x_val); + std::print("std::print({}) w/o :L specifier yields: {}\n\n", x_str, x_val); + + std::print("Turning commas on for the usual output streams...\n"); + RUN(utilities::pretty_print_thousands()); + std::cout << "Sending " << x_str << " to std::cout yields: " << x_val << '\n'; + std::print("std::print({}) with :L specifier yields: {:L}\n", x_str, x_val); + std::print("std::print({}) w/o :L specifier yields: {}\n\n", x_str, x_val); + + std::print("Turning commas off ...\n"); + RUN(utilities::pretty_print_thousands(false)); + std::cout << "Sending " << x_str << " to std::cout yields: " << x_val << '\n'; + std::print("std::print({}) with :L specifier yields: {:L}\n", x_str, x_val); + std::print("std::print({}) w/o :L specifier yields: {}\n\n", x_str, x_val); + + return 0; +} \ No newline at end of file diff --git a/examples/locale04.cpp b/examples/locale04.cpp new file mode 100644 index 0000000..ed17f37 --- /dev/null +++ b/examples/locale04.cpp @@ -0,0 +1,29 @@ +/// @brief Checking support for std::locale in the standard libraries -- GCC is notoriously poor! +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/utilities.h" +#include + +int +main(int argc, char* argv[]) +{ + // Must have one arg -- the name of the locale we wish to use. + if (argc != 2) { + std::print("Missing argument -- usage: '{} locale-name (e.g. 'en_US')\n", argv[0]); + exit(1); + } + + int x = 1'000'000; + std::cout << "Printing 1000000 to std::cout yields: " << x << '\n'; + + try { + std::locale loc(argv[1]); + std::cout.imbue(loc); + std::print("Setting the locale to '{}'\n", loc.name()); + std::cout << "Printing 1000000 to std::cout now yields: " << x << '\n'; + } + catch (std::runtime_error& e) { + std::print("Failed to set the locale to: '{}'\n", argv[1]); + std::print("Exception raised: '{}'\n", e.what()); + } + return 0; +} \ No newline at end of file diff --git a/examples/log01.cpp b/examples/log01.cpp new file mode 100644 index 0000000..1877d8b --- /dev/null +++ b/examples/log01.cpp @@ -0,0 +1,34 @@ +/// @brief Exercise the LOG/DBG macros. +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/log.h" + +int +add(int x, int y) +{ + DBG("This is a message that only gets printed if the DEBUG flag was set at compile time."); + LOG("This is a log message that gets printed *unless* the NO_LOGS flag is set at compile time."); + return y + x; +} + +int +subtract(int x, int y) +{ + // This should put out the source location followed by ": ..." + LOG("x = {}, y = {}", x, y); + return y - x; +} + +int +main() +{ +#ifdef DEBUG + std::cout << "The DEBUG flag is set!\n"; +#endif +#ifdef NDEBUG + std::cout << "The NDEBUG flag is set!\n"; +#endif + + // LOG without args just prints the source location. + LOG(); + return add(10, 11) + subtract(10, 11); +} \ No newline at end of file diff --git a/examples/log02.cpp b/examples/log02.cpp new file mode 100644 index 0000000..de59bc2 --- /dev/null +++ b/examples/log02.cpp @@ -0,0 +1,20 @@ +/// @brief Exercise the LOG/DBG macros. +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include +#include "utilities/log.h" + +template +struct Foo { + static std::string foo(std::string_view x) + { + LOG("Here I am with x = {}", x); + return "RANDOM STRING"; + } +}; + +int +main() +{ + Foo<>::foo("42"); + return 0; +} \ No newline at end of file diff --git a/examples/log03.cpp b/examples/log03.cpp new file mode 100644 index 0000000..1705584 --- /dev/null +++ b/examples/log03.cpp @@ -0,0 +1,18 @@ +/// @brief Exercise the LOG/DBG macros. +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/log.h" + +template +auto +fun(T x) +{ + LOG("value of x = {}", x); + return x; +} + +int +main(int, char*[]) +{ + LOG("Hello world!"); + fun("Hello C++20!"); +} \ No newline at end of file diff --git a/examples/macros01.cpp b/examples/macros01.cpp new file mode 100644 index 0000000..b200b8f --- /dev/null +++ b/examples/macros01.cpp @@ -0,0 +1,26 @@ +/// @brief Exercise the OVERLOAD macro in a trivial way. +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/macros.h" +#include + +/// @brief A trivial macro we will overload depending on the number of arguments it is given +#define COUT(...) OVERLOAD(COUT, __VA_ARGS__) + +// The specific one, two, and three argument versions of the COUT test macro +#define COUT0(x) std::cout << "Zero argument version: " << '\n' +#define COUT1(x) std::cout << "One argument version: " << x << '\n' +#define COUT2(x, y) std::cout << "Two arguments version: " << x << ", " << y << '\n' +#define COUT3(x, y, z) std::cout << "Three arguments version: " << x << ", " << y << ", " << z << '\n' + +int +main() +{ + std::cout << "Compiler: " << COMPILER_NAME << '\n'; + + // NOTE: We only ever call COUT but expect to get the correct concrete version. + COUT(); + COUT("x"); + COUT("x", 2); + COUT("x", 2, 'z'); + return 0; +} \ No newline at end of file diff --git a/examples/scratch01.cpp b/examples/scratch01.cpp new file mode 100644 index 0000000..aa29eee --- /dev/null +++ b/examples/scratch01.cpp @@ -0,0 +1,13 @@ +#include +#include + +struct Whatever { + std::string to_string() const { return "Whatever!"; } +}; + +int main() +{ + Whatever w; + std::cout << std::format("Struct with a to_string() method: '{}'\n", w); + return 0; +} \ No newline at end of file diff --git a/examples/scratch02.cpp b/examples/scratch02.cpp new file mode 100644 index 0000000..f4f68b5 --- /dev/null +++ b/examples/scratch02.cpp @@ -0,0 +1,18 @@ +#include "utilities/log.h" +#include + +template +struct Foo { + static std::string foo(std::string_view x) + { + LOG("Here I am with x = {}", x); + return "ALPHA"; + } +}; + +int +main() +{ + Foo<>::foo("42"); + return 0; +} \ No newline at end of file diff --git a/examples/stopwatch01.cpp b/examples/stopwatch01.cpp new file mode 100644 index 0000000..8a3ac99 --- /dev/null +++ b/examples/stopwatch01.cpp @@ -0,0 +1,23 @@ +/// @brief Simple test of the stopwatch class. +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/format.h" +#include "utilities/print.h" +#include "utilities/stopwatch.h" + +#include +#include + +int +main() +{ + using namespace std::literals; + + auto sleep_duration = 1s; + std::cout << "Sleeping for " << utilities::to_seconds(sleep_duration) << "s ...\n"; + const utilities::stopwatch stopwatch; + std::this_thread::sleep_for(sleep_duration); + std::cout << "Elapsed time: " << stopwatch << '\n'; + + /// Check that the fmt library can handle a stopwatch + std::print("And here is what 'std::print' puts out for elapsed time: {}\n", stopwatch); +} diff --git a/examples/stopwatch02.cpp b/examples/stopwatch02.cpp new file mode 100644 index 0000000..87d935d --- /dev/null +++ b/examples/stopwatch02.cpp @@ -0,0 +1,32 @@ +/// @brief Another simple test of the stopwatch class +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/format.h" +#include "utilities/print.h" +#include "utilities/stopwatch.h" + +#include + +int +main() +{ + using namespace std::literals; + + utilities::stopwatch sw("Overhead stopwatch"); + for (auto sleep_duration = 0ms; sleep_duration <= 2s; sleep_duration += 200ms) { + + sw.click(); + std::this_thread::sleep_for(sleep_duration); + sw.click(); + + double sleep_ms = 1000 * utilities::to_seconds(sleep_duration); + double actual_ms = 1000 * sw.lap(); + double diff = actual_ms - sleep_ms; + double percent = sleep_ms != 0 ? 100 * diff / sleep_ms : 0; + + std::print("Requested sleep for {:8.2f}ms, measured wait was {:8.2f}ms => overhead {:.2f}ms ({:.2f}%)\n", + sleep_ms, actual_ms, diff, percent); + } + std::cout << sw << '\n'; + + return 0; +} diff --git a/examples/stream.txt b/examples/stream.txt new file mode 100644 index 0000000..1818cac --- /dev/null +++ b/examples/stream.txt @@ -0,0 +1,37 @@ +# Trump suit S = 0, H = 1, D = 2, C = 3, NT = 4 +1 + +# Tricks left to play +13 + +# Tricks that N-S must win +13 + +# Who is on lead N = 0, E = 1, S = 2, W = 3 +3 + +# North's hand +3C \ +3H 2H \ # Spurious spaces +AD KD 2D \ +AS KS 7S 6S 5S 4S 3S + +# East's hand +8C 7C 6C 5C 4C \ +QH TH 8H 7H \ +8D 7D 6D \ +2S + +# South's hand +AC QC JC 9C \ +AH KH JH 9H 6H 5H \ # Spurious comment +5D 4D 3D + +# West's hand +KC TC 2C \ +4H \ +QD JD TD 9D \ +QS JS TS 9S 8S + +# The opening lead (XX to have the computer hunt for an optimal lead) +QS diff --git a/examples/stream01.cpp b/examples/stream01.cpp new file mode 100644 index 0000000..03f12fb --- /dev/null +++ b/examples/stream01.cpp @@ -0,0 +1,26 @@ +/// @brief Read from a file line by line +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/utilities.h" +#include + + +int +main(int argc, char* argv[]) +{ + // Must have exactly 1 argument (name of file to read from) + if (argc != 2) check_failed("Usage: '{} ' -- missing filename argument!", argv[0]); + + // Try to open the file + std::ifstream file{argv[1]}; + if (!file) check_failed("Failed to open file '{}'", argv[1]); + + // Read all the lines in the file and the print them sans comments ... + std::string line; + std::size_t n_lines = 0; + while (utilities::read_line(file, line) != 0) { + n_lines++; + std::print("Line #{:d}: '{}'\n", n_lines, line); + } + + return 0; +} diff --git a/examples/stream02.cpp b/examples/stream02.cpp new file mode 100644 index 0000000..039c62f --- /dev/null +++ b/examples/stream02.cpp @@ -0,0 +1,28 @@ +/// @brief Read from a file and tokenize all the lines. +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/utilities.h" +#include + +int +main(int argc, char* argv[]) +{ + // Must have exactly 1 argument (name of file to read from) + if (argc != 2) check_failed("Usage: '{} ' -- missing filename argument!", argv[0]); + + // Try to open the file + std::ifstream file{argv[1]}; + if (!file) check_failed("Failed to open file '{}'", argv[1]); + + std::size_t n_line = 0; + std::string line; + while (utilities::read_line(file, line) != 0) { + n_line++; + std::print("Line {:2d}: '{}'\n", n_line, line); + auto tokens = utilities::split(line); + std::size_t n_tokens = tokens.size(); + std::print("{} token(s):\n", n_tokens); + std::print("{}\n", tokens); + } + + return 0; +} diff --git a/examples/stream03.cpp b/examples/stream03.cpp new file mode 100644 index 0000000..91c3580 --- /dev/null +++ b/examples/stream03.cpp @@ -0,0 +1,43 @@ +/// @brief Read from a file see how long it takes to account for comment lines vs. ignoring them +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/utilities.h" +#include + +int +main(int argc, char* argv[]) +{ + // Must have exactly 1 argument (name of file to read from) + if (argc != 2) check_failed("Usage: '{} ' -- missing filename argument!", argv[0]); + + // Try to open the file + std::ifstream file{argv[1]}; + if (!file) check_failed("Failed to open file '{}'", argv[1]); + + // And we're off ... + utilities::imbue_stream_with_commas(std::cout); + std::print("File '{}' has {} lines, of which {} are non-comment lines.\n", argv[1], utilities::line_count(file, ""), + utilities::line_count(file, "#")); + + utilities::stopwatch sw; + std::size_t n_trials = 100'000; + std::cout << "Trials: " << n_trials << '\n'; + std::print("Running {:L} trials where each trial counts ALL the lines ", n_trials, argv[1]); + sw.click(); + for (std::size_t i = 0; i < n_trials; ++i) { + utilities::line_count(file, ""); + utilities::rewind(file); + } + sw.click(); + std::print("took: {:8.2f}ms.\n", 1000 * sw.lap()); + + std::print("Running {:L} trials where each trial counts the non-comment lines only ", n_trials, argv[1]); + sw.click(); + for (std::size_t i = 0; i < n_trials; ++i) { + utilities::line_count(file, "#"); + utilities::rewind(file); + } + sw.click(); + std::print("took: {:8.2f}ms.\n", 1000 * sw.lap()); + + return 0; +} diff --git a/examples/string01.cpp b/examples/string01.cpp new file mode 100644 index 0000000..759347c --- /dev/null +++ b/examples/string01.cpp @@ -0,0 +1,15 @@ +/// @brief Basic check of some functions in utilities/string.h +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/utilities.h" + +int +main() +{ + std::string str; + while (std::cout << "Text (x to exit)? " && std::getline(std::cin, str)) { + if (str == "X" || str == "x") break; + std::print("upper_cased('{}') = '{}'\n", str, utilities::upper_cased(str)); + std::print("standardized('{}') = '{}'\n", str, utilities::standardized(str)); + } + return 0; +} \ No newline at end of file diff --git a/examples/string02.cpp b/examples/string02.cpp new file mode 100644 index 0000000..5037586 --- /dev/null +++ b/examples/string02.cpp @@ -0,0 +1,14 @@ +/// @brief Basic check of some functions in utilities/string.h +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/utilities.h" + +int +main() +{ + std::string x; + while (std::cout << "Text with whitespace to compact (x to exit)? " && std::getline(std::cin, x)) { + if (x == "X" || x == "x") break; + std::print("condensed('{}') = '{}'\n", x, utilities::condensed(x)); + } + return 0; +} \ No newline at end of file diff --git a/examples/type01.cpp b/examples/type01.cpp new file mode 100644 index 0000000..997b2cc --- /dev/null +++ b/examples/type01.cpp @@ -0,0 +1,20 @@ +/// @brief Basic check of the utilities::type(...) function +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/utilities.h" + +int +main() +{ + utilities::stopwatch sw_default; + utilities::precise_stopwatch sw_precise; + utilities::steady_stopwatch sw_steady; + utilities::system_stopwatch sw_system; + + std::cout << "Compiler: " << COMPILER_NAME << '\n'; + std::print("sw_default has type '{}'\n", utilities::type(sw_default)); + std::print("sw_precise has type '{}'\n", utilities::type(sw_precise)); + std::print("sw_steady has type '{}'\n", utilities::type(sw_steady)); + std::print("sw_system has type '{}'\n", utilities::type(sw_system)); + + return 0; +} \ No newline at end of file diff --git a/examples/type02.cpp b/examples/type02.cpp new file mode 100644 index 0000000..af1cbaa --- /dev/null +++ b/examples/type02.cpp @@ -0,0 +1,21 @@ +/// @brief What do the various compilers do with default template parameters? +/// @copyright Copyright (c) 2024 Nessan Fitzmaurice +#include "utilities/utilities.h" + +// Trivial template with a default type +template +struct trivial {}; + +int +main() +{ + std::print("Compiler: {}\n", COMPILER_NAME); + std::print("trivial: '{}'\n", utilities::type>()); + std::print("trivial: '{}'\n", utilities::type>()); + std::print("trivial: '{}'\n", utilities::type>()); + + // With gcc you can get different output if you pop the next line to the top + std::print("trivial<>: '{}'\n", utilities::type>()); + + return 0; +} diff --git a/include/utilities/check.h b/include/utilities/check.h new file mode 100644 index 0000000..169b2f2 --- /dev/null +++ b/include/utilities/check.h @@ -0,0 +1,66 @@ +/// @brief Three replacements for the standard `assert(condition)` macro that add an informational message. +/// @link https://nessan.github.io/utilities/ +/// SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice +/// SPDX-License-Identifier: MIT +#pragma once + +#include +#include +#include + +/// @brief Throw a check_error exception defined below. Often not caught, so ends the program. +#define check_failed(...) \ + throw check_error { __func__, __FILE__, __LINE__, std::format(__VA_ARGS__) } + +/// @def The `always_check` macro cannot be switched off with compiler flags. +#define always_check(cond, ...) \ + if (!(cond)) check_failed("Statement '{}' is NOT true: {}\n", #cond, std::format(__VA_ARGS__)) + +/// @def The `debug_check` macro expands to a no-op *unless* the `DEBUG` flag is set. +#ifdef DEBUG + #define debug_check(cond, ...) always_check(cond, __VA_ARGS__) +#else + #define debug_check(cond, ...) void(0) +#endif + +/// @def The `check`macro expands to a no-op *only if* the `NDEBUG` flag is set. +#ifdef NDEBUG + #define check(cond, ...) void(0) +#else + #define check(cond, ...) always_check(cond, __VA_ARGS__) +#endif + +/// @brief Our error exceptions capture the location where the exception was created and optionally a payload string. +/// @note These are created by the `check_failed(...)` macro above that inserts the needed location information. +class check_error : public std::logic_error { +public: + check_error(std::string_view func, std::string_view path, std::size_t line, std::string_view payload = "") : + std::logic_error(to_string(func, path, line, payload)) + { + // Empty body--we just needed to populate the parent `std::logic_error`'s `what()` string. + } + + /// @brief Returns the whole exception as a string e.g. "[ERROR] 'foobar' foo.cpp line 25: x = 10, y = 11". + static std::string to_string(std::string_view func, std::string_view path, std::size_t line, + std::string_view payload) + { + auto retval = std::format("\n[ERROR] In function '{}' ({}, line {})", func, filename(path), line); + if (!payload.empty()) { + retval += ":\n"; + retval += payload; + } + return retval; + } + + /// @brief Reduce a full path to just the filename. + static std::string filename(std::string_view path) + { + char sep = '/'; +#ifdef _WIN32 + sep = '\\'; +#endif + auto i = path.rfind(sep, path.length()); + if (i != std::string::npos) return std::string{path.substr(i + 1, path.length() - i)}; + return ""; + } +}; diff --git a/include/utilities/format.h b/include/utilities/format.h new file mode 100644 index 0000000..d37104c --- /dev/null +++ b/include/utilities/format.h @@ -0,0 +1,110 @@ +/// @brief Connect any class with a `std::string to_string() const` method to `std::format`. +/// @note We also include a work-around support for formatting ranges (shouldn't be needed in C++23). +/// @link https://nessan.github.io/utilities/ +/// SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice +/// SPDX-License-Identifier: MIT +#pragma once + +#include +#include +#include + +/// @brief A concept that matches any type that has an accessible `std::string to_string() const` method. +template +concept has_to_string_method = requires(const T& x) { + { x.to_string() } -> std::convertible_to; +}; + +/// @brief Connect any type that has an accessible `std::string to_string() const` method to `std::format +template +struct std::formatter { + template + auto format(const T& rhs, FormatContext& ctx) const + { + return std::format_to(ctx.out(), "{}", rhs.to_string()); + } + + constexpr auto parse(const std::format_parse_context& ctx) + { + // Throw an error for anything that is not default formatted. + auto it = ctx.begin(); + assert(it == ctx.end() || *it == '}'); + return it; + } +}; + +// -------------------------------------------------------------------------------------------------------------------- +// Work-around adds a std::formatter for std::ranges (explicitly avoiding strings which already have a formatter). +// This will go away once std::format properly supports std:ranges. +// Based On: https://github.com/Apress/beginning-cpp23/Workarounds/format_ranges_workaround.h with fixes. +// -------------------------------------------------------------------------------------------------------------------- +#ifndef __cpp_lib_format_ranges + +#include +#include + +namespace utilities { +/// @brief A concept that captures an associative container type. +template +concept is_dictionary = requires { typename T::key_type; }; + +/// @brief A concept that captures an associative container type. +template +concept is_char = std::same_as || std::same_as; + +/// @brief A concept that roughly captures any string type. +/// @note std::format already handles strings well so we want to NOT capture them below. +template + concept is_string = // A "string" for our purposes + is_char> // is a range of characters + && (!requires { typename T::value_type; } // that is either not a container (has no value_type member) + || requires { typename T::traits_type; }); // or a std::basic_string<> (has a traits_type member) +} // namespace utilities + +/// @brief A formatter for a std::range (but deliberately excluding strings which already handled by std::format). +/// @note By default arrays are printed [a0, a1, a3, ...] +/// @note You can suppress the '[' & ']' delimiters by adding a 'n' to the format spec. +template + requires(!utilities::is_string) +struct std::formatter : public std::formatter> { + constexpr auto parse(std::format_parse_context& ctx) + { + auto pos = ctx.begin(); + while (pos != ctx.end() && *pos != '}') { + if (*pos == ':') { + ctx.advance_to(++pos); + return std::formatter>::parse(ctx); + } + else if (*pos == 'n') { + m_surround = false; + } + ++pos; + } + return pos; + } + + template + auto format(const T& range, FormatContext& ctx) const + { + auto pos = ctx.out(); + if (m_surround) { + *pos++ = '['; + ctx.advance_to(pos); + } + bool sep = false; + for (auto&& value : range) { + if (std::exchange(sep, true)) { + *pos++ = ','; + *pos++ = ' '; + } + pos = std::formatter>::format(value, ctx); + ctx.advance_to(pos); + } + if (m_surround) *pos++ = ']'; + return pos; + } + + bool m_surround = true; +}; + +#endif // End of #ifndef __cpp_lib_format_ranges block. \ No newline at end of file diff --git a/include/utilities/log.h b/include/utilities/log.h new file mode 100644 index 0000000..51365f7 --- /dev/null +++ b/include/utilities/log.h @@ -0,0 +1,97 @@ +/// @brief Simple log & debug messages +/// @link https://nessan.github.io/utilities/ +/// SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice +/// SPDX-License-Identifier: MIT +#pragma once + +#include +#include +#include + +/// @brief Create and dispatch a debug messages -- only ever printed if the DEBUG flag is set at compile time. +#ifdef DEBUG + #define DBG(...) MAKE_MESSAGE("DBG", __VA_ARGS__).dispatch() +#else + #define DBG(...) void(0) +#endif + +/// @brief Create and dispatch a log message. Can be turned off by setting NO_LOGS at compile time. +#ifndef NO_LOGS + #define LOG(...) MAKE_MESSAGE("LOG", __VA_ARGS__).dispatch() +#else + #define LOG(...) void(0) +#endif + +/// @brief Messages (instances of the `utilities::message` class below) are constructed using the MAKE_MESSAGE macro. +#define MAKE_MESSAGE(type, ...) \ + utilities::message { __func__, __FILE__, __LINE__, type __VA_OPT__(, std::format(__VA_ARGS__)) } + +namespace utilities { + +/// @brief A utilities::message captures a location where the message was created and optionally a payload string. +/// @note These are created by the `MAKE_MESSAGE(...)` macro above that inserts the needed location information. +class message { +public: + /// @brief Messages are handled by a function with this signature -- there is just one handler at a time. + /// @note The default handler just prints the message to a default stream. + using handler_type = void(const message&); + + /// @brief Class method that lets you set a custom message handling function. + static handler_type*& handler() { return c_handler; } + + /// @brief Class method that sets the message handler back to the default value. + /// @note The default handler just prints the messages to a default stream + static void use_default_handler() { c_handler = default_handler; } + + /// @brief The stream used by the default message handler (you can set it to say a file stream instead). + inline static std::ostream* stream = &std::cout; + + /// @brief These objects are really created using a macro that inserts the needed source code location information. + message(std::string_view func, std::string_view path, std::size_t line, std::string_view type, + std::string_view payload = "") : + m_function{func}, m_filename{filename(path)}, m_line{line}, m_type{type}, m_payload{payload} + { + // Empty body. + } + + /// @brief Returns the whole message as a string e.g. "[DEBUG] 'foobar' foo.cpp line 25: x = 10, y = 11". + std::string to_string() const + { + auto retval = std::format("[{}] function '{}' ({}, line {})", m_type, m_function, m_filename, m_line); + if (!m_payload.empty()) { + retval += ": "; + retval += m_payload; + } + return retval; + } + + /// @brief Dispatch this message to the message handler. + void dispatch() const { c_handler(*this); } + + /// @brief The default message handler just prints the message to the default stream. + static void default_handler(const message& message) { *stream << message.to_string() << std::endl; } + + /// @brief Reduce a full path to just the filename. + static std::string filename(std::string_view path) + { + char sep = '/'; +#ifdef _WIN32 + sep = '\\'; +#endif + auto i = path.rfind(sep, path.length()); + if (i != std::string::npos) return std::string{path.substr(i + 1, path.length() - i)}; + return ""; + } + +private: + std::string m_function; // Function/method where the message originated from. + std::string m_filename; // Filename where the message originated from (just the filename not the path). + std::size_t m_line; // Line in the file where the message originated. + std::string m_type; // The type of this message e.g. "DEBUG". + std::string m_payload; // Any user supplied string that goes with this message. + + // User can set the a handler for all messages -- the default just prints the message to the default stream. + inline static handler_type* c_handler = default_handler; +}; + +} // namespace utilities diff --git a/include/utilities/macros.h b/include/utilities/macros.h new file mode 100644 index 0000000..bba206e --- /dev/null +++ b/include/utilities/macros.h @@ -0,0 +1,66 @@ +/// @brief Some useful common macros. +/// @link https://nessan.github.io/utilities/ +/// NOTE: MSVC's traditional preprocessor barfs on these macros but their newer cross platform compatible one is fine. +/// To use the upgrade, add the '/Zc:preprocessor' flag at compile time. +/// Our CMake module `compiler_init` does that for you. +/// SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice +/// SPDX-License-Identifier: MIT +#pragma once + +/// @brief Invoke the pre-processor stringizing operator but fully expanding any macro argument first! +#define STRINGIZE(s) STRINGIZE_IMPL(s) +#define STRINGIZE_IMPL(s) #s + +/// @brief Concatenate two symbols but making sure to fully expand those symbols if they happen to be macros themselves. +#define CONCAT(a, b) CONCAT_IMPL(a, b) +#define CONCAT_IMPL(a, b) a##b + +/// @brief Turn a semantic version into a string (we overload to handle 1, 2, or 3 argument versions) +#define VERSION_STRING(...) OVERLOAD(VERSION_STRING, __VA_ARGS__) + +// The actual one, two, and three argument versions of that macro +// NOTE: In C++ contiguous strings are concatenated so "2" "." "3" is the same as "2.3" +#define VERSION_STRING1(major) STRINGIZE(major) +#define VERSION_STRING2(major, minor) STRINGIZE(major) "." STRINGIZE(minor) +#define VERSION_STRING3(major, minor, patch) STRINGIZE(major) "." STRINGIZE(minor) "." STRINGIZE(patch) + +/// @brief RUN(code); prints the line of code to the console and then executes it. +/// @note This is a an overloaded macro that is used in some test/example codes to show what specific code is getting +/// executed, optionally followed by one or two results from that call. +#define RUN(...) OVERLOAD(RUN, __VA_ARGS__) + +// The one, two,and three argument versions of RUN +#define RUN1(code) \ + std::cout << "[CODE] " << #code << "\n"; \ + code + +#define RUN2(code, val) \ + RUN1(code); \ + std::cout << "[RESULT] " << #val << ": " << val << '\n' + +#define RUN3(code, val1, val2) \ + RUN1(code); \ + std::cout << "[RESULT] " << #val1 << ": " << val1 << " and " << #val2 << ": " << val2 << '\n' + +/// @brief Preprocessor trickery to allow for macros that can be overloaded by the number of passed arguments. +/// @example #define FOO(...) OVERLOAD(FOO, __VA_ARGS__) will make 'FOO' overloaded on the number of passed args. +//// So FOO() will call the zero arg version FOO0(), FOO(a) will call FOO1(a), FOO(a,b) will call FOO2(a,b) etc. +/// @note You supply whichever specific FOO0(), FOO1(a), FOO2(a,b), FOO2(a,b,c), implementations that make sense, +/// but the consumer can just call FOO(...) and automatically get the correct one. +#define OVERLOAD(macro, ...) CONCAT(macro, ARG_COUNT(__VA_ARGS__))(__VA_ARGS__) + +/// @brief ARG_COUNT(...) expands to the count of its arguments e.g. ARG_COUNT(x,y,z) will expands to 3. +#define ARG_COUNT(...) ARG_COUNT_IMPL(__VA_ARGS__ __VA_OPT__(, ) 9, 8, 7, 6, 5, 4, 3, 2, 1, 0) +#define ARG_COUNT_IMPL(_1, _2, _3, _4, _5, _6, _7, _8, _9, count, ...) count + +/// @brief Compiler name & version as a string -- occasionally useful to have around to annotate test output etc. +/// @note Could add more compilers from e.g. https://github.com/cpredef/predef/blob/master/Compilers.md) +#if defined(_MSC_VER) + #define COMPILER_NAME "MSC " STRINGIZE(_MSC_FULL_VER) +#elif defined(__clang__) + #define COMPILER_NAME "clang " VERSION_STRING(__clang_major__, __clang_minor__, __clang_patchlevel__) +#elif defined(__GNUC__) + #define COMPILER_NAME "gcc " VERSION_STRING(__GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__) +#else + #define COMPILER_NAME "Unidentified Compiler" +#endif diff --git a/include/utilities/print.h b/include/utilities/print.h new file mode 100644 index 0000000..3598a5f --- /dev/null +++ b/include/utilities/print.h @@ -0,0 +1,47 @@ +/// @brief Work-around support for `std::print` (shouldn't be needed in C++23). +/// @link https://github.com/Apress/beginning-cpp23/blob/main/Workarounds/print_workaround.h +/// @link https://nessan.github.io/utilities/ +#pragma once + +#ifdef __cpp_lib_print + #include +#else + +// clang-format off +#include +#include +// clang-format on + +namespace std { +template +void +print(const format_string format, Args&&... args) +{ + std::format_to(std::ostreambuf_iterator(std::cout), format, std::forward(args)...); +} + +template +void +println(const format_string format, Args&&... args) +{ + print(format, std::forward(args)...); + std::cout << '\n'; +} + +template +void +print(const wformat_string format, Args&&... args) +{ + std::format_to(std::ostreambuf_iterator(std::wcout), format, std::forward(args)...); +} + +template +void +println(const wformat_string format, Args&&... args) +{ + print(format, std::forward(args)...); + std::wcout << L'\n'; +} +} // namespace std + +#endif // End of the __cpp_lib_print == false block. diff --git a/include/utilities/stopwatch.h b/include/utilities/stopwatch.h new file mode 100644 index 0000000..80d954a --- /dev/null +++ b/include/utilities/stopwatch.h @@ -0,0 +1,102 @@ +/// @brief Simple utility class that can time how long code blocks take to execute. +/// @link https://nessan.github.io/utilities/ +/// SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice +/// SPDX-License-Identifier: MIT +#pragma once + +#include +#include +#include + +namespace utilities { + +template +class stopwatch { +public: + /// @brief The underlying clock type + using clock_type = Clock; + + /// @brief A stopwatch can have a name to distinguish it from others you may have running + explicit stopwatch(const std::string &str = "") : m_name(str) { reset(); } + + /// @brief Read-only access to the stopwatch's name + std::string name() const { return m_name; } + + /// @brief Read-write access to the stopwatch's name + std::string &name() { return m_name; } + + /// @brief Set/reset the stopwatch's 'zero' point & clear any measured splits. + constexpr void reset() + { + m_zero = clock_type::now(); + m_split = 0; + m_prior = 0; + } + + /// @brief Get the time that has passed from the zero point to now. Units are seconds. + constexpr double elapsed() const + { + std::atomic_thread_fence(std::memory_order_relaxed); + auto t = clock_type::now(); + std::atomic_thread_fence(std::memory_order_relaxed); + return std::chrono::duration(t - m_zero).count(); + } + + /// @brief Clicks the stopwatch to create a new 'split'. + /// @return The elapsed time to the click in seconds. + constexpr double click() + { + auto tau = elapsed(); + m_prior = m_split; + m_split = tau; + return m_split; + } + + /// @brief Returns the split as the time in seconds that elapsed from the zero point to the last click. + constexpr double split() const { return m_split; } + + /// @brief Returns the last 'lap' time in seconds (i.e. the time between prior 2 splits). + constexpr double lap() const { return m_split - m_prior; } + + /// @brief Get a string representation of the stopwatch's elapsed time. + std::string to_string() const { + auto tau = elapsed(); + if(m_name.empty()) return std::format("{}s", tau); + return std::format("{}: {}s", m_name, tau); + } + +private: + using time_point = typename Clock::time_point; + + std::string m_name; // Name of the stopwatch. + time_point m_zero; // The stopwatch measures time (in seconds) from this time point. + double m_split; // The total seconds to when the stopwatch was most recently clicked. + double m_prior; // The prior split. +}; + +/// @brief Usual output operator. Prints the name of the stopwatch if any followed by the elapsed time in seconds. +template +inline std::ostream & +operator<<(std::ostream &os, const stopwatch &rhs) +{ + return os << rhs.to_string(); +} + +/// @brief Small convenience function that converts a std::chrono::duration to seconds in a double. +template +constexpr double +to_seconds(const std::chrono::duration &d) +{ + return std::chrono::duration(d).count(); +} + +/// @brief stopwatch specialization: The most precise stopwatch -- may get put off by system reboots etc. +using precise_stopwatch = stopwatch; + +/// @brief stopwatch specialization: A stopwatch that is guaranteed to be monotonic. +using steady_stopwatch = stopwatch; + +/// @brief stopwatch specialization: A stopwatch that is uses the system clock. +using system_stopwatch = stopwatch; + +} // namespace utilities diff --git a/include/utilities/stream.h b/include/utilities/stream.h new file mode 100644 index 0000000..cfc64b4 --- /dev/null +++ b/include/utilities/stream.h @@ -0,0 +1,116 @@ +/// @brief Some utility functions for that can be used to read from streams. +/// @link https://nessan.github.io/utilities/ +/// SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice +/// SPDX-License-Identifier: MIT +#pragma once + +#include +#include + +namespace utilities { + +/// @brief Reads a 'line' from a stream. +/// @note Unlike the @c std::getline(...) this version strips trailing comments (starting with a '#' by default). +/// It also completely ignores blank lines and assumes that lines ending with a '\' continue to the next. +/// @param s The input stream to read from. +/// @param line We overwrite this with the content we read from the input stream. +/// @param comment_begin We ignore/strip out comments that start with this character ("#" by default). +/// @return The number of characters read. +/// @todo In the code below we should escape comment delimiters that are special (e.g. '*') +static std::size_t +read_line(std::istream& s, std::string& line, std::string_view comment_begin = "#") +{ + // Lambda that trims a string in-place from leading/trailing space characters. + auto trim = [](std::string& str) { + str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](int ch) { return !std::isspace(ch); })); + str.erase(std::find_if(str.rbegin(), str.rend(), [](int ch) { return !std::isspace(ch); }).base(), str.end()); + }; + + // Zap any existing content in the output parameter. + line.clear(); + + // We keep trying to read content until we hit the end-of-file (there are other early exits below). + while (!s.eof()) { + + // Standard read-a-line from a stream. + std::getline(s, line); + + // Remove any trailing comment (starts with the comment_begin). + if (!comment_begin.empty()) { + auto begin = line.find_first_of(comment_begin); + if (begin != std::string::npos) line.erase(begin, line.length() - begin); + } + + // Remove leading & trailing blanks. + trim(line); + + // If after all that we haven't captured anything skip on to try the next line. + std::size_t n = line.length(); + if (n == 0) continue; + + // Handle continuation lines (we know that n != 0). + if (line[n - 1] == '\\') { + // Replace the continuation character with a space + line[n - 1] = ' '; + + // Trim--we'll add one space back if there is a non-empty continuation. + trim(line); + + // Recurse ... + std::string continuation; + read_line(s, continuation, comment_begin); + if (!continuation.empty()) { + line += " "; + line += continuation; + } + } + + // If we have captured some content in `line` we can go home. + if (!line.empty()) break; + } + + return line.length(); +} + +/// @brief Reads one 'line' from a stream and returns that as a new std::string. +/// @note Unlike the @c std::getline(...) this version strips trailing comments (starting with a '#' by default). +/// It also completely ignores blank lines and assumes that lines ending with a '\' continue to the next. +/// @param s The input stream to read from. +/// @param comment_begin We ignore/strip out comments that start with this character ("#" by default) +/// @todo In the code below we should escape comment delimiters that are special (e.g. '*') +inline std::string +read_line(std::istream& s, std::string_view comment_begin = "#") +{ + std::string retval; + read_line(s, retval, comment_begin); + return retval; +} + +/// @brief Rewind an input stream to the start +inline std::istream& +rewind(std::istream& is) +{ + is.clear(); + is.seekg(0, std::istream::beg); + return is; +} + +/// @brief Counts the number of lines in the input stream. +/// @note If the comment start string is empty we use @c std::getline to read the lines, otherwise we use our own +/// version @c read_line so comment lines are excluded etc. +inline std::size_t +line_count(std::istream& is, std::string_view comment_begin = "#") +{ + std::size_t retval = 0; + std::string line; + if (comment_begin.empty()) { + while (std::getline(is, line)) ++retval; + } + else { + while (read_line(is, line, comment_begin)) ++retval; + } + rewind(is); + return retval; +} + +} // namespace utilities diff --git a/include/utilities/string.h b/include/utilities/string.h new file mode 100644 index 0000000..88e7f0f --- /dev/null +++ b/include/utilities/string.h @@ -0,0 +1,501 @@ +/// @brief Lots of utility functions that act on @c std::string and @c std::string_view +/// @link https://nessan.github.io/utilities/ +/// SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice +/// SPDX-License-Identifier: MIT +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace utilities { + +// -------------------------------------------------------------------------------------------------------------------- +// We start with the convert-an-input-string-in-place versions which only work on *non-const* input strings. +// -------------------------------------------------------------------------------------------------------------------- +/// @brief Converts a string to upper case in-place. +/// @note Uses the standard C-library @c toupper(...) function (so will not work for wide character sets). +inline void +upper_case(std::string& str) +{ + std::transform(str.begin(), str.end(), str.begin(), [](int c) { return static_cast(std::toupper(c)); }); +} + +/// @brief Converts a string to lower case in place. +/// @note Uses the standard C-library @c tolower(...) function (so will not work for wide character sets). +inline void +lower_case(std::string& str) +{ + std::transform(str.begin(), str.end(), str.begin(), [](int c) { return static_cast(::tolower(c)); }); +} + +/// @brief Removes any leading white-space from a string in-place +inline void +trim_left(std::string& str) +{ + str.erase(str.begin(), std::find_if(str.begin(), str.end(), [](int ch) { return !std::isspace(ch); })); +} + +/// @brief Remove any trailing white-space from a string in-place. +inline void +trim_right(std::string& str) +{ + str.erase(std::find_if(str.rbegin(), str.rend(), [](int ch) { return !std::isspace(ch); }).base(), str.end()); +} + +/// @brief Removes all leading & trailing white-space from a string in-place. +inline void +trim(std::string& str) +{ + trim_left(str); + trim_right(str); +} + +/// @brief Replace the first occurrence of a target substring with some other string in-place. +/// @param str string to be be converted. +/// @param target the target substring to hunt for. +/// @param replacement what we replace the first occurrence of the target with. +inline void +replace_left(std::string& str, std::string_view target, std::string_view replacement) +{ + auto p = str.find(target); + if (p != std::string::npos) str.replace(p, target.length(), replacement); +} + +/// @brief Replace the final occurrence of a target substring with some other string in-place. +/// @param str string to be be converted. +/// @param target the target substring to hunt for. +/// @param replacement what we replace the last occurrence of the target with. +inline void +replace_right(std::string& str, std::string_view target, std::string_view replacement) +{ + auto p = str.rfind(target); + if (p != std::string::npos) str.replace(p, target.length(), replacement); +} + +/// @brief Replace all occurrences of a target substring with some other string in-place. +/// @param str string to be be converted. +/// @param target the target substring to hunt for. +/// @param replacement what we replace all occurrences of the target with. +inline void +replace(std::string& str, std::string_view target, std::string_view replacement) +{ + std::size_t p = 0; + while ((p = str.find(target, p)) != std::string::npos) { + str.replace(p, target.length(), replacement); + p += replacement.length(); + } +} + +/// @brief Replace all contiguous white space sequences in a string in-place. +/// @param with By default they are replaced with a single space character +/// @param also_trim By default any white space at the beginning and end is removed entirely +inline void +replace_space(std::string& s, const std::string& with = " ", bool also_trim = true) +{ + if (also_trim) trim(s); + std::regex ws{R"(\s+)"}; + s = std::regex_replace(s, ws, with); +} + +/// @brief Condense contiguous white space sequences in a string in-place. +/// @param also_trim By default any white space at the beginning and end is removed entirely +inline void +condense(std::string& s, bool also_trim = true) +{ + replace_space(s, " ", also_trim); +} + +/// @brief Erase the first occurrence of a target substring. +/// @param str string to be be converted. +/// @param target the target substring to hunt for. +inline void +erase_left(std::string& str, std::string_view target) +{ + auto p = str.find(target); + if (p != std::string::npos) str.erase(p, target.length()); +} + +/// @brief Erase the last occurrence of a target substring. +/// @param str string to be be converted. +/// @param target the target substring to hunt for. +inline void +erase_right(std::string& str, std::string_view target) +{ + auto p = str.rfind(target); + if (p != std::string::npos) str.erase(p, target.length()); +} + +/// @brief Erase all occurrences of a target substring. +/// @param str string to be be converted. +/// @param target the target substring to hunt for. +inline void +erase(std::string& str, std::string_view target) +{ + std::size_t p = 0; + while ((p = str.find(target, p)) != std::string::npos) str.erase(p, target.length()); +} + +/// @brief Removes "surrounds" from a @c std::string so for example: (text) -> text. Conversion is in-place. +/// @note Multiples also work so <<>> -> text. The "surrounds" are only removed if they are correctly balanced. +inline void +remove_surrounds(std::string& s) +{ + std::size_t len = s.length(); + while (len > 1) { + // If the first character is alpha-numeric we are done. + char first = s[0]; + if (isalnum(first)) return; + + // First character is not alpha-numeric. + // Grab the last character & check for a match. + char last = s[len - 1]; + bool match = false; + + // Handle cases [text], {text}, , and (text) and then all others + switch (first) { + case '(': + if (last == ')') match = true; + break; + case '[': + if (last == ']') match = true; + break; + case '{': + if (last == '}') match = true; + break; + case '<': + if (last == '>') match = true; + break; + default: + if (last == first) match = true; + break; + } + + if (match) { + // Shrink the string and continue + s = s.substr(1, len - 2); + len -= 2; + } + else { + // No match => no surround so we can exit. + return; + } + } +} + +/// @brief "Standardize" a string -- turns "[ hallo world ] " or " Hallo World" into "HALLO WORLD" +inline void +standardize(std::string& s) +{ + condense(s); + upper_case(s); + remove_surrounds(s); + trim(s); +} + +// -------------------------------------------------------------------------------------------------------------------- +// Next we have all the counterpart create-a-new-string that is a copy of input-string with the appropriate conversion. +// These happily work on *const* input strings as the inputs are left unaltered. +// -------------------------------------------------------------------------------------------------------------------- +/// @brief Returns a new string that is a copy of the input converted to upper case. +/// @note Uses the standard C-library @c toupper(...) function (so will not work for wide character sets). +inline std::string +upper_cased(std::string_view input) +{ + std::string s{input}; + upper_case(s); + return s; +} + +/// @brief Returns a new string that is a copy of the input converted to lower case. +/// @note Uses the standard C-library @c tolower(...) function (so will not work for wide character sets). +inline std::string +lower_cased(std::string_view input) +{ + std::string s{input}; + lower_case(s); + return s; +} + +/// @brief Returns a new string that is a copy of the input with leading white-space removed. +inline std::string +trimmed_left_(std::string_view input) +{ + std::string s{input}; + trim_left(s); + return s; +} + +/// @brief Returns a new string that is a copy of the input with trailing white-space removed. +inline std::string +trimmed_right(std::string_view input) +{ + std::string s{input}; + trim_right(s); + return s; +} + +/// @brief Returns a new string that is a copy of the input with all leading and trailing white-space removed. +inline std::string +trimmed(std::string_view input) +{ + std::string s{input}; + trim(s); + return s; +} + +/// @brief Returns a new string that is a copy of the input with the first occurrence of a target substring replaced. +/// @param target the target substring to hunt for. +/// @param replacement what we replace the first occurrence of the target with. +inline std::string +replaced_left(std::string_view input, std::string_view target, std::string_view replacement) +{ + std::string s{input}; + replace_left(s, target, replacement); + return s; +} + +/// @brief Returns a new string that is a copy of the input with the final occurrence of a target substring replaced. +/// @param target the target substring to hunt for. +/// @param replacement what we replace the first occurrence of the target with. +inline std::string +replaced_right(std::string_view input, std::string_view target, std::string_view replacement) +{ + std::string s{input}; + replace_right(s, target, replacement); + return s; +} + +/// @brief Returns a new string that is a copy of the input with all occurrences of a target substring replaced. +/// @param target the target substring to hunt for. +/// @param replacement what we replace the first occurrence of the target with. +inline std::string +replaced(std::string_view input, std::string_view target, std::string_view replacement) +{ + std::string s{input}; + replace(s, target, replacement); + return s; +} + +/// @brief Returns a new string that is a copy of the input with all contiguous white space sequences replaced. +/// @param with By default they are replaced with a single space character +/// @param also_trim By default any white space at the beginning and end is removed entirely +inline std::string +replaced_space(std::string_view input, const std::string& with = " ", bool also_trim = true) +{ + std::string s{input}; + replace_space(s, with, also_trim); + return s; +} + +/// @brief Returns a copy of the input with all contiguous white space sequences replaced with one space. +/// @param also_trim By default any white space at the beginning and end is removed entirely +inline std::string +condensed(std::string_view input, bool also_trim = true) +{ + std::string s{input}; + condense(s, also_trim); + return s; +} + +/// @brief Returns a new string that is a copy of the input with the first occurrence of a target substring erased. +/// @param target the target substring to hunt for. +inline std::string +erased_left(std::string_view input, std::string_view target) +{ + std::string s{input}; + erase_left(s, target); + return s; +} + +/// @brief Returns a new string that is a copy of the input with the last occurrence of a target substring erased. +/// @param target the target substring to hunt for. +inline std::string +erased_right(std::string_view input, std::string_view target) +{ + std::string s{input}; + erase_right(s, target); + return s; +} + +/// @brief Returns a new string that is a copy of the input with the all occurrence of a target substring erased. +/// @param target the target substring to hunt for. +inline std::string +erased(std::string_view input, std::string_view target) +{ + std::string s{input}; + erase(s, target); + return s; +} + +/// @brief Returns a new string that is a copy of the input with "surrounds" stripped from it. +/// @example The string (text) -> text, the string <<>> -> text etc. +/// @note The "surrounds" are only removed if they are correctly balanced. +inline std::string +removed_surrounds(std::string_view input) +{ + std::string s{input}; + remove_surrounds(s); + return s; +} + +/// @brief Returns a "standardized" string that is a copy of the input. +/// @return For example, input "[ hallo world ]" or " Hallo World" is returned as "HALLO WORLD" +inline std::string +standardized(std::string_view input) +{ + std::string s{input}; + standardize(s); + return s; +} + +// -------------------------------------------------------------------------------------------------------------------- +// Next some functions that have no 'in-place' versus 'out-of-place' versions. +// -------------------------------------------------------------------------------------------------------------------- +/// @brief Check if a string starts with a particular prefix string. +/// @param str the string to check. +/// @param prefix the substring to look for at the start. +inline bool +starts_with(std::string_view str, std::string_view prefix) +{ + return str.find(prefix) == 0; +} + +/// @brief Check if a string ends with a particular suffix string. +/// @param str the string to check. +/// @param suffix the substring to look for at the end. +inline bool +ends_with(std::string_view str, std::string_view suffix) +{ + const auto pos = str.rfind(suffix); + return (pos != std::string::npos) && (pos == (str.length() - suffix.length())); +} + +/// @brief Try to read a value of a particular type from a @c std::string. +/// @note Uses @c std::from_chars(...) to retrieve a possible integral type from a string. +/// @example auto x = possible(str); will try and fill x with a double read from a string. +/// @return @c std::nullopt on failure. +template +constexpr std::optional +possible(std::string_view in, const char** next = nullptr) +{ + in.remove_prefix(in.find_first_not_of("+ ")); + T retval; + auto ec = std::from_chars(in.cbegin(), in.cend(), retval); + if (next) *next = ec.ptr; + if (ec.ec != std::errc{}) return std::nullopt; + return retval; +} + +/// @brief Given input text and delimiters, tokenize the text and pass the tokens to a function +/// @param b If the text is in `str` this parameter might be `cbegin(str)` +/// @param e If the text is in `str` this parameter might be `cend(str)` +/// @param db If the possible delimiters are in ``delims` this might be `cbegin(delims)` +/// @param de If the possible delimiters are in ``delims` this might be `cend(delims)` +/// @param function Will be called with a token like this `function(token_begin, token_end) +/// @note Credit to [blog article](https://tristanbrindle.com/posts/a-quicker-study-on-tokenising/) +template +constexpr void +for_each_token(InputIt ib, InputIt ie, ForwardIt db, ForwardIt de, BinaryFunc function) +{ + while (ib != ie) { + const auto x = std::find_first_of(ib, ie, db, de); // Find a token in the input text + function(ib, x); // Call the user supplied function on the token + if (x == ie) break; // Stop if we hit the end of the input text + ib = std::next(x); // Otherwise go again past that token we just found + } +} + +/// @brief Tokenize a string and put the tokens into the passed output container. +/// @param input The string to tokenize +/// @param output You pass in this "STL" container which we fill with the tokens. +/// @param skip By default we ignore any empty tokens (e.g. two spaces in a row) +/// @param delimiters By default tokens are broken on white space, commas, semi-colons, and colons. +template +constexpr void +tokenize(std::string_view input, Container_t& output, std::string_view delimiters = "\t,;: ", bool skip = true) +{ + auto ib = cbegin(input); + auto ie = cend(input); + auto db = cbegin(delimiters); + auto de = cend(delimiters); + + for_each_token(ib, ie, db, de, [&output, &skip](auto tb, auto te) { + if (tb != te || !skip) { output.emplace_back(tb, te); } + }); +} + +/// @brief Tokenize a string and return the tokens as a vector of strings +/// @param input The string to tokenize +/// @param delimiters By default tokens are broken on white space, commas, semi-colons, and colons. +/// @param skip By default we ignore any empty tokens (e.g. two spaces in a row) +/// @return std::vector This is a vector with all the tokens +inline std::vector +split(std::string_view input, std::string_view delimiters = "\t,;: ", bool skip = true) +{ + std::vector output; + output.reserve(input.size() / 2); + tokenize(input, output, delimiters, skip); + return output; +} + +/// @brief A version of @c regex_replace(...) where each match in turn is is run through a function you supply. +/// @param ib e.g. @c std::cbegin(a_string) +/// @param ie e.g. @c std::cend(a_string) +/// @param re The regular expression that defines the match we are after. +/// @param f A callback function that will be passed the match and should return the desired output string. +/// @return A new string where all the matches in @c s will have been run through @c f. +/// @link https://stackoverflow.com/questions/57193450/c-regex-replace-one-by-one +template +std::basic_string +regex_replace(Iter ib, Iter ie, const std::basic_regex& re, UnaryFunction f) +{ + std::basic_string s; + + using diff_t = typename std::match_results::difference_type; + diff_t match_pos_old = 0; + auto end_last_match = ib; + + auto callback = [&](const std::match_results& match) { + auto match_pos = match.position(0); + auto diff = match_pos - match_pos_old; + + auto start_match = end_last_match; + std::advance(start_match, diff); + + s.append(end_last_match, start_match); + s.append(f(match)); + + auto match_len = match.length(0); + match_pos_old = match_pos + match_len; + end_last_match = start_match; + std::advance(end_last_match, match_len); + }; + + std::regex_iterator begin(ib, ie, re), end; + std::for_each(begin, end, callback); + s.append(end_last_match, ie); + return s; +} + +/// @brief A version of @c regex_replace(...) where each match in turn is is run through a function you supply. +/// @param s The string to hunt for matches in. +/// @param re The regular expression that defines the match we are after. +/// @param f A callback function that will be passed the match and should return the desired output string. +/// @return A new string where all the matches in @c s will have been run through @c f. +/// @link https://stackoverflow.com/questions/57193450/c-regex-replace-one-by-one +template +std::string +regex_replace(const std::string& s, const std::basic_regex& re, UnaryFunction f) +{ + return regex_replace(s.cbegin(), s.cend(), re, f); +} + +} // namespace utilities diff --git a/include/utilities/thousands.h b/include/utilities/thousands.h new file mode 100644 index 0000000..d9a48f2 --- /dev/null +++ b/include/utilities/thousands.h @@ -0,0 +1,58 @@ +/// @brief Add readability formatting to larger numbers so 23410.24 -> 23,410.25. +/// @link https://nessan.github.io/utilities/ +/// SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice +/// SPDX-License-Identifier: MIT +#pragma once + +#include +#include +#include + +namespace utilities { + +// A locale facet that puts the commas in the thousand spots so 10000.5 -> 10,000.5 +struct commas_facet : std::numpunct { + using numpunct::numpunct; + char do_thousands_sep() const override { return ','; } + std::string do_grouping() const override { return "\003"; } +}; + +// We do our own memory management for the commas_facet (set the refs arg to 1). +static commas_facet our_commas_facet{1}; + +// NOTE: GCC on the Mac (or more accurately GCC's libstdc++) is poor at handling std::locale. +// The only locale it seems to know about is "C" or "POSIX" +// The stdlib for clang (libc++) seems to be much more compliant. +static const std::locale default_locale{"C"}; +static const std::locale commas_locale(default_locale, &our_commas_facet); + +/// @brief Force a stream to insert commas into large numbers for readability so that 23456.7 is printed as 23,456.7 +/// @param strm The stream you want to have this property -- defaults to @c std::cout +/// @param on You can set this to @c false to return the stream to its default behaviour. +inline void +imbue_stream_with_commas(std::ios_base& strm = std::cout, bool on = true) +{ + on ? strm.imbue(commas_locale) : strm.imbue(default_locale); +} + +/// @brief Force the global locale to insert commas into large numbers so that 23456.7 is printed as 23,456.7 +/// @param on You can set this to @c false to return the locale to its default behaviour. +/// @note This is primarily used to get `std::format` to work correctly with the {:L} specifier. +inline void +imbue_global_with_commas(bool on = true) +{ + on ? std::locale::global(commas_locale) : std::locale::global(default_locale); +} + +/// @brief Force the global locale & the usual output streams to insert commas into large numbers. +/// @param on You can set this to @c false to return the locale to its default behaviour. +inline void +pretty_print_thousands(bool on = true) +{ + imbue_global_with_commas(on); + imbue_stream_with_commas(std::cout, on); + imbue_stream_with_commas(std::cerr, on); + imbue_stream_with_commas(std::clog, on); +} + +} // namespace utilities diff --git a/include/utilities/type.h b/include/utilities/type.h new file mode 100644 index 0000000..965781b --- /dev/null +++ b/include/utilities/type.h @@ -0,0 +1,40 @@ +/// @brief Returns a string that represents the "type" of an object as seen by the compiler/preprocessor. +/// @link https://nessan.github.io/utilities/ +/// SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice +/// SPDX-License-Identifier: MIT +#pragma once + +#include +#include + +namespace utilities { + +template +constexpr auto type() +{ +#ifdef __clang__ + std::string_view m_name = __PRETTY_FUNCTION__; + std::string_view m_prefix = "auto utilities::type() [T = "; + std::string_view m_suffix = "]"; +#elif defined(__GNUC__) + std::string_view m_name = __PRETTY_FUNCTION__; + std::string_view m_prefix = "constexpr auto utilities::type() [with T = "; + std::string_view m_suffix = "]"; +#elif defined(_MSC_VER) + std::string_view m_name = __FUNCSIG__; + std::string_view m_prefix = "auto __cdecl type_name<"; + std::string_view m_suffix = ">(void)"; +#endif + + m_name.remove_prefix(m_prefix.size()); + m_name.remove_suffix(m_suffix.size()); + return m_name; +} + +template +constexpr auto type(const T&) +{ + return type(); +} + +} // namespace utilities diff --git a/include/utilities/utilities.h b/include/utilities/utilities.h new file mode 100644 index 0000000..eb1e608 --- /dev/null +++ b/include/utilities/utilities.h @@ -0,0 +1,16 @@ +/// @brief An include everything header for the utilities library. +/// @link https://nessan.github.io/utilities/ +/// SPDX-FileCopyrightText: 2024 Nessan Fitzmaurice +/// SPDX-License-Identifier: MIT +#pragma once + +#include "check.h" +#include "format.h" +#include "log.h" +#include "macros.h" +#include "print.h" +#include "stopwatch.h" +#include "stream.h" +#include "string.h" +#include "thousands.h" +#include "type.h" diff --git a/logo.png b/logo.png new file mode 100644 index 0000000..79002c8 Binary files /dev/null and b/logo.png differ