Skip to content

Commit 32a18d2

Browse files
committed
feat(cpp): add valid palindrome
1 parent 776aa6d commit 32a18d2

File tree

5 files changed

+262
-6
lines changed

5 files changed

+262
-6
lines changed

.github/workflows/cpp-package.yml

Lines changed: 125 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
name: Cpp Format & Test
1+
---
2+
name: Presubmit (C++)
23

3-
on: [push]
4+
on: # yamllint disable-line rule:truthy
5+
pull_request:
6+
branches: ["main"]
47

58
jobs:
69
build-cpp:
710
runs-on: ubuntu-latest
811

912
steps:
1013
- uses: actions/checkout@v4
14+
with:
15+
fetch-depth: 0 # Fetch full history for git diff
1116

1217
- name: Install dependencies
1318
run: |
1419
sudo apt-get update
15-
sudo apt-get install -y cmake build-essential
20+
sudo apt-get install -y cmake build-essential clang-format clang-tidy
1621
# Install Google Test from source - most reliable approach
1722
git clone https://github.com/google/googletest.git --depth 1
1823
cd googletest
@@ -25,14 +30,129 @@ jobs:
2530
- name: Debug Google Test configuration
2631
run: make debug-gtest
2732

33+
- name: Detect changed C++ files
34+
id: detect-changes
35+
run: |
36+
# Get list of changed files
37+
if [ "${{ github.event_name }}" = "pull_request" ]; then
38+
# For PR, compare with base branch
39+
CHANGED_FILES=$(git diff --name-only \
40+
${{ github.event.pull_request.base.sha }}..HEAD)
41+
else
42+
# For push, compare with previous commit
43+
if [ "${{ github.event.before }}" = \
44+
"0000000000000000000000000000000000000000" ]; then
45+
# First commit in repo
46+
CHANGED_FILES=$(git diff --name-only --diff-filter=A HEAD)
47+
else
48+
CHANGED_FILES=$(git diff --name-only \
49+
${{ github.event.before }}..HEAD)
50+
fi
51+
fi
52+
53+
echo "All changed files:"
54+
echo "$CHANGED_FILES"
55+
56+
# Filter for C++ files only
57+
CHANGED_CPP_FILES=$(echo "$CHANGED_FILES" | \
58+
grep -E '\.(cc|cpp|cxx|c\+\+|h|hpp|hxx|h\+\+)$' || true)
59+
60+
# Extract unique problem directories from changed C++ files
61+
CHANGED_PROBLEMS=$(echo "$CHANGED_CPP_FILES" | \
62+
grep -E '^problems/[^/]+/' | cut -d'/' -f2 | \
63+
sort -u | tr '\n' ' ' || true)
64+
65+
# Check if Makefile or C++-related config files changed
66+
CPP_CONFIG_CHANGES=$(echo "$CHANGED_FILES" | \
67+
grep -E '(Makefile|CMakeLists\.txt|\.clang-format|\.clang-tidy) \
68+
$' || true)
69+
70+
# Check if common directory changed (affects all problems)
71+
COMMON_CHANGES=$(echo "$CHANGED_FILES" | \
72+
grep -E '^common/' || true)
73+
74+
echo "Changed C++ files:"
75+
echo "$CHANGED_CPP_FILES"
76+
echo "Changed problems: $CHANGED_PROBLEMS"
77+
echo "C++ config changes: $CPP_CONFIG_CHANGES"
78+
echo "Common directory changes: $COMMON_CHANGES"
79+
80+
# Set outputs
81+
echo "changed_problems=$CHANGED_PROBLEMS" >> $GITHUB_OUTPUT
82+
echo "has_cpp_changes=$([ -n "$CHANGED_CPP_FILES" ] && \
83+
echo 'true' || echo 'false')" >> $GITHUB_OUTPUT
84+
echo "has_cpp_config_changes=$([ -n "$CPP_CONFIG_CHANGES" ] && \
85+
echo 'true' || echo 'false')" >> $GITHUB_OUTPUT
86+
echo "has_common_changes=$([ -n "$COMMON_CHANGES" ] && \
87+
echo 'true' || echo 'false')" >> $GITHUB_OUTPUT
88+
echo "should_run_all_tests=$([ -n "$CPP_CONFIG_CHANGES" ] || \
89+
[ -n "$COMMON_CHANGES" ] && echo 'true' || echo 'false')" >> \
90+
$GITHUB_OUTPUT
91+
2892
- name: Format C++ code
93+
if: steps.detect-changes.outputs.has_cpp_changes == 'true'
2994
run: make format-cpp
3095

3196
- name: Lint C++ code
97+
if: steps.detect-changes.outputs.has_cpp_changes == 'true'
3298
run: make lint-cpp
3399

