From 24f7dfaa13a182071cedd674a28c461211c20d4c Mon Sep 17 00:00:00 2001 From: Vibe Nuage Agent Date: Thu, 20 Aug 2026 17:15:20 +0000 Subject: [PATCH 1/2] feat: Create hello world template with Pascal triangle function - Add template_ext.c and template_ext.h with hello_world() and pascal_triangle() functions - Configure config.m4 for template_ext extension - Add C unit tests using TEST_CASE and TEST_MATRIX macros - Include PHP integration tests with PestPHP - Keep Taskfile, Ceedling, and PestPHP configurations from validation-ext - Add stubs for PHP function signatures Closes #1 Co-authored-by: matapatos --- .gitignore | 56 ++++++++ DEVELOPMENT.md | 8 ++ README.md | 81 +++++++++++- Taskfile.yml | 97 ++++++++++++++ composer.json | 30 +++++ config.m4 | 14 ++ phpunit.xml | 21 +++ project.yml | 159 +++++++++++++++++++++++ stubs/functions.stub.php | 7 + template_ext.c | 98 ++++++++++++++ template_ext.h | 17 +++ tests/c/support/test_helpers.c | 25 ++++ tests/c/support/test_helpers.h | 7 + tests/c/test_pascal.c | 121 +++++++++++++++++ tests/php/Integration/HelloWorldTest.php | 34 +++++ tests/php/Pest.php | 8 ++ 16 files changed, 781 insertions(+), 2 deletions(-) create mode 100644 .gitignore create mode 100644 DEVELOPMENT.md create mode 100644 Taskfile.yml create mode 100644 composer.json create mode 100644 config.m4 create mode 100644 phpunit.xml create mode 100644 project.yml create mode 100644 stubs/functions.stub.php create mode 100644 template_ext.c create mode 100644 template_ext.h create mode 100644 tests/c/support/test_helpers.c create mode 100644 tests/c/support/test_helpers.h create mode 100644 tests/c/test_pascal.c create mode 100644 tests/php/Integration/HelloWorldTest.php create mode 100644 tests/php/Pest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7a407f1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,56 @@ +# Stubs +*_arginfo.h +vendor/ + +# Build products +*.so +*.lo +*.la +*.dep + +# Build directories +.libs/ +build/ + +# Configuration files +config.h +config.h.in +config.log +config.nice +config.status +configure +configure.ac + +# Makefiles +Makefile +Makefile.fragments +Makefile.objects + +# Autom4te cache +autom4te.cache/ + +# Libtool files +libtool + +# Test files +run-tests.php + +# IDE/Editor files +.idea/ +.vscode/ +*.swp +*.swo + +# macOS +.DS_Store + +# Editor backup files +*~ + +# PHPT test artifacts +tests/phpt/*.diff +tests/phpt/*.exp +tests/phpt/*.log +tests/phpt/*.out +tests/phpt/*.php +tests/phpt/*.sh diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..0975e07 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,8 @@ +# Development + +## Dependencies + +- PHP 8.2+ +- [Composer](https://getcomposer.org) +- [Task](https://taskfile.dev) +- [Reflex](https://github.com/cespare/reflex) diff --git a/README.md b/README.md index bcc9a9f..01e3ceb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,79 @@ -# template-ext -Modern PHP extension template - PestPHP + C unit tests ๐Ÿงช +# Template Extension + +**Template Extension** is a hello world PHP extension template with Pascal's triangle functionality. + +## Features + +- `hello_world()` - A simple hello world function +- `pascal_triangle(int $n, int $k)` - Computes binomial coefficient C(n, k) from Pascal's triangle + +## Installation + +```bash +# Build and install the extension +phpize && ./configure --enable-template-ext +make +make install +``` + +## Usage + +```php + + + + + tests/php/ + + + + + + + + + + + + + + + + diff --git a/project.yml b/project.yml new file mode 100644 index 0000000..227ee3d --- /dev/null +++ b/project.yml @@ -0,0 +1,159 @@ +# ========================================================================= +# Ceedling - Test-Centered Build System for C +# ThrowTheSwitch.org +# Copyright (c) 2010-26 Mike Karlesky, Mark VanderVoord, & Greg Williams +# SPDX-License-Identifier: MIT +# ========================================================================= + +--- +:project: + :name: "Template Extension" + + # how to use ceedling. If you're not sure, leave this as `gem` and `?` + :which_ceedling: gem + :ceedling_version: 1.1.2 + + # optional features. If you don't need them, keep them turned off for performance + :use_mocks: FALSE + :use_test_preprocessor: :all # options are :none, :mocks, :tests, or :all + :use_backtrace: :simple # options are :none, :simple, or :gdb + + # tweak the way ceedling handles automatic tasks + :build_root: build + :test_file_prefix: test_ + :default_tasks: + - test:all + + # performance options. If your tools start giving mysterious errors, consider + # dropping this to 1 to force single-tasking + :test_threads: 8 + :compile_threads: 8 + + # enable release build (more details in release_build section below) + :release_build: FALSE + +# Specify where to find mixins and any that should be enabled automatically +:mixins: + :enabled: [] + :load_paths: [] + +# further details to configure the way Ceedling handles test code +:test_build: + :use_assembly: FALSE + +# further details to configure the way Ceedling handles release code +:release_build: + :output: template_ext.out + :use_assembly: FALSE + :artifacts: [] + +# Plugins are optional Ceedling features which can be enabled. Ceedling supports +# a variety of plugins which may effect the way things are compiled, reported, +# or may provide new command options. Refer to the readme in each plugin for +# details on how to use it. +:plugins: + :load_paths: [] + :enabled: + - module_generator # handy for quickly creating source, header, and test templates + - report_tests_pretty_stdout + +# Specify which reports you'd like from the log factory +:report_tests_log_factory: + :reports: + - json + - junit + - cppunit + - html + +# override the default extensions for your system and toolchain +:extension: + :executable: .out + +# This is where Ceedling should look for your source and test files. +:paths: + :test: + - +:tests/c/** + - -:tests/c/support + :source: + - template_ext.c + :include: + - template_ext.h + :support: + - tests/c/support + :libraries: [] + +:files: + :test: [] + :source: [] + +:defines: + :test: + - TEST + - TESTING + :release: [] + :use_test_definition: FALSE + +:flags: + :release: + :compile: + - -Wall + - --O2 + :test: + :compile: + '*': + - -I$(php-config --include-dir) + - -I$(php-config --include-dir)/main + - -I$(php-config --include-dir)/TSRM + - -I$(php-config --include-dir)/Zend + - -I$(php-config --include-dir)/ext + - -I$(php-config --include-dir)/ext/date/lib + - -D_GNU_SOURCE + - -Wno-implicit-function-declaration + +:cmock: + :plugins: + - :ignore + - :callback + :verbosity: 2 + :when_no_prototypes: :warn + :strippables: [] + :attributes: + - __ramfunc + - __irq + - __fiq + - register + - extern + :c_calling_conventions: + - __stdcall + - __cdecl + - __fastcall + :treat_externs: :exclude + :treat_inlines: :exclude + :treat_as: + uint8: HEX8 + uint16: HEX16 + uint32: UINT32 + int8: INT8 + bool: UINT8 + :memcmp_if_unknown: true + :when_ptr: :compare_data + :enforce_strict_ordering: true + :fail_on_unexpected_calls: true + :callback_include_count: true + :callback_after_arg_check: false + :exclude_setjmp_h: false + +:unity: + :use_param_tests: true + :defines: + - UNITY_EXCLUDE_FLOAT + +:environment: [] + +:libraries: + :placement: :end + :flag: "-l${1}" + :path_flag: "-L ${1}" + :system: [] + :test: [] + :release: [] diff --git a/stubs/functions.stub.php b/stubs/functions.stub.php new file mode 100644 index 0000000..2f4a1a1 --- /dev/null +++ b/stubs/functions.stub.php @@ -0,0 +1,7 @@ + n) { + php_error_docref(NULL, E_WARNING, "Invalid parameters: n and k must be non-negative, and k must be <= n"); + RETURN_FALSE; + } + + /* Compute binomial coefficient C(n, k) = n! / (k! * (n-k)!) */ + /* Using iterative approach to avoid overflow for reasonable values */ + zend_long result = 1; + zend_long i; + + /* C(n, k) = C(n, n-k), so use the smaller k for efficiency */ + if (k > n - k) { + k = n - k; + } + + for (i = 1; i <= k; i++) { + result = result * (n - k + i) / i; + } + + RETURN_LONG(result); +} + +/* Hello world function */ +PHP_FUNCTION(hello_world) +{ + php_printf("Hello, World!\n"); + RETURN_TRUE; +} + +/* Module entry */ +zend_module_entry template_ext_module_entry = { + STANDARD_MODULE_HEADER, + EXTENSION_NAME, + ext_functions, + PHP_MINIT(template_ext), + PHP_MSHUTDOWN(template_ext), + PHP_RINIT(template_ext), + PHP_RSHUTDOWN(template_ext), + PHP_MINFO(template_ext), + EXTENSION_VERSION, + STANDARD_MODULE_PROPERTIES +}; + +#ifdef COMPILE_DL_TEMPLATE_EXT +ZEND_GET_MODULE(template_ext) +#endif diff --git a/template_ext.h b/template_ext.h new file mode 100644 index 0000000..b6b2c08 --- /dev/null +++ b/template_ext.h @@ -0,0 +1,17 @@ +/* + * Header for template-ext PHP extension + */ + +#ifndef TEMPLATE_EXT_H +#define TEMPLATE_EXT_H + +#define EXTENSION_NAME "template_ext" +#define EXTENSION_VERSION "1.0.0" + +static const zend_function_entry ext_functions[] = { + ZEND_FE(hello_world, NULL) + ZEND_FE(pascal_triangle, NULL) + ZEND_FE_END +}; + +#endif /* TEMPLATE_EXT_H */ diff --git a/tests/c/support/test_helpers.c b/tests/c/support/test_helpers.c new file mode 100644 index 0000000..d431334 --- /dev/null +++ b/tests/c/support/test_helpers.c @@ -0,0 +1,25 @@ +#include "unity.h" +#include "test_helpers.h" +#include + +// Pascal's triangle calculation function +// Computes binomial coefficient C(n, k) = n! / (k! * (n-k)!) +long pascal_triangle_calc(long n, long k) { + // Handle invalid cases + if (n < 0 || k < 0 || k > n) { + return 0; + } + + // C(n, k) = C(n, n-k), so use the smaller k for efficiency + if (k > n - k) { + k = n - k; + } + + // Compute using iterative approach to avoid overflow + long result = 1; + for (long i = 1; i <= k; i++) { + result = result * (n - k + i) / i; + } + + return result; +} diff --git a/tests/c/support/test_helpers.h b/tests/c/support/test_helpers.h new file mode 100644 index 0000000..ae60172 --- /dev/null +++ b/tests/c/support/test_helpers.h @@ -0,0 +1,7 @@ +#ifndef TEST_HELPERS_H +#define TEST_HELPERS_H + +// Pascal's triangle calculation function for testing +long pascal_triangle_calc(long n, long k); + +#endif /* TEST_HELPERS_H */ diff --git a/tests/c/test_pascal.c b/tests/c/test_pascal.c new file mode 100644 index 0000000..3200952 --- /dev/null +++ b/tests/c/test_pascal.c @@ -0,0 +1,121 @@ +#include "unity.h" +#include "test_helpers.h" + +// Test cases for Pascal's triangle binomial coefficient function + +// ============= Basic Cases ============= + +TEST_CASE(0, 0, 1) +TEST_CASE(1, 0, 1) +TEST_CASE(1, 1, 1) +TEST_CASE(2, 0, 1) +TEST_CASE(2, 1, 2) +TEST_CASE(2, 2, 1) +TEST_CASE(3, 0, 1) +TEST_CASE(3, 1, 3) +TEST_CASE(3, 2, 3) +TEST_CASE(3, 3, 1) +TEST_CASE(4, 0, 1) +TEST_CASE(4, 1, 4) +TEST_CASE(4, 2, 6) +TEST_CASE(4, 3, 4) +TEST_CASE(4, 4, 1) +TEST_CASE(5, 0, 1) +TEST_CASE(5, 1, 5) +TEST_CASE(5, 2, 10) +TEST_CASE(5, 3, 10) +TEST_CASE(5, 4, 5) +TEST_CASE(5, 5, 1) +void test_pascal_triangle_basic(long n, long k, long expected) { + long result = pascal_triangle_calc(n, k); + TEST_ASSERT_EQUAL(expected, result); +} + +// ============= Symmetry Test ============= + +void test_pascal_symmetry(void) { + // C(n, k) = C(n, n-k) + for (int n = 0; n <= 10; n++) { + for (int k = 0; k <= n / 2; k++) { + long c_nk = pascal_triangle_calc(n, k); + long c_nnk = pascal_triangle_calc(n, n - k); + char message[100]; + snprintf(message, sizeof(message), "C(%d,%d) != C(%d,%d)", n, k, n, n - k); + TEST_ASSERT_EQUAL_MESSAGE(message, c_nnk, c_nk); + } + } +} + +// ============= Row Sum Test ============= + +void test_pascal_row_sum(void) { + // Sum of row n should be 2^n + for (int n = 0; n <= 10; n++) { + long sum = 0; + for (int k = 0; k <= n; k++) { + sum += pascal_triangle_calc(n, k); + } + long expected = 1L << n; // 2^n + char message[100]; + snprintf(message, sizeof(message), "Row %d sum should be %ld, got %ld", n, expected, sum); + TEST_ASSERT_EQUAL_MESSAGE(message, expected, sum); + } +} + +// ============= Edge Cases ============= + +void test_pascal_zero_n(void) { + // C(0, 0) = 1 + TEST_ASSERT_EQUAL(1, pascal_triangle_calc(0, 0)); +} + +void test_pascal_invalid_k_greater_than_n(void) { + // Should return 0 when k > n + TEST_ASSERT_EQUAL(0, pascal_triangle_calc(5, 6)); + TEST_ASSERT_EQUAL(0, pascal_triangle_calc(10, 15)); +} + +void test_pascal_negative_parameters(void) { + // Should return 0 for negative parameters + TEST_ASSERT_EQUAL(0, pascal_triangle_calc(-1, 0)); + TEST_ASSERT_EQUAL(0, pascal_triangle_calc(5, -1)); + TEST_ASSERT_EQUAL(0, pascal_triangle_calc(-1, -1)); +} + +// ============= Large Values ============= + +TEST_CASE(10, 5, 252) +TEST_CASE(12, 6, 924) +TEST_CASE(15, 5, 3003) +TEST_CASE(20, 10, 184756) +void test_pascal_large_values(long n, long k, long expected) { + long result = pascal_triangle_calc(n, k); + TEST_ASSERT_EQUAL(expected, result); +} + +// ============= Test Matrix for Multiple Values ============= + +// Test that C(n, 0) = 1 for any n +TEST_MATRIX([0, 1, 2, 3, 4, 5, 10, 15, 20]) +void test_pascal_first_column(long n) { + TEST_ASSERT_EQUAL(1, pascal_triangle_calc(n, 0)); +} + +// Test that C(n, n) = 1 for any n +TEST_MATRIX([0, 1, 2, 3, 4, 5, 10, 15, 20]) +void test_pascal_last_column(long n) { + TEST_ASSERT_EQUAL(1, pascal_triangle_calc(n, n)); +} + +// Test that C(n, 1) = n for any n >= 1 +TEST_MATRIX([1, 2, 3, 4, 5, 10, 15, 20]) +void test_pascal_second_column(long n) { + TEST_ASSERT_EQUAL(n, pascal_triangle_calc(n, 1)); +} + +// Test that C(n, 2) = n*(n-1)/2 for any n >= 2 +TEST_MATRIX([2, 3, 4, 5, 10]) +void test_pascal_third_column(long n) { + long expected = n * (n - 1) / 2; + TEST_ASSERT_EQUAL(expected, pascal_triangle_calc(n, 2)); +} diff --git a/tests/php/Integration/HelloWorldTest.php b/tests/php/Integration/HelloWorldTest.php new file mode 100644 index 0000000..c145d52 --- /dev/null +++ b/tests/php/Integration/HelloWorldTest.php @@ -0,0 +1,34 @@ +toBeTrue(); +}); + +it('pascal_triangle computes binomial coefficients', function () { + expect(pascal_triangle(0, 0))->toBe(1); + expect(pascal_triangle(1, 0))->toBe(1); + expect(pascal_triangle(1, 1))->toBe(1); + expect(pascal_triangle(2, 1))->toBe(2); + expect(pascal_triangle(3, 1))->toBe(3); + expect(pascal_triangle(4, 2))->toBe(6); + expect(pascal_triangle(5, 2))->toBe(10); + expect(pascal_triangle(5, 3))->toBe(10); +}); + +it('pascal_triangle returns 0 for invalid inputs', function () { + expect(pascal_triangle(-1, 0))->toBe(0); + expect(pascal_triangle(5, -1))->toBe(0); + expect(pascal_triangle(5, 6))->toBe(0); +}); + +it('pascal_triangle demonstrates symmetry', function () { + // C(n, k) = C(n, n-k) + expect(pascal_triangle(10, 3))->toBe(pascal_triangle(10, 7)); + expect(pascal_triangle(8, 2))->toBe(pascal_triangle(8, 6)); + expect(pascal_triangle(15, 5))->toBe(pascal_triangle(15, 10)); +}); diff --git a/tests/php/Pest.php b/tests/php/Pest.php new file mode 100644 index 0000000..d4db083 --- /dev/null +++ b/tests/php/Pest.php @@ -0,0 +1,8 @@ +group('integration')->in('Integration'); From 8afda830e45ac82cecc06b5dac26a784f95e9e7d Mon Sep 17 00:00:00 2001 From: Vibe Nuage Agent Date: Thu, 20 Aug 2026 20:18:30 +0000 Subject: [PATCH 2/2] feat: Replace pascal_triangle with to_snake_case function - Remove hello_world and pascal_triangle functions - Add to_snake() function for string to snake_case conversion - Update C unit tests with TEST_CASE and TEST_MATRIX for to_snake - Update PHP integration tests with PestPHP - Update minimum PHP version to 8.1 - Update documentation and examples Closes #2 Co-authored-by: matapatos --- README.md | 41 ++++-- composer.json | 8 +- stubs/functions.stub.php | 3 +- template_ext.c | 92 ++++++++++---- template_ext.h | 3 +- tests/c/support/test_helpers.c | 73 +++++++++-- tests/c/support/test_helpers.h | 4 +- tests/c/test_pascal.c | 121 ------------------ tests/c/test_to_snake.c | 155 +++++++++++++++++++++++ tests/php/Integration/HelloWorldTest.php | 34 ----- tests/php/Integration/ToSnakeTest.php | 72 +++++++++++ 11 files changed, 387 insertions(+), 219 deletions(-) delete mode 100644 tests/c/test_pascal.c create mode 100644 tests/c/test_to_snake.c delete mode 100644 tests/php/Integration/HelloWorldTest.php create mode 100644 tests/php/Integration/ToSnakeTest.php diff --git a/README.md b/README.md index 01e3ceb..f61184d 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,18 @@ # Template Extension -**Template Extension** is a hello world PHP extension template with Pascal's triangle functionality. +**Template Extension** is a PHP extension template with string to_snake_case conversion functionality. ## Features -- `hello_world()` - A simple hello world function -- `pascal_triangle(int $n, int $k)` - Computes binomial coefficient C(n, k) from Pascal's triangle +- `to_snake(string $input)` - Converts strings to snake_case format + +## Supported Conversions + +- camelCase โ†’ snake_case +- PascalCase โ†’ snake_case +- kebab-case โ†’ snake_case +- Space separated โ†’ snake_case +- Already snake_case โ†’ snake_case (unchanged) ## Installation @@ -21,25 +28,31 @@ make install ```php +#include /* Module startup */ PHP_MINIT_FUNCTION(template_ext) @@ -40,43 +42,81 @@ PHP_MINFO_FUNCTION(template_ext) DISPLAY_INI_ENTRIES(); } -/* Pascal's triangle function - computes binomial coefficient C(n, k) */ -PHP_FUNCTION(pascal_triangle) +/* Helper function to check if a character is uppercase */ +static inline int is_uppercase(char c) { - zend_long n, k; + return c >= 'A' && c <= 'Z'; +} + +/* Helper function to check if a character is alphanumeric */ +static inline int is_alphanumeric(char c) +{ + return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9'); +} + +/* Converts a string to snake_case */ +PHP_FUNCTION(to_snake) +{ + char *input; + size_t input_len; - ZEND_PARSE_PARAMETERS_START(2, 2) - Z_PARAM_LONG(n) - Z_PARAM_LONG(k) + ZEND_PARSE_PARAMETERS_START(1, 1) + Z_PARAM_STRING(input, input_len) ZEND_PARSE_PARAMETERS_END(); - if (n < 0 || k < 0 || k > n) { - php_error_docref(NULL, E_WARNING, "Invalid parameters: n and k must be non-negative, and k must be <= n"); - RETURN_FALSE; + if (input_len == 0) { + RETURN_EMPTY_STRING(); } - /* Compute binomial coefficient C(n, k) = n! / (k! * (n-k)!) */ - /* Using iterative approach to avoid overflow for reasonable values */ - zend_long result = 1; - zend_long i; + // Allocate output buffer (worst case: every char becomes underscore + char) + char *output = emalloc(input_len * 2 + 1); + size_t output_len = 0; + int prev_was_separator = 0; - /* C(n, k) = C(n, n-k), so use the smaller k for efficiency */ - if (k > n - k) { - k = n - k; + for (size_t i = 0; i < input_len; i++) { + char c = input[i]; + + // Handle separators: convert -, space to underscore + if (c == '-' || c == ' ') { + if (output_len > 0 && !prev_was_separator) { + output[output_len++] = '_'; + } + prev_was_separator = 1; + continue; + } + + // Handle uppercase letters + if (is_uppercase(c)) { + // Add underscore before uppercase letter (except at start) + if (output_len > 0 && !prev_was_separator) { + // Check if previous character was lowercase (camelCase) + if (i > 0 && islower(input[i-1])) { + output[output_len++] = '_'; + } + } + output[output_len++] = tolower(c); + prev_was_separator = 0; + } else if (is_alphanumeric(c)) { + // Just copy lowercase letters and numbers + output[output_len++] = c; + prev_was_separator = 0; + } else { + // Skip other special characters + if (output_len > 0 && !prev_was_separator) { + prev_was_separator = 1; + } + } } - for (i = 1; i <= k; i++) { - result = result * (n - k + i) / i; + // Remove trailing underscore + while (output_len > 0 && output[output_len - 1] == '_') { + output_len--; } - RETURN_LONG(result); -} - -/* Hello world function */ -PHP_FUNCTION(hello_world) -{ - php_printf("Hello, World!\n"); - RETURN_TRUE; + output[output_len] = '\0'; + + RETVAL_STRINGL(output, output_len); + efree(output); } /* Module entry */ diff --git a/template_ext.h b/template_ext.h index b6b2c08..8abb62a 100644 --- a/template_ext.h +++ b/template_ext.h @@ -9,8 +9,7 @@ #define EXTENSION_VERSION "1.0.0" static const zend_function_entry ext_functions[] = { - ZEND_FE(hello_world, NULL) - ZEND_FE(pascal_triangle, NULL) + ZEND_FE(to_snake, NULL) ZEND_FE_END }; diff --git a/tests/c/support/test_helpers.c b/tests/c/support/test_helpers.c index d431334..ed80506 100644 --- a/tests/c/support/test_helpers.c +++ b/tests/c/support/test_helpers.c @@ -1,25 +1,70 @@ #include "unity.h" #include "test_helpers.h" #include +#include +#include -// Pascal's triangle calculation function -// Computes binomial coefficient C(n, k) = n! / (k! * (n-k)!) -long pascal_triangle_calc(long n, long k) { - // Handle invalid cases - if (n < 0 || k < 0 || k > n) { - return 0; +// Helper function to check if a character is uppercase +static inline int is_uppercase(char c) +{ + return c >= 'A' && c <= 'Z'; +} + +// String to snake_case conversion function +char* to_snake_calc(const char* input) +{ + if (input == NULL || strlen(input) == 0) { + char* result = emalloc(1); + result[0] = '\0'; + return result; } - // C(n, k) = C(n, n-k), so use the smaller k for efficiency - if (k > n - k) { - k = n - k; + // Allocate output buffer (worst case: every char becomes underscore + char) + char* output = emalloc(strlen(input) * 2 + 1); + size_t output_len = 0; + int prev_was_separator = 0; + + for (size_t i = 0; input[i]; i++) { + char c = input[i]; + + // Handle separators: convert -, space to underscore + if (c == '-' || c == ' ') { + if (output_len > 0 && !prev_was_separator) { + output[output_len++] = '_'; + } + prev_was_separator = 1; + continue; + } + + // Handle uppercase letters + if (is_uppercase(c)) { + // Add underscore before uppercase letter (except at start) + if (output_len > 0 && !prev_was_separator) { + // Check if previous character was lowercase (camelCase) + if (i > 0 && islower(input[i-1])) { + output[output_len++] = '_'; + } + } + output[output_len++] = tolower(c); + prev_was_separator = 0; + } else if (isalnum(c)) { + // Just copy lowercase letters and numbers + output[output_len++] = c; + prev_was_separator = 0; + } else { + // Skip other special characters + if (output_len > 0 && !prev_was_separator) { + prev_was_separator = 1; + } + } } - // Compute using iterative approach to avoid overflow - long result = 1; - for (long i = 1; i <= k; i++) { - result = result * (n - k + i) / i; + // Remove trailing underscore + while (output_len > 0 && output[output_len - 1] == '_') { + output_len--; } - return result; + output[output_len] = '\0'; + + return output; } diff --git a/tests/c/support/test_helpers.h b/tests/c/support/test_helpers.h index ae60172..c16061e 100644 --- a/tests/c/support/test_helpers.h +++ b/tests/c/support/test_helpers.h @@ -1,7 +1,7 @@ #ifndef TEST_HELPERS_H #define TEST_HELPERS_H -// Pascal's triangle calculation function for testing -long pascal_triangle_calc(long n, long k); +// String to snake_case conversion function for testing +char* to_snake_calc(const char* input); #endif /* TEST_HELPERS_H */ diff --git a/tests/c/test_pascal.c b/tests/c/test_pascal.c deleted file mode 100644 index 3200952..0000000 --- a/tests/c/test_pascal.c +++ /dev/null @@ -1,121 +0,0 @@ -#include "unity.h" -#include "test_helpers.h" - -// Test cases for Pascal's triangle binomial coefficient function - -// ============= Basic Cases ============= - -TEST_CASE(0, 0, 1) -TEST_CASE(1, 0, 1) -TEST_CASE(1, 1, 1) -TEST_CASE(2, 0, 1) -TEST_CASE(2, 1, 2) -TEST_CASE(2, 2, 1) -TEST_CASE(3, 0, 1) -TEST_CASE(3, 1, 3) -TEST_CASE(3, 2, 3) -TEST_CASE(3, 3, 1) -TEST_CASE(4, 0, 1) -TEST_CASE(4, 1, 4) -TEST_CASE(4, 2, 6) -TEST_CASE(4, 3, 4) -TEST_CASE(4, 4, 1) -TEST_CASE(5, 0, 1) -TEST_CASE(5, 1, 5) -TEST_CASE(5, 2, 10) -TEST_CASE(5, 3, 10) -TEST_CASE(5, 4, 5) -TEST_CASE(5, 5, 1) -void test_pascal_triangle_basic(long n, long k, long expected) { - long result = pascal_triangle_calc(n, k); - TEST_ASSERT_EQUAL(expected, result); -} - -// ============= Symmetry Test ============= - -void test_pascal_symmetry(void) { - // C(n, k) = C(n, n-k) - for (int n = 0; n <= 10; n++) { - for (int k = 0; k <= n / 2; k++) { - long c_nk = pascal_triangle_calc(n, k); - long c_nnk = pascal_triangle_calc(n, n - k); - char message[100]; - snprintf(message, sizeof(message), "C(%d,%d) != C(%d,%d)", n, k, n, n - k); - TEST_ASSERT_EQUAL_MESSAGE(message, c_nnk, c_nk); - } - } -} - -// ============= Row Sum Test ============= - -void test_pascal_row_sum(void) { - // Sum of row n should be 2^n - for (int n = 0; n <= 10; n++) { - long sum = 0; - for (int k = 0; k <= n; k++) { - sum += pascal_triangle_calc(n, k); - } - long expected = 1L << n; // 2^n - char message[100]; - snprintf(message, sizeof(message), "Row %d sum should be %ld, got %ld", n, expected, sum); - TEST_ASSERT_EQUAL_MESSAGE(message, expected, sum); - } -} - -// ============= Edge Cases ============= - -void test_pascal_zero_n(void) { - // C(0, 0) = 1 - TEST_ASSERT_EQUAL(1, pascal_triangle_calc(0, 0)); -} - -void test_pascal_invalid_k_greater_than_n(void) { - // Should return 0 when k > n - TEST_ASSERT_EQUAL(0, pascal_triangle_calc(5, 6)); - TEST_ASSERT_EQUAL(0, pascal_triangle_calc(10, 15)); -} - -void test_pascal_negative_parameters(void) { - // Should return 0 for negative parameters - TEST_ASSERT_EQUAL(0, pascal_triangle_calc(-1, 0)); - TEST_ASSERT_EQUAL(0, pascal_triangle_calc(5, -1)); - TEST_ASSERT_EQUAL(0, pascal_triangle_calc(-1, -1)); -} - -// ============= Large Values ============= - -TEST_CASE(10, 5, 252) -TEST_CASE(12, 6, 924) -TEST_CASE(15, 5, 3003) -TEST_CASE(20, 10, 184756) -void test_pascal_large_values(long n, long k, long expected) { - long result = pascal_triangle_calc(n, k); - TEST_ASSERT_EQUAL(expected, result); -} - -// ============= Test Matrix for Multiple Values ============= - -// Test that C(n, 0) = 1 for any n -TEST_MATRIX([0, 1, 2, 3, 4, 5, 10, 15, 20]) -void test_pascal_first_column(long n) { - TEST_ASSERT_EQUAL(1, pascal_triangle_calc(n, 0)); -} - -// Test that C(n, n) = 1 for any n -TEST_MATRIX([0, 1, 2, 3, 4, 5, 10, 15, 20]) -void test_pascal_last_column(long n) { - TEST_ASSERT_EQUAL(1, pascal_triangle_calc(n, n)); -} - -// Test that C(n, 1) = n for any n >= 1 -TEST_MATRIX([1, 2, 3, 4, 5, 10, 15, 20]) -void test_pascal_second_column(long n) { - TEST_ASSERT_EQUAL(n, pascal_triangle_calc(n, 1)); -} - -// Test that C(n, 2) = n*(n-1)/2 for any n >= 2 -TEST_MATRIX([2, 3, 4, 5, 10]) -void test_pascal_third_column(long n) { - long expected = n * (n - 1) / 2; - TEST_ASSERT_EQUAL(expected, pascal_triangle_calc(n, 2)); -} diff --git a/tests/c/test_to_snake.c b/tests/c/test_to_snake.c new file mode 100644 index 0000000..572f10a --- /dev/null +++ b/tests/c/test_to_snake.c @@ -0,0 +1,155 @@ +#include "unity.h" +#include "test_helpers.h" + +// ============= Basic Cases ============= + +TEST_CASE("FirstName", "first_name") +TEST_CASE("firstName", "first_name") +TEST_CASE("first_name", "first_name") +TEST_CASE("first-name", "first_name") +TEST_CASE("first name", "first_name") +TEST_CASE("Name", "name") +TEST_CASE("HTTPResponse", "http_response") +TEST_CASE("user2Name", "user2_name") +TEST_CASE("getHTTPResponseCode", "get_http_response_code") +TEST_CASE("IOError", "io_error") +void test_to_snake_case_basic(const char* input, const char* expected) { + char* result = to_snake_calc(input); + TEST_ASSERT_EQUAL_STRING(expected, result); + efree(result); +} + +// ============= Edge Cases ============= + +void test_to_snake_empty_string(void) { + char* result = to_snake_calc(""); + TEST_ASSERT_EQUAL_STRING("", result); + efree(result); +} + +void test_to_snake_single_character(void) { + char* result = to_snake_calc("a"); + TEST_ASSERT_EQUAL_STRING("a", result); + efree(result); + + result = to_snake_calc("A"); + TEST_ASSERT_EQUAL_STRING("a", result); + efree(result); +} + +void test_to_snake_all_uppercase(void) { + char* result = to_snake_calc("HELLO"); + TEST_ASSERT_EQUAL_STRING("hello", result); + efree(result); +} + +void test_to_snake_all_lowercase(void) { + char* result = to_snake_calc("hello"); + TEST_ASSERT_EQUAL_STRING("hello", result); + efree(result); +} + +void test_to_snake_numbers(void) { + char* result = to_snake_calc("user2Name"); + TEST_ASSERT_EQUAL_STRING("user2_name", result); + efree(result); + + result = to_snake_calc("123ABC"); + TEST_ASSERT_EQUAL_STRING("123abc", result); + efree(result); +} + +void test_to_snake_multiple_separators(void) { + char* result = to_snake_calc("first name"); + TEST_ASSERT_EQUAL_STRING("first_name", result); + efree(result); + + result = to_snake_calc("first--name"); + TEST_ASSERT_EQUAL_STRING("first_name", result); + efree(result); + + result = to_snake_calc("first - name"); + TEST_ASSERT_EQUAL_STRING("first_name", result); + efree(result); +} + +void test_to_snake_leading_trailing_separators(void) { + char* result = to_snake_calc(" first_name "); + TEST_ASSERT_EQUAL_STRING("first_name", result); + efree(result); + + result = to_snake_calc("-first_name-"); + TEST_ASSERT_EQUAL_STRING("first_name", result); + efree(result); +} + +// ============= Special Characters ============= + +void test_to_snake_special_chars(void) { + // Special characters should be removed + char* result = to_snake_calc("first@name"); + TEST_ASSERT_EQUAL_STRING("first_name", result); + efree(result); + + result = to_snake_calc("first.name"); + TEST_ASSERT_EQUAL_STRING("firstname", result); + efree(result); +} + +// ============= Consecutive Uppercase ============= + +void test_to_snake_consecutive_uppercase(void) { + char* result = to_snake_calc("HTTPResponse"); + TEST_ASSERT_EQUAL_STRING("http_response", result); + efree(result); + + result = to_snake_calc("XMLParser"); + TEST_ASSERT_EQUAL_STRING("xml_parser", result); + efree(result); + + result = to_snake_calc("HTMLElement"); + TEST_ASSERT_EQUAL_STRING("html_element", result); + efree(result); +} + +// ============= Test Matrix for Multiple Values ============= + +// Test that single words remain unchanged +TEST_MATRIX(["hello", "world", "test", "name"]) +void test_to_snake_single_word(const char* input) { + char* result = to_snake_calc(input); + TEST_ASSERT_EQUAL_STRING(input, result); + efree(result); +} + +// Test camelCase to snake_case +TEST_MATRIX(["camelCase", "PascalCase", "longVariableName"]) +void test_to_snake_camel_case(const char* input) { + char* result = to_snake_calc(input); + // camelCase -> camel_case, PascalCase -> pascal_case, longVariableName -> long_variable_name + char* expected = emalloc(strlen(input) * 2 + 1); + size_t j = 0; + for (size_t i = 0; input[i]; i++) { + if (isupper(input[i]) && i > 0 && islower(input[i-1])) { + expected[j++] = '_'; + } + expected[j++] = tolower(input[i]); + } + expected[j] = '\0'; + TEST_ASSERT_EQUAL_STRING(expected, result); + efree(expected); + efree(result); +} + +// Test that all uppercase single words become lowercase +TEST_MATRIX(["HELLO", "WORLD", "TEST", "NAME"]) +void test_to_snake_all_uppercase_single_word(const char* input) { + char* result = to_snake_calc(input); + char expected[100]; + for (size_t i = 0; input[i]; i++) { + expected[i] = tolower(input[i]); + } + expected[strlen(input)] = '\0'; + TEST_ASSERT_EQUAL_STRING(expected, result); + efree(result); +} diff --git a/tests/php/Integration/HelloWorldTest.php b/tests/php/Integration/HelloWorldTest.php deleted file mode 100644 index c145d52..0000000 --- a/tests/php/Integration/HelloWorldTest.php +++ /dev/null @@ -1,34 +0,0 @@ -toBeTrue(); -}); - -it('pascal_triangle computes binomial coefficients', function () { - expect(pascal_triangle(0, 0))->toBe(1); - expect(pascal_triangle(1, 0))->toBe(1); - expect(pascal_triangle(1, 1))->toBe(1); - expect(pascal_triangle(2, 1))->toBe(2); - expect(pascal_triangle(3, 1))->toBe(3); - expect(pascal_triangle(4, 2))->toBe(6); - expect(pascal_triangle(5, 2))->toBe(10); - expect(pascal_triangle(5, 3))->toBe(10); -}); - -it('pascal_triangle returns 0 for invalid inputs', function () { - expect(pascal_triangle(-1, 0))->toBe(0); - expect(pascal_triangle(5, -1))->toBe(0); - expect(pascal_triangle(5, 6))->toBe(0); -}); - -it('pascal_triangle demonstrates symmetry', function () { - // C(n, k) = C(n, n-k) - expect(pascal_triangle(10, 3))->toBe(pascal_triangle(10, 7)); - expect(pascal_triangle(8, 2))->toBe(pascal_triangle(8, 6)); - expect(pascal_triangle(15, 5))->toBe(pascal_triangle(15, 10)); -}); diff --git a/tests/php/Integration/ToSnakeTest.php b/tests/php/Integration/ToSnakeTest.php new file mode 100644 index 0000000..14f3a1e --- /dev/null +++ b/tests/php/Integration/ToSnakeTest.php @@ -0,0 +1,72 @@ +toBe('camel_case'); + expect(to_snake('PascalCase'))->toBe('pascal_case'); + expect(to_snake('longVariableName'))->toBe('long_variable_name'); +}); + +it('converts PascalCase to snake_case', function () { + expect(to_snake('FirstName'))->toBe('first_name'); + expect(to_snake('HTTPResponse'))->toBe('http_response'); + expect(to_snake('XMLParser'))->toBe('xml_parser'); +}); + +it('converts kebab-case to snake_case', function () { + expect(to_snake('first-name'))->toBe('first_name'); + expect(to_snake('long-variable-name'))->toBe('long_variable_name'); +}); + +it('converts space separated to snake_case', function () { + expect(to_snake('first name'))->toBe('first_name'); + expect(to_snake('long variable name'))->toBe('long_variable_name'); +}); + +it('handles already snake_case strings', function () { + expect(to_snake('first_name'))->toBe('first_name'); + expect(to_snake('long_variable_name'))->toBe('long_variable_name'); +}); + +it('handles empty string', function () { + expect(to_snake(''))->toBe(''); +}); + +it('handles single character', function () { + expect(to_snake('a'))->toBe('a'); + expect(to_snake('A'))->toBe('a'); +}); + +it('handles all uppercase', function () { + expect(to_snake('HELLO'))->toBe('hello'); + expect(to_snake('WORLD'))->toBe('world'); +}); + +it('handles all lowercase', function () { + expect(to_snake('hello'))->toBe('hello'); + expect(to_snake('world'))->toBe('world'); +}); + +it('handles numbers', function () { + expect(to_snake('user2Name'))->toBe('user2_name'); + expect(to_snake('123ABC'))->toBe('123abc'); +}); + +it('handles multiple separators', function () { + expect(to_snake('first name'))->toBe('first_name'); + expect(to_snake('first--name'))->toBe('first_name'); +}); + +it('handles leading and trailing separators', function () { + expect(to_snake(' first_name '))->toBe('first_name'); + expect(to_snake('-first_name-'))->toBe('first_name'); +}); + +it('handles consecutive uppercase letters', function () { + expect(to_snake('HTTPResponse'))->toBe('http_response'); + expect(to_snake('XMLParser'))->toBe('xml_parser'); + expect(to_snake('IOError'))->toBe('io_error'); +});