diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 868ffd3..130df83 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ -# borrowed this from: -# https://raw.githubusercontent.com/AcademySoftwareFoundation/Imath/main/.github/workflows/ci_workflow.yml -# with some slight simplification +# simple ci config the idea is to use the latest ubuntu, macos, and windows that github runners give us +# test a few different compilers (gcc/clang) in debug and release +# and run our tests via ctest name: CI @@ -14,219 +14,103 @@ on: - '**.md' jobs: - # Linux jobs run in Docker containers, so the latest OS version is OK. macOS - # and Windows jobs need to be locked to specific virtual environment - # versions to mitigate issues from OS updates, and will require maintenance - # as OS versions are retired. - # - # GH Actions (Free plan) supports 20 concurrent jobs, with 5 concurrent macOS - # jobs. This workflow tries to utilize (but not exceed) that budget to - # promote timely CI. - - # --------------------------------------------------------------------------- - # Linux - # --------------------------------------------------------------------------- - # TODO: Add ARM build. Add sanitize build. linux: - name: 'Linux CentOS 7 VFX CY${{ matrix.vfx-cy }} - <${{ matrix.compiler-desc }} , - config=${{ matrix.build-type }}, - shared=${{ matrix.build-shared }}, - cxx=${{ matrix.cxx-standard }}>' - # GH-hosted VM. The build runs in CentOS 7 'container' defined below. + name: '${{ matrix.os }} / ${{ matrix.build-type }} / ${{ matrix.compiler-desc }} ' + runs-on: ubuntu-latest - container: - # DockerHub: https://hub.docker.com/u/aswf - # Source: https://github.com/AcademySoftwareFoundation/aswf-docker - image: aswf/ci-openexr:${{ matrix.vfx-cy }} strategy: matrix: - build: [1, 2, 3, 4, 9, 10, 11, 12, 13, 14, 15, 20, 21, 22] + build: [1, 2, 3, 4, 5, 6] include: # ------------------------------------------------------------------- - # GCC, VFX CY2022 + # CLANG, Release # ------------------------------------------------------------------- - # C++17, Python 3.9 - build: 1 build-type: Release build-shared: 'ON' cxx-standard: 17 - cxx-compiler: g++ - cc-compiler: gcc - compiler-desc: gcc9.3.1 - vfx-cy: 2022 + cxx-compiler: clang++ + cxx-flags: '' + cc-compiler: clang + compiler-desc: clang + os: ubuntu-latest - # C++17, Python 3.9.7, Debug + # ------------------------------------------------------------------- + # CLANG, Debug + # ------------------------------------------------------------------- - build: 2 build-type: Debug build-shared: 'ON' cxx-standard: 17 - cxx-compiler: g++ - cc-compiler: gcc - compiler-desc: gcc9.3.1 - vfx-cy: 2022 - - # C++17, Python 3.9.7, Static - - build: 3 - build-type: Release - build-shared: 'OFF' - cxx-standard: 17 - cxx-compiler: g++ - cc-compiler: gcc - compiler-desc: gcc9.3.1 - vfx-cy: 2022 - - # C++14, Python 3.9.7, Static - - build: 4 - build-type: Release - build-shared: 'ON' - cxx-standard: 14 - cxx-compiler: g++ - cc-compiler: gcc - compiler-desc: gcc9.3.1 - vfx-cy: 2022 - - # C++17, no Python - # - build: 5 - # build-type: Release - # build-shared: 'ON' - # cxx-standard: 17 - # cxx-compiler: g++ - # cc-compiler: gcc - # compiler-desc: gcc9.3.1 - # python: 'OFF' - # python-desc: no python - # vfx-cy: 2022 + cxx-compiler: clang++ + cxx-flags: '' + cc-compiler: clang + compiler-desc: clang + os: ubuntu-latest # ------------------------------------------------------------------- - # GCC, VFX CY2021 + # gcc, Release # ------------------------------------------------------------------- - # C++17, Python 3.7.9 - - build: 9 + - build: 3 build-type: Release build-shared: 'ON' cxx-standard: 17 cxx-compiler: g++ + cxx-flags: '' cc-compiler: gcc - compiler-desc: gcc9.3.1 - python: 'ON' - vfx-cy: 2021 + compiler-desc: gcc + os: ubuntu-latest # ------------------------------------------------------------------- - # GCC, VFX CY2020 + # gcc, Debug # ------------------------------------------------------------------- - # C++14, Python 3.7 - - build: 10 - build-type: Release + - build: 4 + build-type: Debug build-shared: 'ON' - cxx-standard: 14 + cxx-standard: 17 cxx-compiler: g++ + cxx-flags: '' cc-compiler: gcc - compiler-desc: gcc6.3.1 - vfx-cy: 2020 + compiler-desc: gcc + os: ubuntu-latest - # ------------------------------------------------------------------- - # GCC, VFX CY2019 - # ------------------------------------------------------------------- - # C++11, Python 2.7 - - build: 11 - build-type: Release - build-shared: 'ON' - cxx-standard: 11 - cxx-compiler: g++ - cc-compiler: gcc - compiler-desc: gcc6.3.1 - vfx-cy: 2019 # ------------------------------------------------------------------- - # Clang, VFX CY2022 + # CLANG, Release header only # ------------------------------------------------------------------- - # C++17, Python 3.9 - - build: 12 + - build: 5 build-type: Release build-shared: 'ON' + header-only: 'ON' cxx-standard: 17 cxx-compiler: clang++ + cxx-flags: '' cc-compiler: clang - compiler-desc: clang10.4 - vfx-cy: 2022 - - # C++17, Python 3.9.7, Debug - - build: 13 - build-type: Debug - build-shared: 'ON' - cxx-standard: 17 - cxx-compiler: clang++ - cc-compiler: clang - compiler-desc: clang10.4 - vfx-cy: 2022 - - # C++17, Python 3.9.7, Static - - build: 14 - build-type: Release - build-shared: 'OFF' - cxx-standard: 17 - cxx-compiler: clang++ - cc-compiler: clang - compiler-desc: clang10.4 - vfx-cy: 2022 - - # C++14, Python 3.9.7, Static - - build: 15 - build-type: Release - build-shared: 'ON' - cxx-standard: 14 - cxx-compiler: clang++ - cc-compiler: clang - compiler-desc: clang10.4 - vfx-cy: 2022 + compiler-desc: clang + os: ubuntu-latest # ------------------------------------------------------------------- - # Clang, VFX CY2021 + # gcc, Release header only # ------------------------------------------------------------------- - # C++17, Python 3.7.9 - - build: 20 + - build: 6 build-type: Release build-shared: 'ON' + header-only: 'ON' cxx-standard: 17 - cxx-compiler: clang++ - cc-compiler: clang - compiler-desc: clang10.4 - vfx-cy: 2021 - - # ------------------------------------------------------------------- - # Clang, VFX CY2020 - # ------------------------------------------------------------------- - # C++14, Python 3.7 - - build: 21 - build-type: Release - build-shared: 'ON' - cxx-standard: 14 - cxx-compiler: clang++ - cc-compiler: clang - compiler-desc: clang7.8 - vfx-cy: 2020 + cxx-compiler: g++ + cxx-flags: '' + cc-compiler: gcc + compiler-desc: gcc + os: ubuntu-latest - # ------------------------------------------------------------------- - # Clang, VFX CY2019 - # ------------------------------------------------------------------- - # C++11, Python 2.7 - - build: 22 - build-type: Release - build-shared: 'ON' - cxx-standard: 11 - cxx-compiler: clang++ - cc-compiler: clang - compiler-desc: clang7.8 - vfx-cy: 2019 env: CXX: ${{ matrix.cxx-compiler }} CC: ${{ matrix.cc-compiler }} steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Create build directories run: | mkdir _install @@ -234,47 +118,43 @@ jobs: - name: Configure run: | cmake .. \ - -DCMAKE_INSTALL_PREFIX=../_install \ + -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/_install \ -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ -DCMAKE_CXX_STANDARD=${{ matrix.cxx-standard }} \ -DCMAKE_CXX_FLAGS=${{ matrix.cxx-flags }} \ -DCMAKE_VERBOSE_MAKEFILE:BOOL='OFF' \ -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} \ + -DPYSTRING_HEADER_ONLY=${{ matrix.header-only }} working-directory: _build - name: Build run: | cmake --build . \ - --target install \ --config ${{ matrix.build-type }} working-directory: _build - name: Test run: | - ctest -C ${{ matrix.build_type }} + ctest --build-config ${{ matrix.build-type }} --verbose working-directory: _build # --------------------------------------------------------------------------- # macOS # --------------------------------------------------------------------------- - macos_no_python: - name: 'macOS 10.15 - ' - runs-on: macos-10.15 + macos: + name: '${{ matrix.os }} / ${{ matrix.build-type }}' + runs-on: macos-latest strategy: matrix: - build: [1, 2, 3, 4, 5] + build: [1, 2, 3] include: - # C++11 + + # Release - build: 1 build-type: Release build-shared: 'ON' - build-docs: 'ON' cxx-standard: 17 - exclude-tests: + cxx-flags: '' + os: macos-latest # Debug - build: 2 @@ -282,34 +162,22 @@ jobs: build-shared: 'ON' build-docs: 'OFF' cxx-standard: 17 - exclude-tests: + cxx-flags: '' + os: macos-latest - # Static - - build: 3 - build-type: Debug - build-shared: 'OFF' - build-docs: 'OFF' - cxx-standard: 17 - exclude-tests: - # C++14 - - build: 4 + # Release header only + - build: 3 build-type: Release build-shared: 'ON' - build-docs: 'OFF' - cxx-standard: 14 - exclude-tests: + header-only: 'ON' + cxx-standard: 17 + cxx-flags: '' + os: macos-latest - # C++11 - - build: 5 - build-type: Release - build-shared: 'ON' - build-docs: 'OFF' - cxx-standard: 11 - exclude-tests: steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Create build directories run: | mkdir _install @@ -318,23 +186,22 @@ jobs: - name: Configure run: | cmake ../. \ - -DCMAKE_INSTALL_PREFIX=../_install \ + -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/_install \ -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ -DCMAKE_CXX_STANDARD=${{ matrix.cxx-standard }} \ -DCMAKE_CXX_FLAGS=${{ matrix.cxx-flags }} \ -DCMAKE_VERBOSE_MAKEFILE:BOOL='OFF' \ - -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} + -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} \ + -DPYSTRING_HEADER_ONLY=${{ matrix.header-only }} working-directory: _build - name: Build run: | cmake --build . \ - --target install \ --config ${{ matrix.build-type }} \ - -- -j2 working-directory: _build - name: Test run: | - ctest -C ${{ matrix.build_type }} + ctest --build-config ${{ matrix.build-type }} --verbose working-directory: _build # --------------------------------------------------------------------------- @@ -342,113 +209,42 @@ jobs: # --------------------------------------------------------------------------- windows: - name: 'Windows 2019 - <${{ matrix.compiler-desc }}, - config=${{ matrix.build-type }}, - shared=${{ matrix.build-shared }}, - cxx=${{ matrix.cxx-standard }}, - docs=${{ matrix.build-docs }}>' - runs-on: windows-${{ matrix.osver }} + name: '${{ matrix.os }} / ${{ matrix.build-type }}' + runs-on: windows-latest strategy: matrix: - build: [1, 3, 4, 6, 7, 8] + build: [1, 2] include: - # ------------------------------------------------------------------- - # VFX CY2022 - C++17 - Windows 2019 - # ------------------------------------------------------------------- - # C++17, Release Shared + + # Release - build: 1 build-type: Release build-shared: 'ON' - build-docs: 'ON' - compiler-desc: msvc16.11 cxx-standard: 17 - vfx-cy: 2022 - exclude-tests: - osver: 2019 - - # C++17, Debug - - ## - build: 2 - ## build-type: Debug - ## build-shared: 'ON' - ## build-docs: 'OFF' - ## cxx-standard: 17 - ## exclude-tests: + cxx-flags: '' + os: windows-latest - # C++17, Release Static - - build: 3 - build-type: Release - build-shared: 'OFF' - build-docs: 'OFF' - compiler-desc: msvc16.11 - cxx-standard: 17 - vfx-cy: 2022 - exclude-tests: - osver: 2019 - - # ------------------------------------------------------------------- - # VFX CY2022 - C++17 - Windows 2022 - # ------------------------------------------------------------------- - # C++17, Release Shared - - build: 4 - build-type: Release + # Debug + - build: 2 + build-type: Debug build-shared: 'ON' - build-docs: 'ON' - compiler-desc: msvc17.1 - cxx-standard: 17 - vfx-cy: 2022 - exclude-tests: - osver: 2022 - - # C++17, Debug - - ## - build: 5 - ## build-type: Debug - ## build-shared: 'ON' - ## build-docs: 'OFF' - ## cxx-standard: 17 - ## exclude-tests: - - # C++17, Release Static - - build: 6 - build-type: Release - build-shared: 'OFF' - build-docs: 'OFF' - compiler-desc: msvc17.1 cxx-standard: 17 - vfx-cy: 2022 - exclude-tests: - osver: 2022 - - # ------------------------------------------------------------------- - # VFX CY2020 - C++14 - Windows 2019 - # ------------------------------------------------------------------- - # C++14, Release Shared - - build: 7 - build-type: Release - build-shared: 'ON' - build-docs: 'OFF' - compiler-desc: msvc16.11 - cxx-standard: 14 - vfx-cy: 2020 - exclude-tests: - osver: 2019 + cxx-flags: '' + os: windows-latest - # ------------------------------------------------------------------- - # VFX CY2019 - C++11 - Windows 2019 - # ------------------------------------------------------------------- - # C++11, Release Shared - - build: 8 + # Release header only + - build: 3 build-type: Release build-shared: 'ON' - build-docs: 'OFF' - compiler-desc: msvc16.11 - cxx-standard: 11 - exclude-tests: - osver: 2019 + header-only: 'ON' + cxx-standard: 17 + cxx-flags: '' + os: windows-latest + steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - name: Create build directories run: | mkdir _install @@ -458,19 +254,19 @@ jobs: # the windows build needs the -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS to work run: | cmake ../. \ - -DCMAKE_INSTALL_PREFIX=../_install \ + -DCMAKE_INSTALL_PREFIX=${{ github.workspace }}/_install \ -DCMAKE_WINDOWS_EXPORT_ALL_SYMBOLS='ON'\ -DCMAKE_BUILD_TYPE=${{ matrix.build-type }} \ -DCMAKE_CXX_STANDARD=${{ matrix.cxx-standard }} \ -DCMAKE_CXX_FLAGS=${{ matrix.cxx-flags }} \ -DCMAKE_VERBOSE_MAKEFILE:BOOL='OFF' \ - -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} + -DBUILD_SHARED_LIBS=${{ matrix.build-shared }} \ + -DPYSTRING_HEADER_ONLY=${{ matrix.header-only }} shell: bash working-directory: _build - name: Build run: | cmake --build . \ - --target install \ --config ${{ matrix.build-type }} shell: bash working-directory: _build @@ -478,4 +274,4 @@ jobs: run: | ctest -C ${{ matrix.build-type }} shell: bash - working-directory: _build \ No newline at end of file + working-directory: _build diff --git a/.github/workflows/meson.yml b/.github/workflows/meson.yml new file mode 100644 index 0000000..e020145 --- /dev/null +++ b/.github/workflows/meson.yml @@ -0,0 +1,171 @@ +name: Meson + +on: + pull_request: + push: + +jobs: + meson-build-and-tests: + runs-on: ${{ matrix.platform }} + name: ${{ matrix.platform }}, ${{ matrix.mode.name }} ${{ matrix.flavor }} ${{ matrix.library_mode }} + strategy: + fail-fast: false + matrix: + flavor: + - debug + - release + library_mode: + - compiled + - header-only + mode: + - name: default + extra_envs: {} + + # Alternative compiler setups + - name: gcc + extra_envs: + CC: gcc + CXX: g++ + - name: clang + extra_envs: + CC: clang + CXX: clang++ + + - name: sanitize + args: >- + "-Db_sanitize=address,undefined" + extra_envs: {} + + # This is for MSVC, which only supports AddressSanitizer. + # https://learn.microsoft.com/en-us/cpp/sanitizers/ + - name: sanitize+asanonly + args: -Db_sanitize=address + extra_envs: + ASAN_OPTIONS: report_globals=0:halt_on_error=1:abort_on_error=1:print_summary=1 + + - name: clang+sanitize + args: >- + -Db_lundef=false "-Db_sanitize=address,undefined" + extra_envs: + CC: clang + CXX: clang++ + + # default clang on GitHub hosted runners is from MSYS2. + # Use Visual Studio supplied clang-cl instead. + - name: clang-cl+sanitize + args: >- + "-Db_sanitize=address,undefined" + extra_envs: + CC: clang-cl + CXX: clang-cl + platform: + - ubuntu-22.04 + - windows-2022 + - macos-latest + + exclude: + # Only test header-only with a subset of configurations to reduce CI time + # Test header-only only with default compiler in release mode + - library_mode: header-only + flavor: debug + - library_mode: header-only + mode: + name: gcc + - library_mode: header-only + mode: + name: clang + - library_mode: header-only + mode: + name: sanitize + - library_mode: header-only + mode: + name: sanitize+asanonly + - library_mode: header-only + mode: + name: clang+sanitize + - library_mode: header-only + mode: + name: clang-cl+sanitize + + # clang-cl only makes sense on windows. + - platform: ubuntu-22.04 + mode: + name: clang-cl+sanitize + - platform: macos-latest + mode: + name: clang-cl+sanitize + + # Use clang-cl instead of MSYS2 clang. + # + # we already tested clang+sanitize on linux, + # if this doesn't work, it should be an issue for MSYS2 team to consider. + - platform: windows-2022 + mode: + name: clang + - platform: windows-2022 + mode: + name: clang+sanitize + + # MSVC-only sanitizers + - platform: ubuntu-22.04 + mode: + name: sanitize+asanonly + - platform: macos-latest + mode: + name: sanitize+asanonly + - platform: windows-2022 + mode: + name: sanitize + + # clang is the default on macos + # also gcc is an alias to clang + - platform: macos-latest + mode: + name: clang + - platform: macos-latest + mode: + name: gcc + + # gcc is the default on linux + - platform: ubuntu-22.04 + mode: + name: gcc + + # only run sanitizer tests on linux + # + # gcc/clang's codegen shouldn't massively change across platforms, + # and linux supports most of the sanitizers. + - platform: macos-latest + mode: + name: clang+sanitize + - platform: macos-latest + mode: + name: sanitize + steps: + - name: Setup meson + run: | + pipx install meson ninja + - name: Checkout + uses: actions/checkout@v4 + - name: Activate MSVC and Configure + if: ${{ matrix.platform == 'windows-2022' }} + env: ${{ matrix.mode.extra_envs }} + run: | + meson setup build-${{ matrix.flavor }}-${{ matrix.library_mode }} --buildtype=${{ matrix.flavor }} -Ddefault_library=static -Dheader_only=${{ matrix.library_mode == 'header-only' && 'true' || 'false' }} ${{ matrix.mode.args }} --vsenv + - name: Configuring + if: ${{ matrix.platform != 'windows-2022' }} + env: ${{ matrix.mode.extra_envs }} + run: | + meson setup build-${{ matrix.flavor }}-${{ matrix.library_mode }} --buildtype=${{ matrix.flavor }} -Dheader_only=${{ matrix.library_mode == 'header-only' && 'true' || 'false' }} ${{ matrix.mode.args }} + - name: Building + run: | + meson compile -C build-${{ matrix.flavor }}-${{ matrix.library_mode }} + - name: Running tests + env: ${{ matrix.mode.extra_envs }} + run: | + meson test -C build-${{ matrix.flavor }}-${{ matrix.library_mode }} --timeout-multiplier 0 + - uses: actions/upload-artifact@v4 + if: failure() + with: + name: ${{ matrix.platform }}-${{ matrix.mode.name }}-${{ matrix.flavor }}-${{ matrix.library_mode }}-logs + path: build-${{ matrix.flavor }}-${{ matrix.library_mode }}/meson-logs \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index fed418f..a0318b9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,12 +1,66 @@ -cmake_minimum_required(VERSION 3.2) -project(pystring CXX) +cmake_minimum_required(VERSION 3.10) +project(pystring LANGUAGES CXX VERSION 1.1.4) -set(BUILD_SHARED_LIBS YES) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) -add_library(pystring - pystring.cpp - pystring.h -) +option (BUILD_SHARED_LIBS "Build shared libraries (set to OFF to build static libs)" ON) +option(PYSTRING_HEADER_ONLY "Build as header-only library" OFF) + +# If the user hasn't configured cmake with an explicit +# -DCMAKE_INSTALL_PREFIX=..., then set it to safely install into ./dist, to +# help prevent the user from accidentally writing over /usr/local or whatever. +if (CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT + AND PROJECT_IS_TOP_LEVEL) + set (CMAKE_INSTALL_PREFIX "${PROJECT_SOURCE_DIR}/dist" CACHE PATH + "Installation location" FORCE) +endif() +message (STATUS "Installation path will be ${CMAKE_INSTALL_PREFIX}") +include(GNUInstallDirs) + +if(PYSTRING_HEADER_ONLY) + message(STATUS "Building pystring as header-only library") + add_library(pystring INTERFACE) + + target_compile_definitions(pystring INTERFACE PYSTRING_HEADER_ONLY) + + target_include_directories(pystring INTERFACE + $ + $ + ) + + # Install both headers for header-only mode + install(FILES pystring.h pystring_impl.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} + ) +else() + message(STATUS "Building pystring as compiled library") + + add_library(pystring + pystring.cpp + pystring.h + ) + + set_target_properties(pystring PROPERTIES + VERSION ${PROJECT_VERSION} + SOVERSION ${PROJECT_VERSION_MAJOR} + ) + + install(TARGETS pystring + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin + ARCHIVE DESTINATION lib + ) + + install (FILES pystring.h + DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}/${PROJECT_NAME} + COMPONENT developer + ) + +endif() + +# Test executable add_executable (pystring_test test.cpp) TARGET_LINK_LIBRARIES (pystring_test pystring) @@ -14,9 +68,3 @@ TARGET_LINK_LIBRARIES (pystring_test pystring) enable_testing() add_test(NAME PyStringTest COMMAND pystring_test) -include(GNUInstallDirs) - -install(TARGETS pystring - LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} -) - diff --git a/Makefile b/Makefile index 5ac4d7e..a0df9aa 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ LIBTOOL ?= libtool -LIBDIR ?= /usr/lib +PREFIX ?= /usr +INCLUDEDIR ?= ${PREFIX}/include/pystring +LIBDIR ?= ${PREFIX}/lib CXX ?= g++ CXXFLAGS ?= -g -O3 -Wall -Wextra -Wshadow -Wconversion -Wcast-qual -Wformat=2 @@ -12,7 +14,8 @@ libpystring.la: pystring.lo $(LIBTOOL) --mode=link --tag=CXX $(CXX) -o $@ $< -rpath $(LIBDIR) install: libpystring.la - $(LIBTOOL) --mode=install install -c $< $(LIBDIR)/$< + $(LIBTOOL) --mode=install install -Dm755 $< $(DESTDIR)$(LIBDIR)/$< + $(LIBTOOL) --mode=install install -Dm644 pystring.h $(DESTDIR)$(INCLUDEDIR)/pystring.h clean: $(RM) -fr pystring.lo pystring.o libpystring.la .libs diff --git a/meson.build b/meson.build new file mode 100644 index 0000000..5dab5b8 --- /dev/null +++ b/meson.build @@ -0,0 +1,71 @@ +project( + 'pystring', + 'cpp', + version: '1.1.4', + license: 'BSD-3-Clause', + license_files: 'LICENSE', + meson_version: '>=1.3', + default_options: ['cpp_std=c++17,c++11', 'warning_level=3'], +) + +# Option to build as header-only library +header_only = get_option('header_only') + +inc = include_directories('.') +hdrs = files('pystring.h') + +if header_only + # Header-only mode: create a header-only dependency + message('Building pystring as header-only library') + + pystring_dep = declare_dependency( + include_directories: inc, + compile_args: ['-DPYSTRING_HEADER_ONLY'], + ) + + # Install headers for header-only mode + install_headers(hdrs, files('pystring_impl.h'), subdir: 'pystring') + +else + # Compiled mode: build as normal library + message('Building pystring as compiled library') + + srcs = files('pystring.cpp') + + pystring_lib = library( + 'pystring', + srcs, + implicit_include_directories: false, + include_directories: inc, + version: meson.project_version(), + install: true, + ) + + pystring_dep = declare_dependency( + link_with: pystring_lib, + include_directories: inc, + ) + + # Install headers for compiled mode + install_headers(hdrs, subdir: 'pystring') + + # Generate pkg-config file + pkgconfig = import('pkgconfig') + pkgconfig.generate( + pystring_lib, + description: 'C++ functions matching the interface and behavior of python string methods with std::string', + ) +endif + +meson.override_dependency('pystring', pystring_dep) + +# Build and run tests +test( + 'PyStringTest', + executable( + 'pystring_test', + 'test.cpp', + dependencies: pystring_dep, + build_by_default: false, + ), +) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 0000000..77be15e --- /dev/null +++ b/meson_options.txt @@ -0,0 +1 @@ +option('header_only', type: 'boolean', value: false, description: 'Build as header-only library') diff --git a/pystring.cpp b/pystring.cpp index dfc5e09..1dbaad3 100644 --- a/pystring.cpp +++ b/pystring.cpp @@ -2,1646 +2,10 @@ // SPDX-License-Identifier: BSD-3-Clause // https://github.com/imageworks/pystring/blob/master/LICENSE - #include "pystring.h" -#include -#include -#include -#include -#include - -namespace pystring -{ - -#if defined(_WIN32) || defined(_WIN64) || defined(_WINDOWS) || defined(_MSC_VER) -#ifndef WINDOWS -#define WINDOWS -#endif -#endif - -// This definition codes from configure.in in the python src. -// Strictly speaking this limits us to str sizes of 2**31. -// Should we wish to handle this limit, we could use an architecture -// specific #defines and read from ssize_t (unistd.h) if the header exists. -// But in the meantime, the use of int assures maximum arch compatibility. -// This must also equal the size used in the end = MAX_32BIT_INT default arg. - -typedef int Py_ssize_t; -const std::string forward_slash = "/"; -const std::string double_forward_slash = "//"; -const std::string triple_forward_slash = "///"; -const std::string double_back_slash = "\\"; -const std::string empty_string = ""; -const std::string dot = "."; -const std::string double_dot = ".."; -const std::string colon = ":"; - - -/* helper macro to fixup start/end slice values */ -#define ADJUST_INDICES(start, end, len) \ - if (end > len) \ - end = len; \ - else if (end < 0) { \ - end += len; \ - if (end < 0) \ - end = 0; \ - } \ - if (start < 0) { \ - start += len; \ - if (start < 0) \ - start = 0; \ - } - - - namespace { - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// why doesn't the std::reverse work? - /// - void reverse_strings( std::vector< std::string > & result) - { - for (std::vector< std::string >::size_type i = 0; i < result.size() / 2; i++ ) - { - std::swap(result[i], result[result.size() - 1 - i]); - } - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void split_whitespace( const std::string & str, std::vector< std::string > & result, int maxsplit ) - { - std::string::size_type i, j, len = str.size(); - for (i = j = 0; i < len; ) - { - - while ( i < len && ::isspace( str[i] ) ) i++; - j = i; - - while ( i < len && ! ::isspace( str[i]) ) i++; - - - - if (j < i) - { - if ( maxsplit-- <= 0 ) break; - - result.push_back( str.substr( j, i - j )); - - while ( i < len && ::isspace( str[i])) i++; - j = i; - } - } - if (j < len) - { - result.push_back( str.substr( j, len - j )); - } - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void rsplit_whitespace( const std::string & str, std::vector< std::string > & result, int maxsplit ) - { - std::string::size_type len = str.size(); - std::string::size_type i, j; - for (i = j = len; i > 0; ) - { - - while ( i > 0 && ::isspace( str[i - 1] ) ) i--; - j = i; - - while ( i > 0 && ! ::isspace( str[i - 1]) ) i--; - - - - if (j > i) - { - if ( maxsplit-- <= 0 ) break; - - result.push_back( str.substr( i, j - i )); - - while ( i > 0 && ::isspace( str[i - 1])) i--; - j = i; - } - } - if (j > 0) - { - result.push_back( str.substr( 0, j )); - } - //std::reverse( result, result.begin(), result.end() ); - reverse_strings( result ); - } - - } //anonymous namespace - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void split( const std::string & str, std::vector< std::string > & result, const std::string & sep, int maxsplit ) - { - result.clear(); - - if ( maxsplit < 0 ) maxsplit = MAX_32BIT_INT;//result.max_size(); - - - if ( sep.size() == 0 ) - { - split_whitespace( str, result, maxsplit ); - return; - } - - std::string::size_type i,j, len = str.size(), n = sep.size(); - - i = j = 0; - - while ( i+n <= len ) - { - if ( str[i] == sep[0] && str.substr( i, n ) == sep ) - { - if ( maxsplit-- <= 0 ) break; - - result.push_back( str.substr( j, i - j ) ); - i = j = i + n; - } - else - { - i++; - } - } - - result.push_back( str.substr( j, len-j ) ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void rsplit( const std::string & str, std::vector< std::string > & result, const std::string & sep, int maxsplit ) - { - if ( maxsplit < 0 ) - { - split( str, result, sep, maxsplit ); - return; - } - - result.clear(); - - if ( sep.size() == 0 ) - { - rsplit_whitespace( str, result, maxsplit ); - return; - } - - Py_ssize_t i,j, len = (Py_ssize_t) str.size(), n = (Py_ssize_t) sep.size(); - - i = j = len; - - while ( i >= n ) - { - if ( str[i - 1] == sep[n - 1] && str.substr( i - n, n ) == sep ) - { - if ( maxsplit-- <= 0 ) break; - - result.push_back( str.substr( i, j - i ) ); - i = j = i - n; - } - else - { - i--; - } - } - - result.push_back( str.substr( 0, j ) ); - reverse_strings( result ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - #define LEFTSTRIP 0 - #define RIGHTSTRIP 1 - #define BOTHSTRIP 2 - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string do_strip( const std::string & str, int striptype, const std::string & chars ) - { - Py_ssize_t len = (Py_ssize_t) str.size(), i, j, charslen = (Py_ssize_t) chars.size(); - - if ( charslen == 0 ) - { - i = 0; - if ( striptype != RIGHTSTRIP ) - { - while ( i < len && ::isspace( str[i] ) ) - { - i++; - } - } - - j = len; - if ( striptype != LEFTSTRIP ) - { - do - { - j--; - } - while (j >= i && ::isspace(str[j])); - - j++; - } - - - } - else - { - const char * sep = chars.c_str(); - - i = 0; - if ( striptype != RIGHTSTRIP ) - { - while ( i < len && memchr(sep, str[i], charslen) ) - { - i++; - } - } - - j = len; - if (striptype != LEFTSTRIP) - { - do - { - j--; - } - while (j >= i && memchr(sep, str[j], charslen) ); - j++; - } - - - } - - if ( i == 0 && j == len ) - { - return str; - } - else - { - return str.substr( i, j - i ); - } - - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void partition( const std::string & str, const std::string & sep, std::vector< std::string > & result ) - { - result.resize(3); - int index = find( str, sep ); - if ( index < 0 ) - { - result[0] = str; - result[1] = empty_string; - result[2] = empty_string; - } - else - { - result[0] = str.substr( 0, index ); - result[1] = sep; - result[2] = str.substr( index + sep.size(), str.size() ); - } - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void rpartition( const std::string & str, const std::string & sep, std::vector< std::string > & result ) - { - result.resize(3); - int index = rfind( str, sep ); - if ( index < 0 ) - { - result[0] = empty_string; - result[1] = empty_string; - result[2] = str; - } - else - { - result[0] = str.substr( 0, index ); - result[1] = sep; - result[2] = str.substr( index + sep.size(), str.size() ); - } - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string strip( const std::string & str, const std::string & chars ) - { - return do_strip( str, BOTHSTRIP, chars ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string lstrip( const std::string & str, const std::string & chars ) - { - return do_strip( str, LEFTSTRIP, chars ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string rstrip( const std::string & str, const std::string & chars ) - { - return do_strip( str, RIGHTSTRIP, chars ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string join( const std::string & str, const std::vector< std::string > & seq ) - { - std::vector< std::string >::size_type seqlen = seq.size(), i; - - if ( seqlen == 0 ) return empty_string; - if ( seqlen == 1 ) return seq[0]; - - std::string result( seq[0] ); - - for ( i = 1; i < seqlen; ++i ) - { - result += str + seq[i]; - - } - - - return result; - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - namespace - { - /* Matches the end (direction >= 0) or start (direction < 0) of self - * against substr, using the start and end arguments. Returns - * -1 on error, 0 if not found and 1 if found. - */ - - int _string_tailmatch(const std::string & self, const std::string & substr, - Py_ssize_t start, Py_ssize_t end, - int direction) - { - Py_ssize_t len = (Py_ssize_t) self.size(); - Py_ssize_t slen = (Py_ssize_t) substr.size(); - - const char* sub = substr.c_str(); - const char* str = self.c_str(); - - ADJUST_INDICES(start, end, len); - - if (direction < 0) { - // startswith - if (start+slen > len) - return 0; - } else { - // endswith - if (end-start < slen || start > len) - return 0; - if (end-slen > start) - start = end - slen; - } - if (end-start >= slen) - return (!std::memcmp(str+start, sub, slen)); - - return 0; - } - } - - bool endswith( const std::string & str, const std::string & suffix, int start, int end ) - { - int result = _string_tailmatch(str, suffix, - (Py_ssize_t) start, (Py_ssize_t) end, +1); - //if (result == -1) // TODO: Error condition - - return static_cast(result); - } - - - bool startswith( const std::string & str, const std::string & prefix, int start, int end ) - { - int result = _string_tailmatch(str, prefix, - (Py_ssize_t) start, (Py_ssize_t) end, -1); - //if (result == -1) // TODO: Error condition - - return static_cast(result); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - bool isalnum( const std::string & str ) - { - std::string::size_type len = str.size(), i; - if ( len == 0 ) return false; - - - if( len == 1 ) - { - return ::isalnum( str[0] ); - } - - for ( i = 0; i < len; ++i ) - { - if ( !::isalnum( str[i] ) ) return false; - } - return true; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - bool isalpha( const std::string & str ) - { - std::string::size_type len = str.size(), i; - if ( len == 0 ) return false; - if( len == 1 ) return ::isalpha( (int) str[0] ); - - for ( i = 0; i < len; ++i ) - { - if ( !::isalpha( (int) str[i] ) ) return false; - } - return true; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - bool isdigit( const std::string & str ) - { - std::string::size_type len = str.size(), i; - if ( len == 0 ) return false; - if( len == 1 ) return ::isdigit( str[0] ); - - for ( i = 0; i < len; ++i ) - { - if ( ! ::isdigit( str[i] ) ) return false; - } - return true; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - bool islower( const std::string & str ) - { - std::string::size_type len = str.size(), i; - if ( len == 0 ) return false; - if( len == 1 ) return ::islower( str[0] ); - - for ( i = 0; i < len; ++i ) - { - if ( !::islower( str[i] ) ) return false; - } - return true; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - bool isspace( const std::string & str ) - { - std::string::size_type len = str.size(), i; - if ( len == 0 ) return false; - if( len == 1 ) return ::isspace( str[0] ); - - for ( i = 0; i < len; ++i ) - { - if ( !::isspace( str[i] ) ) return false; - } - return true; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - bool istitle( const std::string & str ) - { - std::string::size_type len = str.size(), i; - - if ( len == 0 ) return false; - if ( len == 1 ) return ::isupper( str[0] ); - - bool cased = false, previous_is_cased = false; - - for ( i = 0; i < len; ++i ) - { - if ( ::isupper( str[i] ) ) - { - if ( previous_is_cased ) - { - return false; - } - - previous_is_cased = true; - cased = true; - } - else if ( ::islower( str[i] ) ) - { - if (!previous_is_cased) - { - return false; - } - - previous_is_cased = true; - cased = true; - - } - else - { - previous_is_cased = false; - } - } - - return cased; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - bool isupper( const std::string & str ) - { - std::string::size_type len = str.size(), i; - if ( len == 0 ) return false; - if( len == 1 ) return ::isupper( str[0] ); - - for ( i = 0; i < len; ++i ) - { - if ( !::isupper( str[i] ) ) return false; - } - return true; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string capitalize( const std::string & str ) - { - std::string s( str ); - std::string::size_type len = s.size(), i; - - if ( len > 0) - { - if (::islower(s[0])) s[0] = (char) ::toupper( s[0] ); - } - - for ( i = 1; i < len; ++i ) - { - if (::isupper(s[i])) s[i] = (char) ::tolower( s[i] ); - } - - return s; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string lower( const std::string & str ) - { - std::string s( str ); - std::string::size_type len = s.size(), i; - - for ( i = 0; i < len; ++i ) - { - if ( ::isupper( s[i] ) ) s[i] = (char) ::tolower( s[i] ); - } - - return s; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string upper( const std::string & str ) - { - std::string s( str ) ; - std::string::size_type len = s.size(), i; - - for ( i = 0; i < len; ++i ) - { - if ( ::islower( s[i] ) ) s[i] = (char) ::toupper( s[i] ); - } - - return s; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string swapcase( const std::string & str ) - { - std::string s( str ); - std::string::size_type len = s.size(), i; - - for ( i = 0; i < len; ++i ) - { - if ( ::islower( s[i] ) ) s[i] = (char) ::toupper( s[i] ); - else if (::isupper( s[i] ) ) s[i] = (char) ::tolower( s[i] ); - } - - return s; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string title( const std::string & str ) - { - std::string s( str ); - std::string::size_type len = s.size(), i; - bool previous_is_cased = false; - - for ( i = 0; i < len; ++i ) - { - int c = s[i]; - if ( ::islower(c) ) - { - if ( !previous_is_cased ) - { - s[i] = (char) ::toupper(c); - } - previous_is_cased = true; - } - else if ( ::isupper(c) ) - { - if ( previous_is_cased ) - { - s[i] = (char) ::tolower(c); - } - previous_is_cased = true; - } - else - { - previous_is_cased = false; - } - } - - return s; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string translate( const std::string & str, const std::string & table, const std::string & deletechars ) - { - std::string s; - std::string::size_type len = str.size(), dellen = deletechars.size(); - - if ( table.size() != 256 ) - { - // TODO : raise exception instead - return str; - } - - //if nothing is deleted, use faster code - if ( dellen == 0 ) - { - s = str; - for ( std::string::size_type i = 0; i < len; ++i ) - { - s[i] = table[ s[i] ]; - } - return s; - } - - - int trans_table[256]; - for ( int i = 0; i < 256; i++) - { - trans_table[i] = table[i]; - } - - for ( std::string::size_type i = 0; i < dellen; i++) - { - trans_table[(int) deletechars[i] ] = -1; - } - - for ( std::string::size_type i = 0; i < len; ++i ) - { - if ( trans_table[ (int) str[i] ] != -1 ) - { - s += table[ str[i] ]; - } - } - - return s; - - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string zfill( const std::string & str, int width ) - { - int len = (int)str.size(); - - if ( len >= width ) - { - return str; - } - - std::string s( str ); - - int fill = width - len; - - s = std::string( fill, '0' ) + s; - - - if ( s[fill] == '+' || s[fill] == '-' ) - { - s[0] = s[fill]; - s[fill] = '0'; - } - - return s; - - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string ljust( const std::string & str, int width ) - { - std::string::size_type len = str.size(); - if ( (( int ) len ) >= width ) return str; - return str + std::string( width - len, ' ' ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string rjust( const std::string & str, int width ) - { - std::string::size_type len = str.size(); - if ( (( int ) len ) >= width ) return str; - return std::string( width - len, ' ' ) + str; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string center( const std::string & str, int width ) - { - int len = (int) str.size(); - int marg, left; - - if ( len >= width ) return str; - - marg = width - len; - left = marg / 2 + (marg & width & 1); - - return std::string( left, ' ' ) + str + std::string( marg - left, ' ' ); - - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string slice( const std::string & str, int start, int end ) - { - ADJUST_INDICES(start, end, (int) str.size()); - if ( start >= end ) return empty_string; - return str.substr( start, end - start ); - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - int find( const std::string & str, const std::string & sub, int start, int end ) - { - ADJUST_INDICES(start, end, (int) str.size()); - - std::string::size_type result = str.find( sub, start ); - - // If we cannot find the string, or if the end-point of our found substring is past - // the allowed end limit, return that it can't be found. - if( result == std::string::npos || - (result + sub.size() > (std::string::size_type)end) ) - { - return -1; - } - - return (int) result; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - int index( const std::string & str, const std::string & sub, int start, int end ) - { - return find( str, sub, start, end ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - int rfind( const std::string & str, const std::string & sub, int start, int end ) - { - ADJUST_INDICES(start, end, (int) str.size()); - - std::string::size_type result = str.rfind( sub, end ); - - if( result == std::string::npos || - result < (std::string::size_type)start || - (result + sub.size() > (std::string::size_type)end)) - return -1; - - return (int)result; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - int rindex( const std::string & str, const std::string & sub, int start, int end ) - { - return rfind( str, sub, start, end ); - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string expandtabs( const std::string & str, int tabsize ) - { - std::string s( str ); - - std::string::size_type len = str.size(), i = 0; - int offset = 0; - - int j = 0; - - for ( i = 0; i < len; ++i ) - { - if ( str[i] == '\t' ) - { - - if ( tabsize > 0 ) - { - int fillsize = tabsize - (j % tabsize); - j += fillsize; - s.replace( i + offset, 1, std::string( fillsize, ' ' )); - offset += fillsize - 1; - } - else - { - s.replace( i + offset, 1, empty_string ); - offset -= 1; - } - - } - else - { - j++; - - if (str[i] == '\n' || str[i] == '\r') - { - j = 0; - } - } - } - - return s; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - int count( const std::string & str, const std::string & substr, int start, int end ) - { - int nummatches = 0; - int cursor = start; - - while ( 1 ) - { - cursor = find( str, substr, cursor, end ); - - if ( cursor < 0 ) break; - - cursor += (int) substr.size(); - nummatches += 1; - } - - return nummatches; - - - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - std::string replace( const std::string & str, const std::string & oldstr, const std::string & newstr, int count ) - { - int sofar = 0; - int cursor = 0; - std::string s( str ); - - std::string::size_type oldlen = oldstr.size(), newlen = newstr.size(); - - cursor = find( s, oldstr, cursor ); - - while ( cursor != -1 && cursor <= (int)s.size() ) - { - if ( count > -1 && sofar >= count ) - { - break; - } - - s.replace( cursor, oldlen, newstr ); - cursor += (int) newlen; - - if ( oldlen != 0) - { - cursor = find( s, oldstr, cursor ); - } - else - { - ++cursor; - } - - ++sofar; - } - - return s; - - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - void splitlines( const std::string & str, std::vector< std::string > & result, bool keepends ) - { - result.clear(); - std::string::size_type len = str.size(), i, j, eol; - - for (i = j = 0; i < len; ) - { - while (i < len && str[i] != '\n' && str[i] != '\r') i++; - - eol = i; - if (i < len) - { - if (str[i] == '\r' && i + 1 < len && str[i+1] == '\n') - { - i += 2; - } - else - { - i++; - } - if (keepends) - eol = i; - - } - - result.push_back( str.substr( j, eol - j ) ); - j = i; - - } - - if (j < len) - { - result.push_back( str.substr( j, len - j ) ); - } - - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - std::string mul( const std::string & str, int n ) - { - // Early exits - if (n <= 0) return empty_string; - if (n == 1) return str; - - std::ostringstream os; - for(int i=0; i= 2 && p[1] == ':') - { - std::string path = p; // In case drivespec == p - drivespec = pystring::slice(path, 0, 2); - pathspec = pystring::slice(path, 2); - } - else - { - drivespec = empty_string; - pathspec = p; - } - } - - // On Posix, drive is always empty - void splitdrive_posix(std::string & drivespec, std::string & pathspec, - const std::string & path) - { - drivespec = empty_string; - pathspec = path; - } - - void splitdrive(std::string & drivespec, std::string & pathspec, - const std::string & path) - { -#ifdef WINDOWS - return splitdrive_nt(drivespec, pathspec, path); -#else - return splitdrive_posix(drivespec, pathspec, path); -#endif - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - // Test whether a path is absolute - // In windows, if the character to the right of the colon - // is a forward or backslash it's absolute. - bool isabs_nt(const std::string & path) - { - std::string drivespec, pathspec; - splitdrive_nt(drivespec, pathspec, path); - if(pathspec.empty()) return false; - return ((pathspec[0] == '/') || (pathspec[0] == '\\')); - } - - bool isabs_posix(const std::string & s) - { - return pystring::startswith(s, forward_slash); - } - - bool isabs(const std::string & path) - { -#ifdef WINDOWS - return isabs_nt(path); -#else - return isabs_posix(path); -#endif - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - std::string abspath_nt(const std::string & path, const std::string & cwd) - { - std::string p = path; - if(!isabs_nt(p)) p = join_nt(cwd, p); - return normpath_nt(p); - } - - std::string abspath_posix(const std::string & path, const std::string & cwd) - { - std::string p = path; - if(!isabs_posix(p)) p = join_posix(cwd, p); - return normpath_posix(p); - } - - std::string abspath(const std::string & path, const std::string & cwd) - { -#ifdef WINDOWS - return abspath_nt(path, cwd); -#else - return abspath_posix(path, cwd); -#endif - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - std::string join_nt(const std::vector< std::string > & paths) - { - if(paths.empty()) return empty_string; - if(paths.size() == 1) return paths[0]; - - std::string path = paths[0]; - - for(unsigned int i=1; i= 2 && path[1] != ':') || (b.size() >= 2 && b[1] == ':')) - { - // Path doesnt start with a drive letter - b_nts = true; - } - // Else path has a drive letter, and b doesn't but is absolute. - else if((path.size()>3) || - ((path.size()==3) && !pystring::endswith(path, forward_slash) && !pystring::endswith(path, double_back_slash))) - { - b_nts = true; - } - } - - if(b_nts) - { - path = b; - } - else - { - // Join, and ensure there's a separator. - // assert len(path) > 0 - if( pystring::endswith(path, forward_slash) || pystring::endswith(path, double_back_slash)) - { - if(pystring::startswith(b,forward_slash) || pystring::startswith(b,double_back_slash)) - { - path += pystring::slice(b, 1); - } - else - { - path += b; - } - } - else if(pystring::endswith(path, colon)) - { - path += b; - } - else if(!b.empty()) - { - if(pystring::startswith(b, forward_slash) || pystring::startswith(b,double_back_slash)) - { - path += b; - } - else - { - path += double_back_slash + b; - } - } - else - { - // path is not empty and does not end with a backslash, - // but b is empty; since, e.g., split('a/') produces - // ('a', ''), it's best if join() adds a backslash in - // this case. - path += double_back_slash; - } - } - } - - return path; - } - - // Join two or more pathname components, inserting double_back_slash as needed. - std::string join_nt(const std::string & a, const std::string & b) - { - std::vector< std::string > paths(2); - paths[0] = a; - paths[1] = b; - return join_nt(paths); - } - - // Join pathnames. - // If any component is an absolute path, all previous path components - // will be discarded. - // Ignore the previous parts if a part is absolute. - // Insert a '/' unless the first part is empty or already ends in '/'. - - std::string join_posix(const std::vector< std::string > & paths) - { - if(paths.empty()) return empty_string; - if(paths.size() == 1) return paths[0]; - - std::string path = paths[0]; - - for(unsigned int i=1; i paths(2); - paths[0] = a; - paths[1] = b; - return join_posix(paths); - } - - std::string join(const std::string & path1, const std::string & path2) - { -#ifdef WINDOWS - return join_nt(path1, path2); -#else - return join_posix(path1, path2); -#endif - } - - - std::string join(const std::vector< std::string > & paths) - { -#ifdef WINDOWS - return join_nt(paths); -#else - return join_posix(paths); +// when not in header only mode include the implementations as non inline +// functions +#ifndef PYSTRING_HEADER_ONLY +#include "pystring_impl.h" #endif - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - - // Split a pathname. - // Return (head, tail) where tail is everything after the final slash. - // Either part may be empty - - void split_nt(std::string & head, std::string & tail, const std::string & path) - { - std::string d, p; - splitdrive_nt(d, p, path); - - // set i to index beyond p's last slash - int i = (int)p.size(); - - // walk back to find the index of the first slash from the end - while(i>0 && (p[i-1] != '\\') && (p[i-1] != '/')) - { - i = i - 1; - } - - head = pystring::slice(p,0,i); - tail = pystring::slice(p,i); // now tail has no slashes - - // remove trailing slashes from head, unless it's all slashes - std::string head2 = head; - while(!head2.empty() && ((pystring::slice(head2,-1) == forward_slash) || - (pystring::slice(head2,-1) == double_back_slash))) - { - head2 = pystring::slice(head2,0,-1); - } - - if(!head2.empty()) head = head2; - head = d + head; - } - - - // Split a path in head (everything up to the last '/') and tail (the - // rest). If the path ends in '/', tail will be empty. If there is no - // '/' in the path, head will be empty. - // Trailing '/'es are stripped from head unless it is the root. - - void split_posix(std::string & head, std::string & tail, const std::string & p) - { - int i = pystring::rfind(p, forward_slash) + 1; - - head = pystring::slice(p,0,i); - tail = pystring::slice(p,i); - - if(!head.empty() && (head != pystring::mul(forward_slash, (int) head.size()))) - { - head = pystring::rstrip(head, forward_slash); - } - } - - void split(std::string & head, std::string & tail, const std::string & path) - { -#ifdef WINDOWS - return split_nt(head, tail, path); -#else - return split_posix(head, tail, path); -#endif - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - std::string basename_nt(const std::string & path) - { - std::string head, tail; - split_nt(head, tail, path); - return tail; - } - - std::string basename_posix(const std::string & path) - { - std::string head, tail; - split_posix(head, tail, path); - return tail; - } - - std::string basename(const std::string & path) - { -#ifdef WINDOWS - return basename_nt(path); -#else - return basename_posix(path); -#endif - } - - std::string dirname_nt(const std::string & path) - { - std::string head, tail; - split_nt(head, tail, path); - return head; - } - - std::string dirname_posix(const std::string & path) - { - std::string head, tail; - split_posix(head, tail, path); - return head; - } - - std::string dirname(const std::string & path) - { -#ifdef WINDOWS - return dirname_nt(path); -#else - return dirname_posix(path); -#endif - } - - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - // Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B. - std::string normpath_nt(const std::string & p) - { - std::string path = p; - path = pystring::replace(path, forward_slash,double_back_slash); - - std::string prefix; - splitdrive_nt(prefix, path, path); - - // We need to be careful here. If the prefix is empty, and the path starts - // with a backslash, it could either be an absolute path on the current - // drive (\dir1\dir2\file) or a UNC filename (\\server\mount\dir1\file). It - // is therefore imperative NOT to collapse multiple backslashes blindly in - // that case. - // The code below preserves multiple backslashes when there is no drive - // letter. This means that the invalid filename \\\a\b is preserved - // unchanged, where a\\\b is normalised to a\b. It's not clear that there - // is any better behaviour for such edge cases. - - if(prefix.empty()) - { - // No drive letter - preserve initial backslashes - while(pystring::slice(path,0,1) == double_back_slash) - { - prefix = prefix + double_back_slash; - path = pystring::slice(path,1); - } - } - else - { - // We have a drive letter - collapse initial backslashes - if(pystring::startswith(path, double_back_slash)) - { - prefix = prefix + double_back_slash; - path = pystring::lstrip(path, double_back_slash); - } - } - - std::vector comps; - pystring::split(path, comps, double_back_slash); - - int i = 0; - - while(i<(int)comps.size()) - { - if(comps[i].empty() || comps[i] == dot) - { - comps.erase(comps.begin()+i); - } - else if(comps[i] == double_dot) - { - if(i>0 && comps[i-1] != double_dot) - { - comps.erase(comps.begin()+i-1, comps.begin()+i+1); - i -= 1; - } - else if(i == 0 && pystring::endswith(prefix, double_back_slash)) - { - comps.erase(comps.begin()+i); - } - else - { - i += 1; - } - } - else - { - i += 1; - } - } - - // If the path is now empty, substitute '.' - if(prefix.empty() && comps.empty()) - { - comps.push_back(dot); - } - - return prefix + pystring::join(double_back_slash, comps); - } - - // Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A/B. - // It should be understood that this may change the meaning of the path - // if it contains symbolic links! - // Normalize path, eliminating double slashes, etc. - - std::string normpath_posix(const std::string & p) - { - if(p.empty()) return dot; - - std::string path = p; - - int initial_slashes = pystring::startswith(path, forward_slash) ? 1 : 0; - - // POSIX allows one or two initial slashes, but treats three or more - // as single slash. - - if (initial_slashes && pystring::startswith(path, double_forward_slash) - && !pystring::startswith(path, triple_forward_slash)) - initial_slashes = 2; - - std::vector comps, new_comps; - pystring::split(path, comps, forward_slash); - - for(unsigned int i=0; i 0) - path = pystring::mul(forward_slash, initial_slashes) + path; - - if(path.empty()) return dot; - return path; - } - - std::string normpath(const std::string & path) - { -#ifdef WINDOWS - return normpath_nt(path); -#else - return normpath_posix(path); -#endif - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - /// - /// - - // Split the extension from a pathname. - // Extension is everything from the last dot to the end, ignoring - // leading dots. Returns "(root, ext)"; ext may be empty. - // It is always true that root + ext == p - - void splitext_generic(std::string & root, std::string & ext, - const std::string & p, - const std::string & sep, - const std::string & altsep, - const std::string & extsep) - { - int sepIndex = pystring::rfind(p, sep); - if(!altsep.empty()) - { - int altsepIndex = pystring::rfind(p, altsep); - sepIndex = std::max(sepIndex, altsepIndex); - } - - int dotIndex = pystring::rfind(p, extsep); - if(dotIndex > sepIndex) - { - // Skip all leading dots - int filenameIndex = sepIndex + 1; - - while(filenameIndex < dotIndex) - { - if(pystring::slice(p,filenameIndex) != extsep) - { - root = pystring::slice(p, 0, dotIndex); - ext = pystring::slice(p, dotIndex); - return; - } - - filenameIndex += 1; - } - } - - root = p; - ext = empty_string; - } - - void splitext_nt(std::string & root, std::string & ext, const std::string & path) - { - return splitext_generic(root, ext, path, - double_back_slash, forward_slash, dot); - } - - void splitext_posix(std::string & root, std::string & ext, const std::string & path) - { - return splitext_generic(root, ext, path, - forward_slash, empty_string, dot); - } - - void splitext(std::string & root, std::string & ext, const std::string & path) - { -#ifdef WINDOWS - return splitext_nt(root, ext, path); -#else - return splitext_posix(root, ext, path); -#endif - } - -} // namespace path -} // namespace os - - -}//namespace pystring - - diff --git a/pystring.h b/pystring.h index 35b6416..84e0743 100644 --- a/pystring.h +++ b/pystring.h @@ -6,6 +6,12 @@ #ifndef INCLUDED_PYSTRING_H #define INCLUDED_PYSTRING_H +#ifdef PYSTRING_HEADER_ONLY +#define PYSTRING_INLINE inline +#else +#define PYSTRING_INLINE +#endif + #include #include @@ -32,102 +38,102 @@ namespace pystring ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string with only its first character capitalized. /// - std::string capitalize( const std::string & str ); + PYSTRING_INLINE std::string capitalize( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return centered in a string of length width. Padding is done using spaces. /// - std::string center( const std::string & str, int width ); + PYSTRING_INLINE std::string center( const std::string & str, int width ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the number of occurrences of substring sub in string S[start:end]. Optional /// arguments start and end are interpreted as in slice notation. /// - int count( const std::string & str, const std::string & substr, int start = 0, int end = MAX_32BIT_INT); + PYSTRING_INLINE int count( const std::string & str, const std::string & substr, int start = 0, int end = MAX_32BIT_INT); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return True if the string ends with the specified suffix, otherwise return False. With /// optional start, test beginning at that position. With optional end, stop comparing at that position. /// - bool endswith( const std::string & str, const std::string & suffix, int start = 0, int end = MAX_32BIT_INT ); + PYSTRING_INLINE bool endswith( const std::string & str, const std::string & suffix, int start = 0, int end = MAX_32BIT_INT ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string where all tab characters are expanded using spaces. If tabsize /// is not given, a tab size of 8 characters is assumed. /// - std::string expandtabs( const std::string & str, int tabsize = 8); + PYSTRING_INLINE std::string expandtabs( const std::string & str, int tabsize = 8); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the lowest index in the string where substring sub is found, such that sub is /// contained in the range [start, end). Optional arguments start and end are interpreted as /// in slice notation. Return -1 if sub is not found. /// - int find( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); + PYSTRING_INLINE int find( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Synonym of find right now. Python version throws exceptions. This one currently doesn't /// - int index( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); + PYSTRING_INLINE int index( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if all characters in the string are alphanumeric and there is at least one /// character, false otherwise. /// - bool isalnum( const std::string & str ); + PYSTRING_INLINE bool isalnum( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if all characters in the string are alphabetic and there is at least one /// character, false otherwise /// - bool isalpha( const std::string & str ); + PYSTRING_INLINE bool isalpha( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if all characters in the string are digits and there is at least one /// character, false otherwise. /// - bool isdigit( const std::string & str ); + PYSTRING_INLINE bool isdigit( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if all cased characters in the string are lowercase and there is at least one /// cased character, false otherwise. /// - bool islower( const std::string & str ); + PYSTRING_INLINE bool islower( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if there are only whitespace characters in the string and there is at least /// one character, false otherwise. /// - bool isspace( const std::string & str ); + PYSTRING_INLINE bool isspace( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if the string is a titlecased string and there is at least one character, /// i.e. uppercase characters may only follow uncased characters and lowercase characters only /// cased ones. Return false otherwise. /// - bool istitle( const std::string & str ); + PYSTRING_INLINE bool istitle( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return true if all cased characters in the string are uppercase and there is at least one /// cased character, false otherwise. /// - bool isupper( const std::string & str ); + PYSTRING_INLINE bool isupper( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a string which is the concatenation of the strings in the sequence seq. /// The separator between elements is the str argument /// - std::string join( const std::string & str, const std::vector< std::string > & seq ); + PYSTRING_INLINE std::string join( const std::string & str, const std::vector< std::string > & seq ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the string left justified in a string of length width. Padding is done using /// spaces. The original string is returned if width is less than str.size(). /// - std::string ljust( const std::string & str, int width ); + PYSTRING_INLINE std::string ljust( const std::string & str, int width ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string converted to lowercase. /// - std::string lower( const std::string & str ); + PYSTRING_INLINE std::string lower( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string with leading characters removed. If chars is omitted or None, @@ -135,13 +141,13 @@ namespace pystring /// characters in the string will be stripped from the beginning of the string this method /// is called on (argument "str" ). /// - std::string lstrip( const std::string & str, const std::string & chars = "" ); + PYSTRING_INLINE std::string lstrip( const std::string & str, const std::string & chars = "" ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string, concatenated N times, together. /// Corresponds to the __mul__ operator. /// - std::string mul( const std::string & str, int n); + PYSTRING_INLINE std::string mul( const std::string & str, int n); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Split the string around first occurance of sep. @@ -149,7 +155,7 @@ namespace pystring /// be the text before sep, sep itself, and the remaining text. If sep is /// not found, the original string will be returned with two empty strings. /// - void partition( const std::string & str, const std::string & sep, std::vector< std::string > & result ); + PYSTRING_INLINE void partition( const std::string & str, const std::string & sep, std::vector< std::string > & result ); inline std::vector< std::string > partition( const std::string & str, const std::string & sep ) { std::vector< std::string > result; @@ -157,30 +163,42 @@ namespace pystring return result; } + ////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief If str starts with prefix return a copy of the string with prefix at the start + /// removed otherwise return an unmodified copy of the string. + /// + PYSTRING_INLINE std::string removeprefix( const std::string & str, const std::string & prefix ); + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// @brief If str ends with suffix return a copy of the string with suffix at the end removed + /// otherwise return an unmodified copy of the string. + /// + PYSTRING_INLINE std::string removesuffix( const std::string & str, const std::string & suffix ); + ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string with all occurrences of substring old replaced by new. If /// the optional argument count is given, only the first count occurrences are replaced. /// - std::string replace( const std::string & str, const std::string & oldstr, const std::string & newstr, int count = -1); + PYSTRING_INLINE std::string replace( const std::string & str, const std::string & oldstr, const std::string & newstr, int count = -1); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the highest index in the string where substring sub is found, such that sub is /// contained within s[start,end]. Optional arguments start and end are interpreted as in /// slice notation. Return -1 on failure. /// - int rfind( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); + PYSTRING_INLINE int rfind( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Currently a synonym of rfind. The python version raises exceptions. This one currently /// does not /// - int rindex( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); + PYSTRING_INLINE int rindex( const std::string & str, const std::string & sub, int start = 0, int end = MAX_32BIT_INT ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the string right justified in a string of length width. Padding is done using /// spaces. The original string is returned if width is less than str.size(). /// - std::string rjust( const std::string & str, int width); + PYSTRING_INLINE std::string rjust( const std::string & str, int width); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Split the string around last occurance of sep. @@ -188,7 +206,7 @@ namespace pystring /// be the text before sep, sep itself, and the remaining text. If sep is /// not found, the original string will be returned with two empty strings. /// - void rpartition( const std::string & str, const std::string & sep, std::vector< std::string > & result ); + PYSTRING_INLINE void rpartition( const std::string & str, const std::string & sep, std::vector< std::string > & result ); inline std::vector< std::string > rpartition ( const std::string & str, const std::string & sep ) { std::vector< std::string > result; @@ -201,14 +219,14 @@ namespace pystring /// characters are removed. If not "", the characters in the string will be stripped from the /// end of the string this method is called on. /// - std::string rstrip( const std::string & str, const std::string & chars = "" ); + PYSTRING_INLINE std::string rstrip( const std::string & str, const std::string & chars = "" ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Fills the "result" list with the words in the string, using sep as the delimiter string. /// If maxsplit is > -1, at most maxsplit splits are done. If sep is "", /// any whitespace string is a separator. /// - void split( const std::string & str, std::vector< std::string > & result, const std::string & sep = "", int maxsplit = -1); + PYSTRING_INLINE void split( const std::string & str, std::vector< std::string > & result, const std::string & sep = "", int maxsplit = -1); inline std::vector< std::string > split( const std::string & str, const std::string & sep = "", int maxsplit = -1) { std::vector< std::string > result; @@ -223,7 +241,7 @@ namespace pystring /// If maxsplit is > -1, at most maxsplit splits are done. If sep is "", /// any whitespace string is a separator. /// - void rsplit( const std::string & str, std::vector< std::string > & result, const std::string & sep = "", int maxsplit = -1); + PYSTRING_INLINE void rsplit( const std::string & str, std::vector< std::string > & result, const std::string & sep = "", int maxsplit = -1); inline std::vector< std::string > rsplit( const std::string & str, const std::string & sep = "", int maxsplit = -1) { std::vector< std::string > result; @@ -235,7 +253,7 @@ namespace pystring /// @brief Return a list of the lines in the string, breaking at line boundaries. Line breaks /// are not included in the resulting list unless keepends is given and true. /// - void splitlines( const std::string & str, std::vector< std::string > & result, bool keepends = false ); + PYSTRING_INLINE void splitlines( const std::string & str, std::vector< std::string > & result, bool keepends = false ); inline std::vector< std::string > splitlines( const std::string & str, bool keepends = false ) { std::vector< std::string > result; @@ -248,48 +266,48 @@ namespace pystring /// test string beginning at that position. With optional end, stop comparing string at that /// position /// - bool startswith( const std::string & str, const std::string & prefix, int start = 0, int end = MAX_32BIT_INT ); + PYSTRING_INLINE bool startswith( const std::string & str, const std::string & prefix, int start = 0, int end = MAX_32BIT_INT ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string with leading and trailing characters removed. If chars is "", /// whitespace characters are removed. If given not "", the characters in the string will be /// stripped from the both ends of the string this method is called on. /// - std::string strip( const std::string & str, const std::string & chars = "" ); + PYSTRING_INLINE std::string strip( const std::string & str, const std::string & chars = "" ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string with uppercase characters converted to lowercase and vice versa. /// - std::string swapcase( const std::string & str ); + PYSTRING_INLINE std::string swapcase( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a titlecased version of the string: words start with uppercase characters, /// all remaining cased characters are lowercase. /// - std::string title( const std::string & str ); + PYSTRING_INLINE std::string title( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string where all characters occurring in the optional argument /// deletechars are removed, and the remaining characters have been mapped through the given /// translation table, which must be a string of length 256. /// - std::string translate( const std::string & str, const std::string & table, const std::string & deletechars = ""); + PYSTRING_INLINE std::string translate( const std::string & str, const std::string & table, const std::string & deletechars = ""); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a copy of the string converted to uppercase. /// - std::string upper( const std::string & str ); + PYSTRING_INLINE std::string upper( const std::string & str ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the numeric string left filled with zeros in a string of length width. The original /// string is returned if width is less than str.size(). /// - std::string zfill( const std::string & str, int width ); + PYSTRING_INLINE std::string zfill( const std::string & str, int width ); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief function matching python's slice functionality. /// - std::string slice( const std::string & str, int start = 0, int end = MAX_32BIT_INT); + PYSTRING_INLINE std::string slice( const std::string & str, int start = 0, int end = MAX_32BIT_INT); /// /// @ } @@ -327,26 +345,26 @@ namespace path /// program; where basename for '/foo/bar/' returns 'bar', the basename() function returns an /// empty string (''). - std::string basename(const std::string & path); - std::string basename_nt(const std::string & path); - std::string basename_posix(const std::string & path); + PYSTRING_INLINE std::string basename(const std::string & path); + PYSTRING_INLINE std::string basename_nt(const std::string & path); + PYSTRING_INLINE std::string basename_posix(const std::string & path); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return the directory name of pathname path. This is the first half of the pair /// returned by split(path). - std::string dirname(const std::string & path); - std::string dirname_nt(const std::string & path); - std::string dirname_posix(const std::string & path); + PYSTRING_INLINE std::string dirname(const std::string & path); + PYSTRING_INLINE std::string dirname_nt(const std::string & path); + PYSTRING_INLINE std::string dirname_posix(const std::string & path); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return True if path is an absolute pathname. On Unix, that means it begins with a /// slash, on Windows that it begins with a (back)slash after chopping off a potential drive /// letter. - bool isabs(const std::string & path); - bool isabs_nt(const std::string & path); - bool isabs_posix(const std::string & s); + PYSTRING_INLINE bool isabs(const std::string & path); + PYSTRING_INLINE bool isabs_nt(const std::string & path); + PYSTRING_INLINE bool isabs_posix(const std::string & s); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Return a normalized absolutized version of the pathname path. @@ -354,9 +372,9 @@ namespace path /// NOTE: This differs from the interface of the python equivalent in that it requires you /// to pass in the current working directory as an argument. - std::string abspath(const std::string & path, const std::string & cwd); - std::string abspath_nt(const std::string & path, const std::string & cwd); - std::string abspath_posix(const std::string & path, const std::string & cwd); + PYSTRING_INLINE std::string abspath(const std::string & path, const std::string & cwd); + PYSTRING_INLINE std::string abspath_nt(const std::string & path, const std::string & cwd); + PYSTRING_INLINE std::string abspath_posix(const std::string & path, const std::string & cwd); ////////////////////////////////////////////////////////////////////////////////////////////// @@ -369,13 +387,13 @@ namespace path /// current directory on drive C: (c:foo), not c:\foo. /// This dispatches based on the compilation OS - std::string join(const std::string & path1, const std::string & path2); - std::string join_nt(const std::string & path1, const std::string & path2); - std::string join_posix(const std::string & path1, const std::string & path2); + PYSTRING_INLINE std::string join(const std::string & path1, const std::string & path2); + PYSTRING_INLINE std::string join_nt(const std::string & path1, const std::string & path2); + PYSTRING_INLINE std::string join_posix(const std::string & path1, const std::string & path2); - std::string join(const std::vector< std::string > & paths); - std::string join_nt(const std::vector< std::string > & paths); - std::string join_posix(const std::vector< std::string > & paths); + PYSTRING_INLINE std::string join(const std::vector< std::string > & paths); + PYSTRING_INLINE std::string join_nt(const std::vector< std::string > & paths); + PYSTRING_INLINE std::string join_posix(const std::vector< std::string > & paths); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Normalize a pathname. This collapses redundant separators and up-level references @@ -384,9 +402,9 @@ namespace path /// It should be understood that this may change the meaning of the path if it contains /// symbolic links! - std::string normpath(const std::string & path); - std::string normpath_nt(const std::string & path); - std::string normpath_posix(const std::string & path); + PYSTRING_INLINE std::string normpath(const std::string & path); + PYSTRING_INLINE std::string normpath_nt(const std::string & path); + PYSTRING_INLINE std::string normpath_posix(const std::string & path); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Split the pathname path into a pair, (head, tail) where tail is the last pathname @@ -397,9 +415,9 @@ namespace path /// join(head, tail) returns a path to the same location as path (but the strings may /// differ). - void split(std::string & head, std::string & tail, const std::string & path); - void split_nt(std::string & head, std::string & tail, const std::string & path); - void split_posix(std::string & head, std::string & tail, const std::string & path); + PYSTRING_INLINE void split(std::string & head, std::string & tail, const std::string & path); + PYSTRING_INLINE void split_nt(std::string & head, std::string & tail, const std::string & path); + PYSTRING_INLINE void split_posix(std::string & head, std::string & tail, const std::string & path); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Split the pathname path into a pair (drive, tail) where drive is either a drive @@ -407,18 +425,18 @@ namespace path /// drive will always be the empty string. In all cases, drive + tail will be the same as /// path. - void splitdrive(std::string & drivespec, std::string & pathspec, const std::string & path); - void splitdrive_nt(std::string & drivespec, std::string & pathspec, const std::string & p); - void splitdrive_posix(std::string & drivespec, std::string & pathspec, const std::string & path); + PYSTRING_INLINE void splitdrive(std::string & drivespec, std::string & pathspec, const std::string & path); + PYSTRING_INLINE void splitdrive_nt(std::string & drivespec, std::string & pathspec, const std::string & p); + PYSTRING_INLINE void splitdrive_posix(std::string & drivespec, std::string & pathspec, const std::string & path); ////////////////////////////////////////////////////////////////////////////////////////////// /// @brief Split the pathname path into a pair (root, ext) such that root + ext == path, and /// ext is empty or begins with a period and contains at most one period. Leading periods on /// the basename are ignored; splitext('.cshrc') returns ('.cshrc', ''). - void splitext(std::string & root, std::string & ext, const std::string & path); - void splitext_nt(std::string & root, std::string & ext, const std::string & path); - void splitext_posix(std::string & root, std::string & ext, const std::string & path); + PYSTRING_INLINE void splitext(std::string & root, std::string & ext, const std::string & path); + PYSTRING_INLINE void splitext_nt(std::string & root, std::string & ext, const std::string & path); + PYSTRING_INLINE void splitext_posix(std::string & root, std::string & ext, const std::string & path); /// /// @ } @@ -428,4 +446,8 @@ namespace path } // namespace pystring +#if PYSTRING_HEADER_ONLY +#include "pystring_impl.h" +#endif + #endif diff --git a/pystring_impl.h b/pystring_impl.h new file mode 100644 index 0000000..e6d6a48 --- /dev/null +++ b/pystring_impl.h @@ -0,0 +1,1693 @@ +// Copyright Contributors to the Pystring project. +// SPDX-License-Identifier: BSD-3-Clause +// https://github.com/imageworks/pystring/blob/master/LICENSE + +#include +#include +#include +#include +#include + +namespace pystring +{ + +#if defined(_WIN32) || defined(_WIN64) || defined(_WINDOWS) || defined(_MSC_VER) +#ifndef WINDOWS +#define WINDOWS +#endif +#endif + +// This definition codes from configure.in in the python src. +// Strictly speaking this limits us to str sizes of 2**31. +// Should we wish to handle this limit, we could use an architecture +// specific #defines and read from ssize_t (unistd.h) if the header exists. +// But in the meantime, the use of int assures maximum arch compatibility. +// This must also equal the size used in the end = MAX_32BIT_INT default arg. + +typedef int Py_ssize_t; +const std::string forward_slash = "/"; +const std::string double_forward_slash = "//"; +const std::string triple_forward_slash = "///"; +const std::string double_back_slash = "\\"; +const std::string empty_string = ""; +const std::string dot = "."; +const std::string double_dot = ".."; +const std::string colon = ":"; + + +/* helper macro to fixup start/end slice values */ +#define PYSTRING_ADJUST_INDICES(start, end, len) \ + if (end > len) \ + end = len; \ + else if (end < 0) { \ + end += len; \ + if (end < 0) \ + end = 0; \ + } \ + if (start < 0) { \ + start += len; \ + if (start < 0) \ + start = 0; \ + } + + + namespace { + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// why doesn't the std::reverse work? + /// + void reverse_strings( std::vector< std::string > & result) + { + for (std::vector< std::string >::size_type i = 0; i < result.size() / 2; i++ ) + { + std::swap(result[i], result[result.size() - 1 - i]); + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void split_whitespace( const std::string & str, std::vector< std::string > & result, int maxsplit ) + { + std::string::size_type i, j, len = str.size(); + for (i = j = 0; i < len; ) + { + + while ( i < len && ::isspace( str[i] ) ) i++; + j = i; + + while ( i < len && ! ::isspace( str[i]) ) i++; + + + + if (j < i) + { + if ( maxsplit-- <= 0 ) break; + + result.push_back( str.substr( j, i - j )); + + while ( i < len && ::isspace( str[i])) i++; + j = i; + } + } + if (j < len) + { + result.push_back( str.substr( j, len - j )); + } + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void rsplit_whitespace( const std::string & str, std::vector< std::string > & result, int maxsplit ) + { + std::string::size_type len = str.size(); + std::string::size_type i, j; + for (i = j = len; i > 0; ) + { + + while ( i > 0 && ::isspace( str[i - 1] ) ) i--; + j = i; + + while ( i > 0 && ! ::isspace( str[i - 1]) ) i--; + + + + if (j > i) + { + if ( maxsplit-- <= 0 ) break; + + result.push_back( str.substr( i, j - i )); + + while ( i > 0 && ::isspace( str[i - 1])) i--; + j = i; + } + } + if (j > 0) + { + result.push_back( str.substr( 0, j )); + } + //std::reverse( result, result.begin(), result.end() ); + reverse_strings( result ); + } + + } //anonymous namespace + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void split( const std::string & str, std::vector< std::string > & result, const std::string & sep, int maxsplit ) + { + result.clear(); + + if ( maxsplit < 0 ) maxsplit = MAX_32BIT_INT;//result.max_size(); + + + if ( sep.size() == 0 ) + { + split_whitespace( str, result, maxsplit ); + return; + } + + std::string::size_type i,j, len = str.size(), n = sep.size(); + + i = j = 0; + + while ( i+n <= len ) + { + if ( str[i] == sep[0] && str.substr( i, n ) == sep ) + { + if ( maxsplit-- <= 0 ) break; + + result.push_back( str.substr( j, i - j ) ); + i = j = i + n; + } + else + { + i++; + } + } + + result.push_back( str.substr( j, len-j ) ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void rsplit( const std::string & str, std::vector< std::string > & result, const std::string & sep, int maxsplit ) + { + if ( maxsplit < 0 ) + { + split( str, result, sep, maxsplit ); + return; + } + + result.clear(); + + if ( sep.size() == 0 ) + { + rsplit_whitespace( str, result, maxsplit ); + return; + } + + Py_ssize_t i,j, len = (Py_ssize_t) str.size(), n = (Py_ssize_t) sep.size(); + + i = j = len; + + while ( i >= n ) + { + if ( str[i - 1] == sep[n - 1] && str.substr( i - n, n ) == sep ) + { + if ( maxsplit-- <= 0 ) break; + + result.push_back( str.substr( i, j - i ) ); + i = j = i - n; + } + else + { + i--; + } + } + + result.push_back( str.substr( 0, j ) ); + reverse_strings( result ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + #define PYSTRING_LEFTSTRIP 0 + #define PYSTRING_RIGHTSTRIP 1 + #define PYSTRING_BOTHSTRIP 2 + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string do_strip( const std::string & str, int striptype, const std::string & chars ) + { + Py_ssize_t len = (Py_ssize_t) str.size(), i, j, charslen = (Py_ssize_t) chars.size(); + + if ( charslen == 0 ) + { + i = 0; + if ( striptype != PYSTRING_RIGHTSTRIP ) + { + while ( i < len && ::isspace( str[i] ) ) + { + i++; + } + } + + j = len; + if ( striptype != PYSTRING_LEFTSTRIP ) + { + do + { + j--; + } + while (j >= i && ::isspace(str[j])); + + j++; + } + + + } + else + { + const char * sep = chars.c_str(); + + i = 0; + if ( striptype != PYSTRING_RIGHTSTRIP ) + { + while ( i < len && memchr(sep, str[i], charslen) ) + { + i++; + } + } + + j = len; + if (striptype != PYSTRING_LEFTSTRIP) + { + do + { + j--; + } + while (j >= i && memchr(sep, str[j], charslen) ); + j++; + } + + + } + + if ( i == 0 && j == len ) + { + return str; + } + else + { + return str.substr( i, j - i ); + } + + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void partition( const std::string & str, const std::string & sep, std::vector< std::string > & result ) + { + result.resize(3); + int index = find( str, sep ); + if ( index < 0 ) + { + result[0] = str; + result[1] = empty_string; + result[2] = empty_string; + } + else + { + result[0] = str.substr( 0, index ); + result[1] = sep; + result[2] = str.substr( index + sep.size(), str.size() ); + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void rpartition( const std::string & str, const std::string & sep, std::vector< std::string > & result ) + { + result.resize(3); + int index = rfind( str, sep ); + if ( index < 0 ) + { + result[0] = empty_string; + result[1] = empty_string; + result[2] = str; + } + else + { + result[0] = str.substr( 0, index ); + result[1] = sep; + result[2] = str.substr( index + sep.size(), str.size() ); + } + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string strip( const std::string & str, const std::string & chars ) + { + return do_strip( str, PYSTRING_BOTHSTRIP, chars ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string lstrip( const std::string & str, const std::string & chars ) + { + return do_strip( str, PYSTRING_LEFTSTRIP, chars ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string rstrip( const std::string & str, const std::string & chars ) + { + return do_strip( str, PYSTRING_RIGHTSTRIP, chars ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string join( const std::string & str, const std::vector< std::string > & seq ) + { + std::vector< std::string >::size_type seqlen = seq.size(), i; + + if ( seqlen == 0 ) return empty_string; + if ( seqlen == 1 ) return seq[0]; + + std::string result( seq[0] ); + + for ( i = 1; i < seqlen; ++i ) + { + result += str + seq[i]; + + } + + + return result; + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + namespace + { + /* Matches the end (direction >= 0) or start (direction < 0) of self + * against substr, using the start and end arguments. Returns + * -1 on error, 0 if not found and 1 if found. + */ + + int _string_tailmatch(const std::string & self, const std::string & substr, + Py_ssize_t start, Py_ssize_t end, + int direction) + { + Py_ssize_t len = (Py_ssize_t) self.size(); + Py_ssize_t slen = (Py_ssize_t) substr.size(); + + const char* sub = substr.c_str(); + const char* str = self.c_str(); + + PYSTRING_ADJUST_INDICES(start, end, len); + + if (direction < 0) { + // startswith + if (start+slen > len) + return 0; + } else { + // endswith + if (end-start < slen || start > len) + return 0; + if (end-slen > start) + start = end - slen; + } + if (end-start >= slen) + return (!std::memcmp(str+start, sub, slen)); + + return 0; + } + } + + bool endswith( const std::string & str, const std::string & suffix, int start, int end ) + { + int result = _string_tailmatch(str, suffix, + (Py_ssize_t) start, (Py_ssize_t) end, +1); + //if (result == -1) // TODO: Error condition + + return static_cast(result); + } + + + bool startswith( const std::string & str, const std::string & prefix, int start, int end ) + { + int result = _string_tailmatch(str, prefix, + (Py_ssize_t) start, (Py_ssize_t) end, -1); + //if (result == -1) // TODO: Error condition + + return static_cast(result); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + bool isalnum( const std::string & str ) + { + std::string::size_type len = str.size(), i; + if ( len == 0 ) return false; + + + if( len == 1 ) + { + return ::isalnum( str[0] ); + } + + for ( i = 0; i < len; ++i ) + { + if ( !::isalnum( str[i] ) ) return false; + } + return true; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + bool isalpha( const std::string & str ) + { + std::string::size_type len = str.size(), i; + if ( len == 0 ) return false; + if( len == 1 ) return ::isalpha( (int) str[0] ); + + for ( i = 0; i < len; ++i ) + { + if ( !::isalpha( (int) str[i] ) ) return false; + } + return true; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + bool isdigit( const std::string & str ) + { + std::string::size_type len = str.size(), i; + if ( len == 0 ) return false; + if( len == 1 ) return ::isdigit( str[0] ); + + for ( i = 0; i < len; ++i ) + { + if ( ! ::isdigit( str[i] ) ) return false; + } + return true; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + bool islower( const std::string & str ) + { + std::string::size_type len = str.size(), i; + if ( len == 0 ) return false; + if( len == 1 ) return ::islower( str[0] ); + + // python's islower is a lot more leniant than c++ + // this will match the python behavior so that something like + // islower("hello123") is true + + bool has_cased = false; + for (i = 0; i < len; ++i) { + if (::islower(str[i])) + has_cased = true; + else if (::isupper(str[i])) + return false; + } + return has_cased; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + bool isspace( const std::string & str ) + { + std::string::size_type len = str.size(), i; + if ( len == 0 ) return false; + if( len == 1 ) return ::isspace( str[0] ); + + for ( i = 0; i < len; ++i ) + { + if ( !::isspace( str[i] ) ) return false; + } + return true; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + bool istitle( const std::string & str ) + { + std::string::size_type len = str.size(), i; + + if ( len == 0 ) return false; + if ( len == 1 ) return ::isupper( str[0] ); + + bool cased = false, previous_is_cased = false; + + for ( i = 0; i < len; ++i ) + { + if ( ::isupper( str[i] ) ) + { + if ( previous_is_cased ) + { + return false; + } + + previous_is_cased = true; + cased = true; + } + else if ( ::islower( str[i] ) ) + { + if (!previous_is_cased) + { + return false; + } + + previous_is_cased = true; + cased = true; + + } + else + { + previous_is_cased = false; + } + } + + return cased; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + bool isupper( const std::string & str ) + { + std::string::size_type len = str.size(), i; + if ( len == 0 ) return false; + if( len == 1 ) return ::isupper( str[0] ); + + // python's isupper is a lot more leniant than c++ + // this will match the python behavior so that something like + // isupper("HELLO123") is true + + bool has_cased = false; + for (std::string::size_type i = 0; i < str.size(); ++i) { + if (::isupper(str[i])) + has_cased = true; + else if (::islower(str[i])) + return false; + } + return has_cased; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string capitalize( const std::string & str ) + { + std::string s( str ); + std::string::size_type len = s.size(), i; + + if ( len > 0) + { + if (::islower(s[0])) s[0] = (char) ::toupper( s[0] ); + } + + for ( i = 1; i < len; ++i ) + { + if (::isupper(s[i])) s[i] = (char) ::tolower( s[i] ); + } + + return s; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string lower( const std::string & str ) + { + std::string s( str ); + std::string::size_type len = s.size(), i; + + for ( i = 0; i < len; ++i ) + { + if ( ::isupper( s[i] ) ) s[i] = (char) ::tolower( s[i] ); + } + + return s; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string upper( const std::string & str ) + { + std::string s( str ) ; + std::string::size_type len = s.size(), i; + + for ( i = 0; i < len; ++i ) + { + if ( ::islower( s[i] ) ) s[i] = (char) ::toupper( s[i] ); + } + + return s; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string swapcase( const std::string & str ) + { + std::string s( str ); + std::string::size_type len = s.size(), i; + + for ( i = 0; i < len; ++i ) + { + if ( ::islower( s[i] ) ) s[i] = (char) ::toupper( s[i] ); + else if (::isupper( s[i] ) ) s[i] = (char) ::tolower( s[i] ); + } + + return s; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string title( const std::string & str ) + { + std::string s( str ); + std::string::size_type len = s.size(), i; + bool previous_is_cased = false; + + for ( i = 0; i < len; ++i ) + { + int c = s[i]; + if ( ::islower(c) ) + { + if ( !previous_is_cased ) + { + s[i] = (char) ::toupper(c); + } + previous_is_cased = true; + } + else if ( ::isupper(c) ) + { + if ( previous_is_cased ) + { + s[i] = (char) ::tolower(c); + } + previous_is_cased = true; + } + else + { + previous_is_cased = false; + } + } + + return s; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string translate( const std::string & str, const std::string & table, const std::string & deletechars ) + { + std::string s; + std::string::size_type len = str.size(), dellen = deletechars.size(); + + if ( table.size() != 256 ) + { + // TODO : raise exception instead + return str; + } + + //if nothing is deleted, use faster code + if ( dellen == 0 ) + { + s = str; + for ( std::string::size_type i = 0; i < len; ++i ) + { + s[i] = table[ s[i] ]; + } + return s; + } + + + int trans_table[256]; + for ( int i = 0; i < 256; i++) + { + trans_table[i] = table[i]; + } + + for ( std::string::size_type i = 0; i < dellen; i++) + { + trans_table[(int) deletechars[i] ] = -1; + } + + for ( std::string::size_type i = 0; i < len; ++i ) + { + if ( trans_table[ (int) str[i] ] != -1 ) + { + s += table[ str[i] ]; + } + } + + return s; + + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string zfill( const std::string & str, int width ) + { + int len = (int)str.size(); + + if ( len >= width ) + { + return str; + } + + std::string s( str ); + + int fill = width - len; + + s = std::string( fill, '0' ) + s; + + + if ( s[fill] == '+' || s[fill] == '-' ) + { + s[0] = s[fill]; + s[fill] = '0'; + } + + return s; + + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string ljust( const std::string & str, int width ) + { + std::string::size_type len = str.size(); + if ( (( int ) len ) >= width ) return str; + return str + std::string( width - len, ' ' ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string rjust( const std::string & str, int width ) + { + std::string::size_type len = str.size(); + if ( (( int ) len ) >= width ) return str; + return std::string( width - len, ' ' ) + str; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string center( const std::string & str, int width ) + { + int len = (int) str.size(); + int marg, left; + + if ( len >= width ) return str; + + marg = width - len; + left = marg / 2 + (marg & width & 1); + + return std::string( left, ' ' ) + str + std::string( marg - left, ' ' ); + + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string slice( const std::string & str, int start, int end ) + { + PYSTRING_ADJUST_INDICES(start, end, (int) str.size()); + if ( start >= end ) return empty_string; + return str.substr( start, end - start ); + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + int find( const std::string & str, const std::string & sub, int start, int end ) + { + PYSTRING_ADJUST_INDICES(start, end, (int) str.size()); + + std::string::size_type result = str.find( sub, start ); + + // If we cannot find the string, or if the end-point of our found substring is past + // the allowed end limit, return that it can't be found. + if( result == std::string::npos || + (result + sub.size() > (std::string::size_type)end) ) + { + return -1; + } + + return (int) result; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + int index( const std::string & str, const std::string & sub, int start, int end ) + { + return find( str, sub, start, end ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + int rfind( const std::string & str, const std::string & sub, int start, int end ) + { + PYSTRING_ADJUST_INDICES(start, end, (int) str.size()); + + std::string::size_type result = str.rfind( sub, end ); + + if( result == std::string::npos || + result < (std::string::size_type)start || + (result + sub.size() > (std::string::size_type)end)) + return -1; + + return (int)result; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + int rindex( const std::string & str, const std::string & sub, int start, int end ) + { + return rfind( str, sub, start, end ); + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string expandtabs( const std::string & str, int tabsize ) + { + std::string s( str ); + + std::string::size_type len = str.size(), i = 0; + int offset = 0; + + int j = 0; + + for ( i = 0; i < len; ++i ) + { + if ( str[i] == '\t' ) + { + + if ( tabsize > 0 ) + { + int fillsize = tabsize - (j % tabsize); + j += fillsize; + s.replace( i + offset, 1, std::string( fillsize, ' ' )); + offset += fillsize - 1; + } + else + { + s.replace( i + offset, 1, empty_string ); + offset -= 1; + } + + } + else + { + j++; + + if (str[i] == '\n' || str[i] == '\r') + { + j = 0; + } + } + } + + return s; + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + int count( const std::string & str, const std::string & substr, int start, int end ) + { + int nummatches = 0; + int cursor = start; + + // special handling for an empty substring + // this will match python's behavior of + // "bob".count("") == 4 + // "".count("") == 1 + if ( substr.empty() ) + { + PYSTRING_ADJUST_INDICES(start, end, (int)str.size()); + return end - start + 1; + } + + while ( 1 ) + { + cursor = find( str, substr, cursor, end ); + + if ( cursor < 0 ) break; + + cursor += (int) substr.size(); + nummatches += 1; + } + + return nummatches; + + + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + std::string replace( const std::string & str, const std::string & oldstr, const std::string & newstr, int count ) + { + int sofar = 0; + int cursor = 0; + std::string s( str ); + + std::string::size_type oldlen = oldstr.size(), newlen = newstr.size(); + + cursor = find( s, oldstr, cursor ); + + while ( cursor != -1 && cursor <= (int)s.size() ) + { + if ( count > -1 && sofar >= count ) + { + break; + } + + s.replace( cursor, oldlen, newstr ); + cursor += (int) newlen; + + if ( oldlen != 0) + { + cursor = find( s, oldstr, cursor ); + } + else + { + ++cursor; + } + + ++sofar; + } + + return s; + + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + void splitlines( const std::string & str, std::vector< std::string > & result, bool keepends ) + { + result.clear(); + std::string::size_type len = str.size(), i, j, eol; + + for (i = j = 0; i < len; ) + { + while (i < len && str[i] != '\n' && str[i] != '\r') i++; + + eol = i; + if (i < len) + { + if (str[i] == '\r' && i + 1 < len && str[i+1] == '\n') + { + i += 2; + } + else + { + i++; + } + if (keepends) + eol = i; + + } + + result.push_back( str.substr( j, eol - j ) ); + j = i; + + } + + if (j < len) + { + result.push_back( str.substr( j, len - j ) ); + } + + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + std::string mul( const std::string & str, int n ) + { + // Early exits + if (n <= 0) return empty_string; + if (n == 1) return str; + + std::ostringstream os; + for(int i=0; i= 2 && p[1] == ':') + { + std::string path = p; // In case drivespec == p + drivespec = pystring::slice(path, 0, 2); + pathspec = pystring::slice(path, 2); + } + else + { + drivespec = empty_string; + pathspec = p; + } + } + + // On Posix, drive is always empty + void splitdrive_posix(std::string & drivespec, std::string & pathspec, + const std::string & path) + { + drivespec = empty_string; + pathspec = path; + } + + void splitdrive(std::string & drivespec, std::string & pathspec, + const std::string & path) + { +#ifdef WINDOWS + return splitdrive_nt(drivespec, pathspec, path); +#else + return splitdrive_posix(drivespec, pathspec, path); +#endif + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + // Test whether a path is absolute + // In windows, if the character to the right of the colon + // is a forward or backslash it's absolute. + bool isabs_nt(const std::string & path) + { + std::string drivespec, pathspec; + splitdrive_nt(drivespec, pathspec, path); + if(pathspec.empty()) return false; + return ((pathspec[0] == '/') || (pathspec[0] == '\\')); + } + + bool isabs_posix(const std::string & s) + { + return pystring::startswith(s, forward_slash); + } + + bool isabs(const std::string & path) + { +#ifdef WINDOWS + return isabs_nt(path); +#else + return isabs_posix(path); +#endif + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + std::string abspath_nt(const std::string & path, const std::string & cwd) + { + std::string p = path; + if(!isabs_nt(p)) p = join_nt(cwd, p); + return normpath_nt(p); + } + + std::string abspath_posix(const std::string & path, const std::string & cwd) + { + std::string p = path; + if(!isabs_posix(p)) p = join_posix(cwd, p); + return normpath_posix(p); + } + + std::string abspath(const std::string & path, const std::string & cwd) + { +#ifdef WINDOWS + return abspath_nt(path, cwd); +#else + return abspath_posix(path, cwd); +#endif + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + std::string join_nt(const std::vector< std::string > & paths) + { + if(paths.empty()) return empty_string; + if(paths.size() == 1) return paths[0]; + + std::string path = paths[0]; + + for(unsigned int i=1; i= 2 && path[1] != ':') || (b.size() >= 2 && b[1] == ':')) + { + // Path doesnt start with a drive letter + b_nts = true; + } + // Else path has a drive letter, and b doesn't but is absolute. + else if((path.size()>3) || + ((path.size()==3) && !pystring::endswith(path, forward_slash) && !pystring::endswith(path, double_back_slash))) + { + b_nts = true; + } + } + + if(b_nts) + { + path = b; + } + else + { + // Join, and ensure there's a separator. + // assert len(path) > 0 + if( pystring::endswith(path, forward_slash) || pystring::endswith(path, double_back_slash)) + { + if(pystring::startswith(b,forward_slash) || pystring::startswith(b,double_back_slash)) + { + path += pystring::slice(b, 1); + } + else + { + path += b; + } + } + else if(pystring::endswith(path, colon)) + { + path += b; + } + else if(!b.empty()) + { + if(pystring::startswith(b, forward_slash) || pystring::startswith(b,double_back_slash)) + { + path += b; + } + else + { + path += double_back_slash + b; + } + } + else + { + // path is not empty and does not end with a backslash, + // but b is empty; since, e.g., split('a/') produces + // ('a', ''), it's best if join() adds a backslash in + // this case. + path += double_back_slash; + } + } + } + + return path; + } + + // Join two or more pathname components, inserting double_back_slash as needed. + std::string join_nt(const std::string & a, const std::string & b) + { + std::vector< std::string > paths(2); + paths[0] = a; + paths[1] = b; + return join_nt(paths); + } + + // Join pathnames. + // If any component is an absolute path, all previous path components + // will be discarded. + // Ignore the previous parts if a part is absolute. + // Insert a '/' unless the first part is empty or already ends in '/'. + + std::string join_posix(const std::vector< std::string > & paths) + { + if(paths.empty()) return empty_string; + if(paths.size() == 1) return paths[0]; + + std::string path = paths[0]; + + for(unsigned int i=1; i paths(2); + paths[0] = a; + paths[1] = b; + return join_posix(paths); + } + + std::string join(const std::string & path1, const std::string & path2) + { +#ifdef WINDOWS + return join_nt(path1, path2); +#else + return join_posix(path1, path2); +#endif + } + + + std::string join(const std::vector< std::string > & paths) + { +#ifdef WINDOWS + return join_nt(paths); +#else + return join_posix(paths); +#endif + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + + // Split a pathname. + // Return (head, tail) where tail is everything after the final slash. + // Either part may be empty + + void split_nt(std::string & head, std::string & tail, const std::string & path) + { + std::string d, p; + splitdrive_nt(d, p, path); + + // set i to index beyond p's last slash + int i = (int)p.size(); + + // walk back to find the index of the first slash from the end + while(i>0 && (p[i-1] != '\\') && (p[i-1] != '/')) + { + i = i - 1; + } + + head = pystring::slice(p,0,i); + tail = pystring::slice(p,i); // now tail has no slashes + + // remove trailing slashes from head, unless it's all slashes + std::string head2 = head; + while(!head2.empty() && ((pystring::slice(head2,-1) == forward_slash) || + (pystring::slice(head2,-1) == double_back_slash))) + { + head2 = pystring::slice(head2,0,-1); + } + + if(!head2.empty()) head = head2; + head = d + head; + } + + + // Split a path in head (everything up to the last '/') and tail (the + // rest). If the path ends in '/', tail will be empty. If there is no + // '/' in the path, head will be empty. + // Trailing '/'es are stripped from head unless it is the root. + + void split_posix(std::string & head, std::string & tail, const std::string & p) + { + int i = pystring::rfind(p, forward_slash) + 1; + + head = pystring::slice(p,0,i); + tail = pystring::slice(p,i); + + if(!head.empty() && (head != pystring::mul(forward_slash, (int) head.size()))) + { + head = pystring::rstrip(head, forward_slash); + } + } + + void split(std::string & head, std::string & tail, const std::string & path) + { +#ifdef WINDOWS + return split_nt(head, tail, path); +#else + return split_posix(head, tail, path); +#endif + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + std::string basename_nt(const std::string & path) + { + std::string head, tail; + split_nt(head, tail, path); + return tail; + } + + std::string basename_posix(const std::string & path) + { + std::string head, tail; + split_posix(head, tail, path); + return tail; + } + + std::string basename(const std::string & path) + { +#ifdef WINDOWS + return basename_nt(path); +#else + return basename_posix(path); +#endif + } + + std::string dirname_nt(const std::string & path) + { + std::string head, tail; + split_nt(head, tail, path); + return head; + } + + std::string dirname_posix(const std::string & path) + { + std::string head, tail; + split_posix(head, tail, path); + return head; + } + + std::string dirname(const std::string & path) + { +#ifdef WINDOWS + return dirname_nt(path); +#else + return dirname_posix(path); +#endif + } + + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + // Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A\B. + std::string normpath_nt(const std::string & p) + { + std::string path = p; + path = pystring::replace(path, forward_slash,double_back_slash); + + std::string prefix; + splitdrive_nt(prefix, path, path); + + // We need to be careful here. If the prefix is empty, and the path starts + // with a backslash, it could either be an absolute path on the current + // drive (\dir1\dir2\file) or a UNC filename (\\server\mount\dir1\file). It + // is therefore imperative NOT to collapse multiple backslashes blindly in + // that case. + // The code below preserves multiple backslashes when there is no drive + // letter. This means that the invalid filename \\\a\b is preserved + // unchanged, where a\\\b is normalised to a\b. It's not clear that there + // is any better behaviour for such edge cases. + + if(prefix.empty()) + { + // No drive letter - preserve initial backslashes + while(pystring::slice(path,0,1) == double_back_slash) + { + prefix = prefix + double_back_slash; + path = pystring::slice(path,1); + } + } + else + { + // We have a drive letter - collapse initial backslashes + if(pystring::startswith(path, double_back_slash)) + { + prefix = prefix + double_back_slash; + path = pystring::lstrip(path, double_back_slash); + } + } + + std::vector comps; + pystring::split(path, comps, double_back_slash); + + int i = 0; + + while(i<(int)comps.size()) + { + if(comps[i].empty() || comps[i] == dot) + { + comps.erase(comps.begin()+i); + } + else if(comps[i] == double_dot) + { + if(i>0 && comps[i-1] != double_dot) + { + comps.erase(comps.begin()+i-1, comps.begin()+i+1); + i -= 1; + } + else if(i == 0 && pystring::endswith(prefix, double_back_slash)) + { + comps.erase(comps.begin()+i); + } + else + { + i += 1; + } + } + else + { + i += 1; + } + } + + // If the path is now empty, substitute '.' + if(prefix.empty() && comps.empty()) + { + comps.push_back(dot); + } + + return prefix + pystring::join(double_back_slash, comps); + } + + // Normalize a path, e.g. A//B, A/./B and A/foo/../B all become A/B. + // It should be understood that this may change the meaning of the path + // if it contains symbolic links! + // Normalize path, eliminating double slashes, etc. + + std::string normpath_posix(const std::string & p) + { + if(p.empty()) return dot; + + std::string path = p; + + int initial_slashes = pystring::startswith(path, forward_slash) ? 1 : 0; + + // POSIX allows one or two initial slashes, but treats three or more + // as single slash. + + if (initial_slashes && pystring::startswith(path, double_forward_slash) + && !pystring::startswith(path, triple_forward_slash)) + initial_slashes = 2; + + std::vector comps, new_comps; + pystring::split(path, comps, forward_slash); + + for(unsigned int i=0; i 0) + path = pystring::mul(forward_slash, initial_slashes) + path; + + if(path.empty()) return dot; + return path; + } + + std::string normpath(const std::string & path) + { +#ifdef WINDOWS + return normpath_nt(path); +#else + return normpath_posix(path); +#endif + } + + ////////////////////////////////////////////////////////////////////////////////////////////// + /// + /// + + // Split the extension from a pathname. + // Extension is everything from the last dot to the end, ignoring + // leading dots. Returns "(root, ext)"; ext may be empty. + // It is always true that root + ext == p + + void splitext_generic(std::string & root, std::string & ext, + const std::string & p, + const std::string & sep, + const std::string & altsep, + const std::string & extsep) + { + int sepIndex = pystring::rfind(p, sep); + if(!altsep.empty()) + { + int altsepIndex = pystring::rfind(p, altsep); + sepIndex = std::max(sepIndex, altsepIndex); + } + + int dotIndex = pystring::rfind(p, extsep); + if(dotIndex > sepIndex) + { + // Skip all leading dots + int filenameIndex = sepIndex + 1; + + while(filenameIndex < dotIndex) + { + if(pystring::slice(p,filenameIndex) != extsep) + { + root = pystring::slice(p, 0, dotIndex); + ext = pystring::slice(p, dotIndex); + return; + } + + filenameIndex += 1; + } + } + + root = p; + ext = empty_string; + } + + void splitext_nt(std::string & root, std::string & ext, const std::string & path) + { + return splitext_generic(root, ext, path, + double_back_slash, forward_slash, dot); + } + + void splitext_posix(std::string & root, std::string & ext, const std::string & path) + { + return splitext_generic(root, ext, path, + forward_slash, empty_string, dot); + } + + void splitext(std::string & root, std::string & ext, const std::string & path) + { +#ifdef WINDOWS + return splitext_nt(root, ext, path); +#else + return splitext_posix(root, ext, path); +#endif + } + +} // namespace path +} // namespace os + + +}//namespace pystring + + diff --git a/test.cpp b/test.cpp index 110c673..8c59046 100644 --- a/test.cpp +++ b/test.cpp @@ -7,6 +7,25 @@ #include "pystring.h" #include "unittest.h" +// Helper wrappers (for pystring functions that don't have direct return values) + +namespace { +static std::vector pystring_split(const std::string& s, int maxsplit) + { std::vector r; pystring::split(s, r, "", maxsplit); return r; } +static std::vector pystring_split(const std::string& s, const std::string& sep, int maxsplit) + { std::vector r; pystring::split(s, r, sep, maxsplit); return r; } +static std::vector pystring_rsplit(const std::string& s, int maxsplit) + { std::vector r; pystring::rsplit(s, r, "", maxsplit); return r; } +static std::vector pystring_rsplit(const std::string& s, const std::string& sep, int maxsplit) + { std::vector r; pystring::rsplit(s, r, sep, maxsplit); return r; } +static std::vector pystring_partition(const std::string& s, const std::string& sep) + { std::vector r; pystring::partition(s, sep, r); return r; } +static std::vector pystring_rpartition(const std::string& s, const std::string& sep) + { std::vector r; pystring::rpartition(s, sep, r); return r; } +static std::vector pystring_splitlines(const std::string& s, bool keepends) + { std::vector r; pystring::splitlines(s, r, keepends); return r; } +} // namespace + PYSTRING_TEST_APP(PyStringUnitTests) PYSTRING_ADD_TEST(pystring, endswith) @@ -33,6 +52,17 @@ PYSTRING_ADD_TEST(pystring, endswith) PYSTRING_CHECK_EQUAL(pystring::endswith("abcdef", "cdef", -10), true); } +PYSTRING_ADD_TEST(pystring, isupper) +{ + PYSTRING_CHECK_EQUAL(pystring::isupper("ABC"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("abc"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABc"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("AB-C"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("HELLO 123"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("123"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("HELLO!"), true); +} + PYSTRING_ADD_TEST(pystring, find) { PYSTRING_CHECK_EQUAL(pystring::find("", ""), 0); @@ -98,6 +128,18 @@ PYSTRING_ADD_TEST(pystring, rfind) PYSTRING_CHECK_EQUAL(pystring::rfind("abcabcabc", "abc", 6, 8), -1); } +PYSTRING_ADD_TEST(pystring, removeprefix) +{ + PYSTRING_CHECK_EQUAL(pystring::removeprefix("abcdef", "abc"), "def"); + PYSTRING_CHECK_EQUAL(pystring::removeprefix("abcdef", "bcd"), "abcdef"); +} + +PYSTRING_ADD_TEST(pystring, removesuffix) +{ + PYSTRING_CHECK_EQUAL(pystring::removesuffix("abcdef", "def"), "abc"); + PYSTRING_CHECK_EQUAL(pystring::removesuffix("abcdef", "cde"), "abcdef"); +} + PYSTRING_ADD_TEST(pystring, replace) { PYSTRING_CHECK_EQUAL(pystring::replace("abcdef", "foo", "bar"), "abcdef"); @@ -658,3 +700,567 @@ PYSTRING_ADD_TEST(pystring_os_path, splitext) splitext_nt(root, ext, "c:\\a.b.c"); PYSTRING_CHECK_EQUAL(root, "c:\\a.b"); PYSTRING_CHECK_EQUAL(ext, ".c"); splitext_nt(root, ext, "c:\\a_b.c"); PYSTRING_CHECK_EQUAL(root, "c:\\a_b"); PYSTRING_CHECK_EQUAL(ext, ".c"); } + +// the python3_compat tests were auto generated from python +// to make sure that our functions exactly match the values that are being returned in python 3.11 + +PYSTRING_ADD_TEST(python3_compat, capitalize) +{ + PYSTRING_CHECK_EQUAL(pystring::capitalize(""), ""); + PYSTRING_CHECK_EQUAL(pystring::capitalize("hello"), "Hello"); + PYSTRING_CHECK_EQUAL(pystring::capitalize("HELLO"), "Hello"); + PYSTRING_CHECK_EQUAL(pystring::capitalize("Hello World"), "Hello world"); + PYSTRING_CHECK_EQUAL(pystring::capitalize("hello123"), "Hello123"); + PYSTRING_CHECK_EQUAL(pystring::capitalize("123"), "123"); + PYSTRING_CHECK_EQUAL(pystring::capitalize("hElLo"), "Hello"); + PYSTRING_CHECK_EQUAL(pystring::capitalize("hello world!"), "Hello world!"); +} + +PYSTRING_ADD_TEST(python3_compat, center) +{ + PYSTRING_CHECK_EQUAL(pystring::center("hello", 10), " hello "); + PYSTRING_CHECK_EQUAL(pystring::center("hello", 3), "hello"); + PYSTRING_CHECK_EQUAL(pystring::center("", 5), " "); + PYSTRING_CHECK_EQUAL(pystring::center("hello", 5), "hello"); +} + +PYSTRING_ADD_TEST(python3_compat, count_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::count("", "", 0), 1); + PYSTRING_CHECK_EQUAL(pystring::count("bob", ""), 4); + PYSTRING_CHECK_EQUAL(pystring::count("", "bob"), 0); + PYSTRING_CHECK_EQUAL(pystring::count("hello world", "o", 0), 2); + PYSTRING_CHECK_EQUAL(pystring::count("hello world", "l", 0), 3); + PYSTRING_CHECK_EQUAL(pystring::count("hello world", "xyz", 0), 0); + PYSTRING_CHECK_EQUAL(pystring::count("hello world", "", 0), 12); + PYSTRING_CHECK_EQUAL(pystring::count("aaa", "a", 0), 3); + PYSTRING_CHECK_EQUAL(pystring::count("aaa", "aa", 0), 1); + PYSTRING_CHECK_EQUAL(pystring::count("hello", "l", 2, 5), 2); +} + +PYSTRING_ADD_TEST(python3_compat, endswith_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "hello", 0), false); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "world", 0), true); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "", 0), true); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "hello", 1), false); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "ello", 1), false); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello", "hello world", 0), false); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "world", 0, 5), false); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello world", "hello", 0, 5), true); + PYSTRING_CHECK_EQUAL(pystring::endswith("", "", 0), true); + PYSTRING_CHECK_EQUAL(pystring::endswith("hello", "hello", 0), true); +} + +PYSTRING_ADD_TEST(python3_compat, expandtabs) +{ + PYSTRING_CHECK_EQUAL(pystring::expandtabs("hello\tworld", 8), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::expandtabs("hello\tworld", 4), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::expandtabs("\t\t", 4), " "); + PYSTRING_CHECK_EQUAL(pystring::expandtabs("no tabs", 4), "no tabs"); + PYSTRING_CHECK_EQUAL(pystring::expandtabs("a\tb\tc", 4), "a b c"); +} + +PYSTRING_ADD_TEST(python3_compat, find_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "world", 0), 6); + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "hello", 0), 0); + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "xyz", 0), -1); + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "", 0), 0); + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "o", 0), 4); + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "o", 5), 7); + PYSTRING_CHECK_EQUAL(pystring::find("hello world", "o", 0, 5), 4); + PYSTRING_CHECK_EQUAL(pystring::find("", "hello", 0), -1); + PYSTRING_CHECK_EQUAL(pystring::find("hello", "", 0), 0); + PYSTRING_CHECK_EQUAL(pystring::find("abcabc", "bc", 0), 1); + PYSTRING_CHECK_EQUAL(pystring::find("abcabc", "bc", 2), 4); +} + +PYSTRING_ADD_TEST(python3_compat, isalnum) +{ + PYSTRING_CHECK_EQUAL(pystring::isalnum(""), false); + PYSTRING_CHECK_EQUAL(pystring::isalnum("abc"), true); + PYSTRING_CHECK_EQUAL(pystring::isalnum("123"), true); + PYSTRING_CHECK_EQUAL(pystring::isalnum("abc123"), true); + PYSTRING_CHECK_EQUAL(pystring::isalnum("abc!"), false); + PYSTRING_CHECK_EQUAL(pystring::isalnum(" "), false); + PYSTRING_CHECK_EQUAL(pystring::isalnum("abc 123"), false); +} + +PYSTRING_ADD_TEST(python3_compat, isalpha) +{ + PYSTRING_CHECK_EQUAL(pystring::isalpha(""), false); + PYSTRING_CHECK_EQUAL(pystring::isalpha("abc"), true); + PYSTRING_CHECK_EQUAL(pystring::isalpha("ABC"), true); + PYSTRING_CHECK_EQUAL(pystring::isalpha("abcABC"), true); + PYSTRING_CHECK_EQUAL(pystring::isalpha("abc1"), false); + PYSTRING_CHECK_EQUAL(pystring::isalpha("abc!"), false); + PYSTRING_CHECK_EQUAL(pystring::isalpha(" "), false); + PYSTRING_CHECK_EQUAL(pystring::isalpha("a b"), false); +} + +PYSTRING_ADD_TEST(python3_compat, isdigit) +{ + PYSTRING_CHECK_EQUAL(pystring::isdigit(""), false); + PYSTRING_CHECK_EQUAL(pystring::isdigit("0"), true); + PYSTRING_CHECK_EQUAL(pystring::isdigit("123"), true); + PYSTRING_CHECK_EQUAL(pystring::isdigit("12.3"), false); + PYSTRING_CHECK_EQUAL(pystring::isdigit("12 3"), false); + PYSTRING_CHECK_EQUAL(pystring::isdigit("abc"), false); + PYSTRING_CHECK_EQUAL(pystring::isdigit("123abc"), false); + PYSTRING_CHECK_EQUAL(pystring::isdigit(" "), false); +} + +PYSTRING_ADD_TEST(python3_compat, islower) +{ + PYSTRING_CHECK_EQUAL(pystring::islower(""), false); + PYSTRING_CHECK_EQUAL(pystring::islower("a"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("A"), false); + PYSTRING_CHECK_EQUAL(pystring::islower("abc"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("ABC"), false); + PYSTRING_CHECK_EQUAL(pystring::islower("abC"), false); + PYSTRING_CHECK_EQUAL(pystring::islower("abc123"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("123"), false); + PYSTRING_CHECK_EQUAL(pystring::islower("abc 123"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("hello!"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("HELLO!"), false); + PYSTRING_CHECK_EQUAL(pystring::islower(" "), false); + PYSTRING_CHECK_EQUAL(pystring::islower("abc\n"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("!@#$"), false); + PYSTRING_CHECK_EQUAL(pystring::islower("a!b"), true); + PYSTRING_CHECK_EQUAL(pystring::islower("1a"), true); +} + +PYSTRING_ADD_TEST(python3_compat, isspace) +{ + PYSTRING_CHECK_EQUAL(pystring::isspace(""), false); + PYSTRING_CHECK_EQUAL(pystring::isspace(" "), true); + PYSTRING_CHECK_EQUAL(pystring::isspace(" "), true); + PYSTRING_CHECK_EQUAL(pystring::isspace("\t"), true); + PYSTRING_CHECK_EQUAL(pystring::isspace("\n"), true); + PYSTRING_CHECK_EQUAL(pystring::isspace("\r"), true); + PYSTRING_CHECK_EQUAL(pystring::isspace(" \t\n"), true); + PYSTRING_CHECK_EQUAL(pystring::isspace("a"), false); + PYSTRING_CHECK_EQUAL(pystring::isspace(" a "), false); + PYSTRING_CHECK_EQUAL(pystring::isspace(" a"), false); +} + +PYSTRING_ADD_TEST(python3_compat, istitle) +{ + PYSTRING_CHECK_EQUAL(pystring::istitle(""), false); + PYSTRING_CHECK_EQUAL(pystring::istitle("Title"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("Title Case"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("title"), false); + PYSTRING_CHECK_EQUAL(pystring::istitle("TITLE"), false); + PYSTRING_CHECK_EQUAL(pystring::istitle("Title123"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("A"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("a"), false); + PYSTRING_CHECK_EQUAL(pystring::istitle("Title Case Here"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("Title Case"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("123 Title"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("Already A Title"), true); + PYSTRING_CHECK_EQUAL(pystring::istitle("not A Title"), false); +} + +PYSTRING_ADD_TEST(python3_compat, isupper) +{ + PYSTRING_CHECK_EQUAL(pystring::isupper(""), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("A"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("a"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABC"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("abc"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABc"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABC123"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("123"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABC 123"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("HELLO!"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("hello!"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper(" "), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABC\n"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("ABC\tDEF"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("!@#$"), false); + PYSTRING_CHECK_EQUAL(pystring::isupper("A!B"), true); + PYSTRING_CHECK_EQUAL(pystring::isupper("1A"), true); +} + +PYSTRING_ADD_TEST(python3_compat, join_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::join(",", {}), ""); + PYSTRING_CHECK_EQUAL(pystring::join(",", {""}), ""); + PYSTRING_CHECK_EQUAL(pystring::join(",", {"a"}), "a"); + PYSTRING_CHECK_EQUAL(pystring::join(",", {"a", "b", "c"}), "a,b,c"); + PYSTRING_CHECK_EQUAL(pystring::join("", {"a", "b", "c"}), "abc"); + PYSTRING_CHECK_EQUAL(pystring::join(", ", {"hello", "world"}), "hello, world"); + PYSTRING_CHECK_EQUAL(pystring::join("-", {"one", "two", "three"}), "one-two-three"); + PYSTRING_CHECK_EQUAL(pystring::join("/", {"path", "to", "file"}), "path/to/file"); +} + +PYSTRING_ADD_TEST(python3_compat, ljust) +{ + PYSTRING_CHECK_EQUAL(pystring::ljust("hello", 10), "hello "); + PYSTRING_CHECK_EQUAL(pystring::ljust("hello", 3), "hello"); + PYSTRING_CHECK_EQUAL(pystring::ljust("", 5), " "); + PYSTRING_CHECK_EQUAL(pystring::ljust("hello", 5), "hello"); +} + +PYSTRING_ADD_TEST(python3_compat, lower) +{ + PYSTRING_CHECK_EQUAL(pystring::lower(""), ""); + PYSTRING_CHECK_EQUAL(pystring::lower("hello"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::lower("HELLO"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::lower("Hello World"), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::lower("hello123"), "hello123"); + PYSTRING_CHECK_EQUAL(pystring::lower("123"), "123"); + PYSTRING_CHECK_EQUAL(pystring::lower("hElLo"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::lower("hello world!"), "hello world!"); +} + +PYSTRING_ADD_TEST(python3_compat, lstrip) +{ + PYSTRING_CHECK_EQUAL(pystring::lstrip(""), ""); + PYSTRING_CHECK_EQUAL(pystring::lstrip(" hello "), "hello "); + PYSTRING_CHECK_EQUAL(pystring::lstrip(" hello ", " "), "hello "); + PYSTRING_CHECK_EQUAL(pystring::lstrip("xxhelloxx", "x"), "helloxx"); + PYSTRING_CHECK_EQUAL(pystring::lstrip("\t\nhello\t\n"), "hello\t\n"); + PYSTRING_CHECK_EQUAL(pystring::lstrip("hello"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::lstrip(" "), ""); + PYSTRING_CHECK_EQUAL(pystring::lstrip("aabbcc", "ac"), "bbcc"); + PYSTRING_CHECK_EQUAL(pystring::lstrip("hello world "), "hello world "); +} + +PYSTRING_ADD_TEST(python3_compat, mul) +{ + PYSTRING_CHECK_EQUAL(pystring::mul("ab", 3), "ababab"); + PYSTRING_CHECK_EQUAL(pystring::mul("ab", 0), ""); + PYSTRING_CHECK_EQUAL(pystring::mul("ab", 1), "ab"); + PYSTRING_CHECK_EQUAL(pystring::mul("", 5), ""); + PYSTRING_CHECK_EQUAL(pystring::mul("x", 5), "xxxxx"); +} + + +PYSTRING_ADD_TEST(python3_compat, removeprefix_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::removeprefix("hello world", "hello "), "world"); + PYSTRING_CHECK_EQUAL(pystring::removeprefix("hello world", "xyz"), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::removeprefix("hello world", ""), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::removeprefix("hello", "hello"), ""); + PYSTRING_CHECK_EQUAL(pystring::removeprefix("", "x"), ""); +} + +PYSTRING_ADD_TEST(python3_compat, removesuffix_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::removesuffix("hello world", "hello "), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::removesuffix("hello world", "xyz"), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::removesuffix("hello world", ""), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::removesuffix("hello", "hello"), ""); + PYSTRING_CHECK_EQUAL(pystring::removesuffix("", "x"), ""); +} + +PYSTRING_ADD_TEST(python3_compat, replace_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::replace("hello world", "world", "python", -1), "hello python"); + PYSTRING_CHECK_EQUAL(pystring::replace("hello world", "o", "0", -1), "hell0 w0rld"); + PYSTRING_CHECK_EQUAL(pystring::replace("hello world", "o", "0", 1), "hell0 world"); + PYSTRING_CHECK_EQUAL(pystring::replace("hello world", "xyz", "abc", -1), "hello world"); + PYSTRING_CHECK_EQUAL(pystring::replace("hello world", "", "_", -1), "_h_e_l_l_o_ _w_o_r_l_d_"); + PYSTRING_CHECK_EQUAL(pystring::replace("aaa", "a", "bb", -1), "bbbbbb"); + PYSTRING_CHECK_EQUAL(pystring::replace("aaa", "a", "bb", 2), "bbbba"); + PYSTRING_CHECK_EQUAL(pystring::replace("", "a", "b", -1), ""); + PYSTRING_CHECK_EQUAL(pystring::replace("hello", "hello", "", -1), ""); +} + +PYSTRING_ADD_TEST(python3_compat, rfind_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "world", 0), 6); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "hello", 0), 0); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "xyz", 0), -1); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "", 0), 11); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "o", 0), 7); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "o", 5), 7); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello world", "o", 0, 5), 4); + PYSTRING_CHECK_EQUAL(pystring::rfind("", "hello", 0), -1); + PYSTRING_CHECK_EQUAL(pystring::rfind("hello", "", 0), 5); + PYSTRING_CHECK_EQUAL(pystring::rfind("abcabc", "bc", 0), 4); + PYSTRING_CHECK_EQUAL(pystring::rfind("abcabc", "bc", 2), 4); +} + +PYSTRING_ADD_TEST(python3_compat, rjust) +{ + PYSTRING_CHECK_EQUAL(pystring::rjust("hello", 10), " hello"); + PYSTRING_CHECK_EQUAL(pystring::rjust("hello", 3), "hello"); + PYSTRING_CHECK_EQUAL(pystring::rjust("", 5), " "); + PYSTRING_CHECK_EQUAL(pystring::rjust("hello", 5), "hello"); +} + + +PYSTRING_ADD_TEST(python3_compat, rstrip) +{ + PYSTRING_CHECK_EQUAL(pystring::rstrip(""), ""); + PYSTRING_CHECK_EQUAL(pystring::rstrip(" hello "), " hello"); + PYSTRING_CHECK_EQUAL(pystring::rstrip(" hello ", " "), " hello"); + PYSTRING_CHECK_EQUAL(pystring::rstrip("xxhelloxx", "x"), "xxhello"); + PYSTRING_CHECK_EQUAL(pystring::rstrip("\t\nhello\t\n"), "\t\nhello"); + PYSTRING_CHECK_EQUAL(pystring::rstrip("hello"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::rstrip(" "), ""); + PYSTRING_CHECK_EQUAL(pystring::rstrip("aabbcc", "ac"), "aabb"); + PYSTRING_CHECK_EQUAL(pystring::rstrip("hello world "), "hello world"); +} + +PYSTRING_ADD_TEST(python3_compat, slice_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::slice("hello", 0, 5), "hello"); + PYSTRING_CHECK_EQUAL(pystring::slice("hello", 1, 4), "ell"); + PYSTRING_CHECK_EQUAL(pystring::slice("hello", 0, -1), "hell"); + PYSTRING_CHECK_EQUAL(pystring::slice("hello", -3, -1), "ll"); + PYSTRING_CHECK_EQUAL(pystring::slice("hello", 2, 2), ""); + PYSTRING_CHECK_EQUAL(pystring::slice("hello", 3, 1), ""); + PYSTRING_CHECK_EQUAL(pystring::slice("", 0, 0), ""); + PYSTRING_CHECK_EQUAL(pystring::slice("hello world", 6, 11), "world"); +} + + +PYSTRING_ADD_TEST(python3_compat, startswith_python3) +{ + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "hello", 0), true); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "world", 0), false); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "", 0), true); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "hello", 1), false); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "ello", 1), true); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello", "hello world", 0), false); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "world", 0, 5), false); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello world", "hello", 0, 5), true); + PYSTRING_CHECK_EQUAL(pystring::startswith("", "", 0), true); + PYSTRING_CHECK_EQUAL(pystring::startswith("hello", "hello", 0), true); +} + +PYSTRING_ADD_TEST(python3_compat, strip) +{ + PYSTRING_CHECK_EQUAL(pystring::strip(""), ""); + PYSTRING_CHECK_EQUAL(pystring::strip(" hello "), "hello"); + PYSTRING_CHECK_EQUAL(pystring::strip(" hello ", " "), "hello"); + PYSTRING_CHECK_EQUAL(pystring::strip("xxhelloxx", "x"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::strip("\t\nhello\t\n"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::strip("hello"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::strip(" "), ""); + PYSTRING_CHECK_EQUAL(pystring::strip("aabbcc", "ac"), "bb"); + PYSTRING_CHECK_EQUAL(pystring::strip("hello world "), "hello world"); +} + +PYSTRING_ADD_TEST(python3_compat, swapcase) +{ + PYSTRING_CHECK_EQUAL(pystring::swapcase(""), ""); + PYSTRING_CHECK_EQUAL(pystring::swapcase("hello"), "HELLO"); + PYSTRING_CHECK_EQUAL(pystring::swapcase("HELLO"), "hello"); + PYSTRING_CHECK_EQUAL(pystring::swapcase("Hello World"), "hELLO wORLD"); + PYSTRING_CHECK_EQUAL(pystring::swapcase("hello123"), "HELLO123"); + PYSTRING_CHECK_EQUAL(pystring::swapcase("123"), "123"); + PYSTRING_CHECK_EQUAL(pystring::swapcase("hElLo"), "HeLlO"); + PYSTRING_CHECK_EQUAL(pystring::swapcase("hello world!"), "HELLO WORLD!"); +} + +PYSTRING_ADD_TEST(python3_compat, title) +{ + PYSTRING_CHECK_EQUAL(pystring::title(""), ""); + PYSTRING_CHECK_EQUAL(pystring::title("hello world"), "Hello World"); + PYSTRING_CHECK_EQUAL(pystring::title("HELLO WORLD"), "Hello World"); + PYSTRING_CHECK_EQUAL(pystring::title("hello"), "Hello"); + PYSTRING_CHECK_EQUAL(pystring::title("it's a test"), "It'S A Test"); + PYSTRING_CHECK_EQUAL(pystring::title("they're bill's friends from the UK"), "They'Re Bill'S Friends From The Uk"); + PYSTRING_CHECK_EQUAL(pystring::title("hello123world"), "Hello123World"); +} + +PYSTRING_ADD_TEST(python3_compat, upper) +{ + PYSTRING_CHECK_EQUAL(pystring::upper(""), ""); + PYSTRING_CHECK_EQUAL(pystring::upper("hello"), "HELLO"); + PYSTRING_CHECK_EQUAL(pystring::upper("HELLO"), "HELLO"); + PYSTRING_CHECK_EQUAL(pystring::upper("Hello World"), "HELLO WORLD"); + PYSTRING_CHECK_EQUAL(pystring::upper("hello123"), "HELLO123"); + PYSTRING_CHECK_EQUAL(pystring::upper("123"), "123"); + PYSTRING_CHECK_EQUAL(pystring::upper("hElLo"), "HELLO"); + PYSTRING_CHECK_EQUAL(pystring::upper("hello world!"), "HELLO WORLD!"); +} + +PYSTRING_ADD_TEST(python3_compat, zfill) +{ + PYSTRING_CHECK_EQUAL(pystring::zfill("42", 5), "00042"); + PYSTRING_CHECK_EQUAL(pystring::zfill("42", 2), "42"); + PYSTRING_CHECK_EQUAL(pystring::zfill("-42", 5), "-0042"); + PYSTRING_CHECK_EQUAL(pystring::zfill("+42", 5), "+0042"); + PYSTRING_CHECK_EQUAL(pystring::zfill("hello", 8), "000hello"); + PYSTRING_CHECK_EQUAL(pystring::zfill("", 3), "000"); +} + + +PYSTRING_ADD_TEST(python3_compat, rpartition_python3) +{ + { + std::vector _e17 = {"hello", " ", "world"}; + PYSTRING_CHECK_ASSERT((pystring_rpartition("hello world", " ")) == _e17); + } + { + std::vector _e19 = {"", "", "hello world"}; + PYSTRING_CHECK_ASSERT((pystring_rpartition("hello world", "xyz")) == _e19); + } + { + std::vector _e21 = {"", "hello", ""}; + PYSTRING_CHECK_ASSERT((pystring_rpartition("hello", "hello")) == _e21); + } + { + std::vector _e23 = {"", "", ""}; + PYSTRING_CHECK_ASSERT((pystring_rpartition("", "x")) == _e23); + } + { + std::vector _e25 = {"aXb", "X", "c"}; + PYSTRING_CHECK_ASSERT((pystring_rpartition("aXbXc", "X")) == _e25); + } + { + std::vector _e27 = {"hello ", "world", ""}; + PYSTRING_CHECK_ASSERT((pystring_rpartition("hello world", "world")) == _e27); + } +} + + +PYSTRING_ADD_TEST(python3_compat, rsplit_python3) +{ + { + std::vector _e12 = {"hello world", "foo"}; + PYSTRING_CHECK_ASSERT((pystring_rsplit("hello world foo", " ", 1)) == _e12); + } + { + std::vector _e13 = {"a,b", "c", "d"}; + PYSTRING_CHECK_ASSERT((pystring_rsplit("a,b,c,d", ",", 2)) == _e13); + } + { + std::vector _e14 = {" hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_rsplit(" hello world ", 1)) == _e14); + } + { + std::vector _e15 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_rsplit("hello world", -1)) == _e15); + } +} + + +PYSTRING_ADD_TEST(python3_compat, split_python3) +{ + { + std::vector _e0 = {}; + PYSTRING_CHECK_ASSERT((pystring_split("", -1)) == _e0); + } + { + std::vector _e1 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_split("hello world", -1)) == _e1); + } + { + std::vector _e2 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_split(" hello world ", -1)) == _e2); + } + { + std::vector _e3 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_split("hello world", " ", -1)) == _e3); + } + { + std::vector _e4 = {"hello world"}; + PYSTRING_CHECK_ASSERT((pystring_split("hello world", " ", 0)) == _e4); + } + { + std::vector _e5 = {"hello", "world foo"}; + PYSTRING_CHECK_ASSERT((pystring_split("hello world foo", " ", 1)) == _e5); + } + { + std::vector _e6 = {"a", "b", "c", "d"}; + PYSTRING_CHECK_ASSERT((pystring_split("a,b,c,d", ",", -1)) == _e6); + } + { + std::vector _e7 = {"a", "b", "c,d"}; + PYSTRING_CHECK_ASSERT((pystring_split("a,b,c,d", ",", 2)) == _e7); + } + { + std::vector _e8 = {"a", "b", "c"}; + PYSTRING_CHECK_ASSERT((pystring_split("aXXbXXc", "XX", -1)) == _e8); + } + { + std::vector _e9 = {"hello"}; + PYSTRING_CHECK_ASSERT((pystring_split("hello", "x", -1)) == _e9); + } + { + std::vector _e10 = {}; + PYSTRING_CHECK_ASSERT((pystring_split(" ", -1)) == _e10); + } + { + std::vector _e11 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_split("\thello\tworld\t", -1)) == _e11); + } +} + + + +PYSTRING_ADD_TEST(python3_compat, splitlines) +{ + { + std::vector _e28 = {}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("", false)) == _e28); + } + { + std::vector _e29 = {}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("", true)) == _e29); + } + { + std::vector _e30 = {"hello"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello", false)) == _e30); + } + { + std::vector _e31 = {"hello"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello", true)) == _e31); + } + { + std::vector _e32 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\nworld", false)) == _e32); + } + { + std::vector _e33 = {"hello\n", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\nworld", true)) == _e33); + } + { + std::vector _e34 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\r\nworld", false)) == _e34); + } + { + std::vector _e35 = {"hello\r\n", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\r\nworld", true)) == _e35); + } + { + std::vector _e36 = {"hello", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\rworld", false)) == _e36); + } + { + std::vector _e37 = {"hello\r", "world"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\rworld", true)) == _e37); + } + { + std::vector _e38 = {"line1", "line2", "line3"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("line1\nline2\nline3", false)) == _e38); + } + { + std::vector _e39 = {"line1\n", "line2\n", "line3"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("line1\nline2\nline3", true)) == _e39); + } + { + std::vector _e40 = {"hello"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\n", false)) == _e40); + } + { + std::vector _e41 = {"hello\n"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("hello\n", true)) == _e41); + } + { + std::vector _e42 = {"", "hello"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("\nhello", false)) == _e42); + } + { + std::vector _e43 = {"\n", "hello"}; + PYSTRING_CHECK_ASSERT((pystring_splitlines("\nhello", true)) == _e43); + } +} + diff --git a/unittest.h b/unittest.h index 536d9cb..585f417 100644 --- a/unittest.h +++ b/unittest.h @@ -23,11 +23,11 @@ struct PYSTRINGTest PYSTRINGTestFunc function; }; -typedef std::vector UnitTests; +typedef std::vector UnitTests; UnitTests& GetUnitTests(); -struct AddTest { AddTest(PYSTRINGTest* test); }; +struct AddTest { AddTest(PYSTRINGTest&& test); }; /// PYSTRING_CHECK_* macros checks if the conditions is met, and if not, /// prints an error message indicating the module and line where the @@ -102,22 +102,22 @@ struct AddTest { AddTest(PYSTRINGTest* test); }; #define PYSTRING_ADD_TEST(group, name) \ static void pystringtest_##group##_##name(); \ - AddTest pystringaddtest_##group##_##name(new PYSTRINGTest(#group, #name, pystringtest_##group##_##name)); \ + AddTest pystringaddtest_##group##_##name(PYSTRINGTest(#group, #name, pystringtest_##group##_##name)); \ static void pystringtest_##group##_##name() #define PYSTRING_TEST_SETUP() \ int unit_test_failures = 0 #define PYSTRING_TEST_APP(app) \ - std::vector& GetUnitTests() { \ - static std::vector pystring_unit_tests; \ + std::vector& GetUnitTests() { \ + static std::vector pystring_unit_tests; \ return pystring_unit_tests; } \ - AddTest::AddTest(PYSTRINGTest* test){GetUnitTests().push_back(test);}; \ + AddTest::AddTest(PYSTRINGTest&& test){GetUnitTests().emplace_back(test);} \ PYSTRING_TEST_SETUP(); \ int main(int, char **) { std::cerr << "\n" << #app <<"\n\n"; \ for(size_t i = 0; i < GetUnitTests().size(); ++i) { \ - int _tmp = unit_test_failures; GetUnitTests()[i]->function(); \ - std::cerr << "Test [" << GetUnitTests()[i]->group << "] [" << GetUnitTests()[i]->name << "] - "; \ + int _tmp = unit_test_failures; GetUnitTests()[i].function(); \ + std::cerr << "Test [" << GetUnitTests()[i].group << "] [" << GetUnitTests()[i].name << "] - "; \ std::cerr << (_tmp == unit_test_failures ? "PASSED" : "FAILED") << "\n"; } \ std::cerr << "\n" << unit_test_failures << " tests failed\n\n"; \ return unit_test_failures; }