34-
- name: Run C++ tests
35-
run: make test-cpp:all
100+
- name: Run C++ tests for changed problems
101+
if: >
102+
steps.detect-changes.outputs.changed_problems != '' &&
103+
steps.detect-changes.outputs.should_run_all_tests == 'false'
104+
run: |
105+
PROBLEMS="${{ steps.detect-changes.outputs.changed_problems }}"
106+
echo "Running tests for changed problems: $PROBLEMS"
107+
108+
for problem in $PROBLEMS; do
109+
echo "Testing problem: $problem"
110+
# Convert snake_case to kebab-case for make target
111+
kebab_case=$(echo "$problem" | sed 's/_/-/g')
112+
make test-cpp:$kebab_case || exit 1
113+
done
114+
115+
- name: Run all C++ tests (dependencies changed)
116+
if: steps.detect-changes.outputs.should_run_all_tests == 'true'
117+
run: |
118+
echo "Running all tests due to C++ configuration or common changes"
119+
make test-cpp:all
36120
37121
- name: Clean
122+
if: always()
38123
run: make clean
124+
125+
- name: Summary
126+
if: always()
127+
run: |
128+
echo "## Test Summary" >> $GITHUB_STEP_SUMMARY
129+
130+
if [ "${{ steps.detect-changes.outputs.has_cpp_changes }}" = \
131+
"false" ] && \
132+
[ "${{ steps.detect-changes.outputs.has_cpp_config_changes }}" = \
133+
"false" ] && \
134+
[ "${{ steps.detect-changes.outputs.has_common_changes }}" = \
135+
"false" ]; then
136+
echo "- No C++ files changed - skipped all tasks" >> \
137+
$GITHUB_STEP_SUMMARY
138+
elif [ "${{ steps.detect-changes.outputs.should_run_all_tests }}" = \
139+
"true" ]; then
140+
if [ "${{ steps.detect-changes.outputs.has_cpp_config_changes }}" \
141+
= "true" ]; then
142+
echo "- Ran all tests due to config changes" >> \
143+
$GITHUB_STEP_SUMMARY
144+
fi
145+
if [ "${{ steps.detect-changes.outputs.has_common_changes }}" = \
146+
"true" ]; then
147+
echo "- Ran all tests due to common directory changes" >> \
148+
$GITHUB_STEP_SUMMARY
149+
fi
150+
elif [ "${{ steps.detect-changes.outputs.changed_problems }}" != \
151+
"" ]; then
152+
problems="${{ steps.detect-changes.outputs.changed_problems }}"
153+
echo "- Ran targeted tests for: $problems" >> \
154+
$GITHUB_STEP_SUMMARY
155+
else
156+
echo "- No tests needed (no problem files changed)" >> \
157+
$GITHUB_STEP_SUMMARY
158+
fi

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ test-cpp\:%:
121121
test_result=$$?; \
122122
rm -f $(PROBLEMS_DIR)/$$snake_case_name/test_runner; \
123123
cd - > /dev/null; \
124+
make clean SILENT=true; \
124125
exit $$test_result; \
125126
else \
126127
echo "$(call color_red,Failed to compile tests for $*)"; \
@@ -141,7 +142,7 @@ test-py\:%:
141142
for dir in $(PROBLEM_DIRS); do \
142143
if [ -f $$dir/*_test.py ]; then \
143144
echo "$(call color_blue,Testing Python in $$dir...)"; \
144-
cd $$dir && $(PYTEST) *_test.py --color=yes -v; \
145+
cd $$dir && $(PYTEST) *_test.py --color=yes; \
145146
test_result=$$?; \
146147
if [ $$test_result -ne 0 ]; then \
147148
overall_result=$$test_result; \
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#include "valid_palindrome.h"
2+
3+
using std::string;
4+
5+
bool isPalindrome(string s) {
6+
int left = 0;
7+
int right = static_cast<int>(s.size()) - 1;
8+
9+
// Transform the string (in-place) to lowercase
10+
std::transform(s.begin(), s.end(), s.begin(),
11+
[](unsigned char c) { return std::tolower(c); });
12+
13+
while (left < right) {
14+
// Move left pointer to the next alphanumeric character
15+
while (left < right && !std::isalnum(s[left])) {
16+
++left;
17+
}
18+
19+
// Move right pointer to the previous alphanumeric character
20+
while (left < right && !std::isalnum(s[right])) {
21+
--right;
22+
}
23+
24+
// Compare characters at left and right pointers
25+
if (s[left] != s[right]) {
26+
return false; // Not a palindrome
27+
}
28+
29+
++left;
30+
--right;
31+
}
32+
return true; // Is a palindrome
33+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
#include <string>
2+
3+
bool isPalindrome(std::string s);
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#include "valid_palindrome.h"
2+
3+
#include <gtest/gtest.h>
4+
#include <string>
5+
6+
using std::string;
7+
8+
struct ValidPalindromeCase {
9+
const std::string test_name;
10+
const string s;
11+
const bool expected;
12+
};
13+
14+
using ValidPalindromeTest = ::testing::TestWithParam<ValidPalindromeCase>;
15+
16+
TEST_P(ValidPalindromeTest, TestCases) {
17+
const ValidPalindromeCase &test_case = GetParam();
18+
const bool result = isPalindrome(test_case.s);
19+
EXPECT_EQ(result, test_case.expected);
20+
}
21+
22+
INSTANTIATE_TEST_SUITE_P(
23+
ValidPalindromeTestCases, ValidPalindromeTest,
24+
::testing::Values(
25+
ValidPalindromeCase{.test_name = "ClassicPanama",
26+
.s = "A man, a plan, a canal: Panama",
27+
.expected = true},
28+
ValidPalindromeCase{
29+
.test_name = "RaceCarFalse", .s = "race a car", .expected = false},
30+
ValidPalindromeCase{
31+
.test_name = "EmptyString", .s = "", .expected = true},
32+
ValidPalindromeCase{
33+
.test_name = "SingleChar", .s = "a", .expected = true},
34+
ValidPalindromeCase{
35+
.test_name = "CaseInsensitive", .s = "Aa", .expected = true},
36+
ValidPalindromeCase{
37+
.test_name = "TwoCharFalse", .s = "ab", .expected = false},
38+
ValidPalindromeCase{
39+
.test_name = "TwoCharTrue", .s = "aa", .expected = true},
40+
ValidPalindromeCase{
41+
.test_name = "SimplePalindrome", .s = "racecar", .expected = true},
42+
ValidPalindromeCase{
43+
.test_name = "SimpleFalse", .s = "hello", .expected = false},
44+
ValidPalindromeCase{
45+
.test_name = "SantaNasa", .s = "A Santa at NASA", .expected = true},
46+
ValidPalindromeCase{.test_name = "CarCatQuestion",
47+
.s = "Was it a car or a cat I saw?",
48+
.expected = true},
49+
ValidPalindromeCase{.test_name = "NixonApostrophe",
50+
.s = "No 'x' in Nixon",
51+
.expected = true},
52+
ValidPalindromeCase{
53+
.test_name = "MadamAdam", .s = "Madam, I'm Adam", .expected = true},
54+
ValidPalindromeCase{.test_name = "NeverOddEven",
55+
.s = "never odd or even",
56+
.expected = true},
57+
ValidPalindromeCase{
58+
.test_name = "NopeFalse", .s = "nope", .expected = false},
59+
ValidPalindromeCase{
60+
.test_name = "MixedAlphanumFalse", .s = "0P", .expected = false},
61+
ValidPalindromeCase{.test_name = "PanamaExclamation",
62+
.s = "A man, a plan, a canal: Panama!",
63+
.expected = true},
64+
ValidPalindromeCase{.test_name = "RaceEcarHyphen",
65+
.s = "race a E-car",
66+
.expected = true},
67+
ValidPalindromeCase{.test_name = "AbleElba",
68+
.s = "Able was I ere I saw Elba",
69+
.expected = true},
70+
ValidPalindromeCase{
71+
.test_name = "NumericPalindrome", .s = "12321", .expected = true},
72+
ValidPalindromeCase{
73+
.test_name = "NumericFalse", .s = "12345", .expected = false},
74+
ValidPalindromeCase{.test_name = "AlphanumPalindrome",
75+
.s = "a1b2c3c2b1a",
76+
.expected = true},
77+
ValidPalindromeCase{.test_name = "AlphanumFalse",
78+
.s = "1a2b3c3c2b1a",
79+
.expected = false},
80+
ValidPalindromeCase{.test_name = "OnlySpecialChars",
81+
.s = ".,!@#$%^&*()",
82+
.expected = true},
83+
ValidPalindromeCase{.test_name = "SpecialCharsMiddle",
84+
.s = "a.,!@#$%^&*()a",
85+
.expected = true},
86+
ValidPalindromeCase{.test_name = "DammitMad",
87+
.s = "Dammit, I'm mad!",
88+
.expected = true},
89+
ValidPalindromeCase{
90+
.test_name = "StepPets", .s = "Step on no pets", .expected = true},
91+
ValidPalindromeCase{.test_name = "RatQuestion",
92+
.s = "Was it a rat I saw?",
93+
.expected = true},
94+
ValidPalindromeCase{.test_name = "OwlWorm",
95+
.s = "Mr. Owl ate my metal worm",
96+
.expected = true}),
97+
[](const ::testing::TestParamInfo<ValidPalindromeCase> &info) {
98+
return info.param.test_name;
99+
});

0 commit comments

Comments
 (0)