🕯️ Debugging Grimoire: The Tome of Cursed Knowledge
This is my personal collection of hard-earned debugging wisdom, gathered from countless nights battling rogue services, broken deployments, and cursed error messages. The goal is simple: never struggle with the same problem twice.
Rather than leaving my notes scattered across random bookmarks, Discord servers, and Google Docs, this grimoire serves as a single source of truth for all things database, Linux, and DevOps. More sections might be added over time—especially for programming languages and specific frameworks.
If you're reading this, you're either past-me, future-me, or an unfortunate soul who has stumbled upon my secret knowledge. Either way, remember:
-
🛠️ Debugging is an art, not a science.
-
💀 Nothing works until it does.
-
✅ The green checkmark is the ultimate goal.
Welcome to the Grimoire of Debugging Madness. May it save me (and possibly others) from unnecessary suffering.
Setting Up SSH for GitHub
This will make GitHub stop asking you for your username and access token every time.
Step 1: Check if you already have a key
- Before generating a new one, check if you already have an SSH key:
ls -al ~/.ssh
If you see files like id_rsa
and id_rsa.pub
(or id_ed25519
and id_ed25519.pub
), you probably already have an SSH key. If not, generate one.
Step 2: Generate a New SSH key (if needed)
If you don't have an existing SSH key, generate a new one:
ssh-keygen -t ed25519 -C "yourmail@example.com"
- When it asks for a file location, just press Enter (this will save it in
~/.ssh/id_ed25519
). - When it asks for a passphrase, you can leave it empty (or set one for extra security).
Step 3: Add Your SSH Key to the SSH Agent
- Now, you need to add the key to your local SSH agent so it gets used automatically:
eval "$(ssh-agent -s)"
- Then add your key:
ssh-add ~/.ssh/id_ed25519
(If you used rsa
, replace id_ed25519
with id_rsa
.)
Step 4: Copy Your SSH Key to GitHub
Now, you need to add your SSH key to your GitHub account.
- Copy the key to your clipboard:
cat ~/.ssh/id_ed25519.pub
It will output something like:
ssh-ed25519 AAAAC3Nza...yourlongpublickeyhere yourmail@example.com
- Go to GitHub → SSH Keys Settings
- Click "New SSH Key", paste your key, and give it a name.
- Save it.
Step 5: Test the Connection
- Check if GitHub recognizes your SSH key:
ssh -T git@github.com
If everything is set up correctly, you should see:
Hi <your-github-username>! You've successfully authenticated, but GitHub does not provide shell access.
Step 6: Change Your Git Remote to Use SSH
- If your Git remote is still using HTTPS (which asks for a password), switch it to SSH:
git remote -v
If you see:
origin https://github.com/your-username/repository.git (fetch)
origin https://github.com/your-username/repository.git (push)
- Change it to SSH:
git remote set-url origin git@github.com:your-username/repository.git
Now, every push/pull will use SSH, and you’ll never have to enter your password again.
The Skinny Ruby Queen: Minimal Dockerfile for Production
1. Build Stage
- We're building in style. Ruby + Alpine = skinny legend
FROM ruby:3.3-alpine AS build
- Install a full dev toolchain to compile native gems (yes, Ruby still lives in C land)
RUN apk add --no-cache build-base
- Set the working directory—aka the sacred ground where it all happens
WORKDIR /usr/src/app
- Copy only Gemfile and lockfile first (layer caching magic)
COPY Gemfile Gemfile.lock ./
- Configure bundler to install gems locally under vendor/bundle. This will be copied over to the final image later like a blessed artifact
RUN bundle config set --local path 'vendor/bundle' \
&& bundle install
- Copy the rest of your application—code, chaos, and all
COPY . .
2. Final Stage
- A clean Alpine base with Ruby and none of that build baggage. We like our containers light.
FROM ruby:3.3-alpine
- Set the working dir again (yes, you need to re-declare it—Docker has no memory of its past life)
WORKDIR /usr/src/app
- Copy everything from the build stage, including those precious compiled gems
COPY --from=build /usr/src/app /usr/src/app
- Let Ruby know where the gems are—because it forgets if you don’t tell it
ENV GEM_PATH=/usr/src/app/vendor/bundle/ruby/3.3.0
ENV PATH=$GEM_PATH/bin:$PATH
- Install only the runtime dependencies needed for your app to vibe
RUN apk add --no-cache \
libstdc++ \ # C++ runtime
libffi \ # Needed by some gems (e.g., FFI, psych)
yaml \ # YAML parsing
zlib \ # Compression stuff
openssl \ # HTTPS, TLS, etc.
tzdata # So your logs don’t think it's 1970
- Declare yourself: prod mode on
ENV RACK_ENV=production
ENV PORT=8080
EXPOSE 8080
- Finally, launch the Ruby app like the main character it is
CMD ["ruby", "server.rb"]
Some useful commands:
docker build -t your-image-name .
docker images
docker run -p 8080:8080 your-image-id
docker rmi your-image-id
docker container prune
Pro Tips from the Underworld: If you're using gems that compile C extensions (like pg, nokogiri, ffi), you’ll likely need additional Alpine dependencies, e.g.:
RUN apk add --no-cache build-base libxml2-dev libxslt-dev postgresql-dev
For scripts that are long-running, consider using:
CMD ["ruby", "start.rb"]
Or even:
CMD ["rackup", "--host", "0.0.0.0", "--port", "8080"]
Deploying mdBook to GitHub Pages With GitHub Actions
Step 1: Setup the Repo
-
Create a new GitHub repo.
-
Run:
cargo install mdbook
mdbook init my-docs
cd my-docs
Step 2: Add GitHub Actions Workflow
- Create .github/workflows/deploy.yml:
name: Deploy mdBook to GitHub Pages
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Install mdBook
run: cargo install mdbook
- name: Build the book
run: mdbook build
- name: Setup SSH Authentication
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan github.com >> ~/.ssh/known_hosts
- name: Deploy to GitHub Pages
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book
SSH_PRIVATE_KEY
-> (id_ed25519
)
GITHUB_TOKEN
-> GitHub adds this automatically
Step 3: Add Secrets
- Generate a separate SSH key for CI/CD
ssh-keygen -t id_ed25519 -C "GitHub Actions Deploy Key"
-
Go to Repo -> Settings -> Secrets and Variables -> Actions
-
Add
SSH_PRIVATE_KEY
-> Paste the private key (id_ed25519
) -
Go to Repo -> Settings -> Deploy key
-
Paste the public key (
id_ed25519.pub
)
Step 4: Enable Permissions
- Go to Repo -> Settings -> Actions -> General
- Under Workflow Permissions, enable: ✅ Read and Write Permissions ✅ Allow GitHub Actions to create and approve pull requests
Step 5: Push and Deploy
git add .
git commit -m "Deploy Book"
git push origin main
If it all goes well, your docs should be live.
CI/CD Pipeline Setup for Cloud Run
Deploy your projects automatically with a simple git commit and git push. To do this, you need to Install the gcloud CLI
Step 1: Test Locally with Docker
Build the image and test before pushing anything to Google Cloud.
docker build -t my-portfolio .
docker run -p 8080:8080 my-portfolio
- Fix any port, environment, or dependency issues locally first.
- Once it works locally, move on to Google Cloud.
Step 2: Set Up Google Cloud
-
Before running these commands, be sure to:
- Check current GCP project:
gcloud config list project
- Set active project
gcloud config set project YOUR_PROJECT_ID
- You can also view all projects your account can access:
gcloud projects list
-
Enable the required APIs (run these in your terminal):
gcloud services enable \
cloudbuild.googleapis.com \
run.googleapis.com \
artifactregistry.googleapis.com
This ensures Google Cloud has all necessary services activated.
- Create an Artifact Registry repo for Docker images:
gcloud artifacts repositories create portfolio-repo \
--repository-format=docker \
--location=europe-west1 \
--description="Docker repository for portfolio deployment"
This stores your container images so Cloud Run can pull them.
Step 3: Create a Service Account for GitHub Actions
- Create a user for CI/CD:
gcloud iam service-accounts create github-deployer \
--description="GitHub Actions service account" \
--display-name="GitHub Deployer"
This creates a dedicated user for deploying the app.
- Grant it permissions:
gcloud projects add-iam-policy-binding $YOUR_PROJECT_ID \
--member=serviceAccount:github-deployer@$YOUR_PROJECT_ID.iam.gserviceaccount.com \
--role=roles/run.admin
gcloud projects add-iam-policy-binding $YOUR_PROJECT_ID \
--member=serviceAccount:github-deployer@$YOUR_PROJECT_ID.iam.gserviceaccount.com \
--role=roles/artifactregistry.writer
gcloud projects add-iam-policy-binding $YOUR_PROJECT_ID \
--member=serviceAccount:github-deployer@$YOUR_PROJECT_ID.iam.gserviceaccount.com \
--role=roles/storage.admin
GitHub Actions can now push images & deploy to Cloud Run.
- Generate a key file for the service account:
gcloud iam service-accounts keys create key.json \
--iam-account=github-deployer@$YOUR_PROJECT_ID.iam.gserviceaccount.com
This creates key.json, which contains the credentials.
Add Secrets to GitHub
-
Go to your GitHub repo -> Settings -> Secrets and Variables -> Actions
-
Add two secrets in Secrets -> repository secrets:
1.GCP_SERVICE_ACCOUNT_KEY → Copy & paste the full contents of key.json.
2.GCP_PROJECT_ID → Your Google Cloud project ID.
Now, GitHub Actions can authenticate with Google Cloud
Step 5: Create GitHub Actions Workflows (deploy.yml
)
- In your repo, create: .github/workflows/deploy.yml
name: Deploy to Cloud Run
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Authenticate with Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SERVICE_ACCOUNT_KEY }}
- name: Set Up Google Cloud SDK
run: |
gcloud auth configure-docker europe-west2-docker.pkg.dev
- name: Build and push Docker Image
run: |
docker build -t europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/portfolio-repo/portfolio .
docker push europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/portfolio-repo/portfolio
- name: Deploy to Cloud Run
run: |
gcloud run deploy portfolio-site \
--image europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT_ID }}/portfolio-repo/portfolio \
--platform managed \
--region europe-west1 \
--allow-unauthenticated
Now, every push to main
will automatically deploy to Cloud Run.
Step 6: Push & Deploy
- Once everything is set up:
git add .
git commit -m "Setup GitHub Actions CI/CD"
git push origin main
Check GitHub Actions -> It should build & deploy your project automatically.
Adding a Domain to Google Cloud Run
I have unfortunately decided to swallow my pride and use the Google Cloud UI for this one.
Step 1: Set Up Domain Mapping
-
Go to the google cloud console -> select the Cloud Run service.
-
Click "Manage custom domains"
-
CLick Add Mapping -> "Add service domain mapping"
-
Select the service you want to map to -> select your deployed project.
-
Enter your domain name -> Click "Continue"
-
Google Cloud will generate DNS records -> copy these
Step 2: Update DNS Settings in Your Domain Host**
-
Go to your domain provider (Cloudflare, Namecheap, Google Domains, etc.).
-
Paste the DNS records exactly as given.
-
If you are using Cloudflare, set your records to "DNS Only" (disabling proxy mode) so Google can verify them.
Step 3: Verify the DNS Changes
- While waiting, feel free to test your domain name on nslookup.io.
- If the IPv4 and IPv6 addresses matches what Google gave you, then you're good.
Bonus: Enable Subdomains
- Bonus: in your domain host DNS settings, add * as a host, CNAME as type and ghs.googlehosted.com if you want subdomains.
-Now any subdomain (blog.yourdomain.com
, api.yourdomain.com
, etc.) will automatically work.
Fix: If Your Cloud Run Region Doesn’t Support Domain Mapping
🔥 If you see:
❌ "Domain mappings are not available in this region."
💀 Google Cloud decided your region isn’t good enough.
-
Just edit the YAML file in your repository to switch to a supported one.
-
Commit and push the change.
-
In your Cloud Run services, remove the old container.
How to configure Google Cloud Storage Bucket to store any files
- Create a Google Cloud Storage Bucket. Make sure to pick a unique bucket name!
- Example locations: us-central1, europe-west2, asia-east1.
gcloud storage buckets create gs://UNIQUE_BUCKET_NAME
--location=SERVER_LOCATION
--uniform-bucket-level-access
# Navigate to your folder path
cd ~/Downloads
# Upload the entire "public" folder
gsutil cp -r /public gs://UNIQUE_BUCKET_NAME
# Upload a single file, or an entire folder (later updates)
gsutil cp myfile.png gs://UNIQUE_BUCKET_NAME
gsutil cp -r myfolder gs://UNIQUE_BUCKET_NAME
# Automate Upload with Wildcards (this uploads all .jpg files in the current directory)
gsutil cp *.jpg gs://UNIQUE_BUCKET_NAME
# (Optional) Make files Public. This will make the files publicly accessed via URL.
gsutil iam ch allUsers:objectViewer gs://UNIQUE_BUCKET_NAME
# Access to an image in the folder with your web browser, once uploaded.
https://storage.googleapis.com/UNIQUE_BUCKET_NAME/your-file.jpg
# List Files in the Bucket
gsutil ls gs://UNIQUE_BUCKET_NAME
# Delete Files (if needed)
gsutil rm gs://UNIQUE_BUCKET_NAME/filename.png
Setup PostgreSQL for Testing Environments
1. Setting Up PostgreSQL for GitHub Actions
- Go to your GitHub repo → Settings → Secrets and variables → Actions
- Add your Postgres credentials as secrets
- In your CI/CD workflow (.github/workflows/deploy.yml), use them like this:
env:
DATABASE_URL: ${{ secrets.DATABASE_URL }}
- Now your database credentials are safe, instead of hardcoded into a .env file waiting to get leaked.
2. Running PostgreSQL as a Service in GitHub Actions:
Save the following YAML file inside .github/workflows/ as test-postgres.yml.
name: Run Tests with PostgreSQL
on:
push:
branches:
- main
- develop
pull_request:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: testdb
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U user"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout Repository
uses: actions/checkout@v3
- name: Set up PostgreSQL
run: |
psql -h localhost -U user -d testdb -c "SELECT 'PostgreSQL is running';"
env:
PGPASSWORD: password
- name: Run Tests
run: ./run_tests.sh
env:
DATABASE_URL: "postgres://user:password@localhost:5432/testdb"
- Spins up a fresh new Postgres instance for every GitHub Actions run, keeping tests isolated and repeatable.
- Prevents side effects from shared databases.
3. Local Testing with Docker-Compose (optional)
- To test in a local environment.
- Use docker-compose for quick local Postgres testing:
version: '3.8'
services:
db:
image: postgres:latest
restart: always
environment:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
ports:
- "5432:5432"
- This lets you test locally in the same way GitHub Actions does.
- Resets everything with each restart, ensuring clean test environments.
Set Up PostgreSQL in a Google Cloud VM using Docker Compose
1. Google Cloud Prep
- Make sure you have a Google Cloud Project set up.
- Enable Billing, Compute Engine, and Cloud SQL Admin API.
- Create a VM instance (Debian, obviously).
2. Generate SSH key locally:
ssh-keygen -t rsa -b 4096 -C "pwatpwat@yourdomain.dev"
Hit Enter a few times to use default paths (~/.ssh/id_rsa
).
3. Connect to the VM instance
Connect
gcloud config set project pwatgres
pwatgres = project name.
- Check if the first connection worked:
gcloud config list
- Add your SSH key to the project:
gcloud compute os-login ssh-keys add --key-file=~/.ssh/id_rsa.pub
- Confirm access:
gcloud compute ssh pwat-db-vm --zone=europe-west1-b
Basic Post-Boot Hardening
Firewall with ufw:
sudo apt update && sudo apt upgrade -y
sudo apt install ufw -y
sudo ufw allow OpenSSH
sudo ufw enable
Fail2Ban (basic brute-force protection)
sudo apt install fail2ban -y
sudo systemctl enable fail2ban
sudo systemctl start fail2ban
4. Docker Setup
sudo apt update && sudo apt install docker.io -y
sudo systemctl enable docker
sudo systemctl start docker
- Test if the daemon hears your call:
docker --version
- Install Docker Compose:
sudo apt install docker-compose -y
docker-compose --version
- Let Yourself Command the Docker Army
sudo usermod -aG docker $USER
newgrp docker
You now have Docker privileges without needing sudo every time like a mortal.
5. Create Docker Compose Project
mkdir ~/pwatgres && cd ~/pwatgres
nano docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:16
restart: always
container_name: pwatgres
env_file:
- .env
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
- Create the .env file
Inside ~/pwatgres/
:
nano .env
Example contents:
POSTGRES_DB=mydb
POSTGRES_USER=admin
POSTGRES_PASSWORD=changemepls
Save and close. DO NOT commit this if you ever sync this repo.
You can lock this .env file down with:
chmod 600 .env
Deploy that beast
docker-compose up -d
Misc
- To shut down gracefully:
sudo shutdown +1 "The API layer dreams tonight. Goodnight, sweet daemon."
Security: Avoid Paying for Google’s Mistakes
- Set up a billing alert. If your database starts scaling up unnecessarily, you will get charged.
- Limit instance size in Compute Engine (e.g., ec2-nano).
Create an HMAC Server API with Python
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
app = FastAPI()
def calc_digest(key, message):
key = bytes(key, 'utf-8')
message = bytes(message, 'utf-8')
dig = hmac.new(key, message, hashlib.sha256)
return dig.hexdigest()
# HMAC Server
@app.post("/verify")
async def verify_signature(request: Request):
body = await request.json()
recieved_mac = request.headers.get("X-HMAC-Signature")
if not recieved_mac:
raise HTTPException(status_code=400, detail="Missing HMAC header")
msg_string = f"{body['mac_address']}:{body['timestamp']}"
expected_mac = calc_digest('secret-key', msg_string)
if not hmac.compare_digest(recieved_mac, expected_mac):
raise HTTPException(status_code=403, detail="Invalid signature")
return {"status": "Verified"}
Testing HMAC Protected Endpoints with curl (Bash Script)
#!/bin/bash
MESSAGE='{"mac_address":"12:34:56:78:9a:bc","timestamp":"2025-04-30T15:00:00"}'
SIGNATURE=$(echo -n '{"mac_address":"12:34:56:78:9a:bc","timestamp":"2025-04-30T15:00:00"}' |
openssl dgst -sha256 -hmac "secret-key" | sed 's/^.* //')
curl -X POST http://127.0.0.1:8000/verify \
-H "Content-Type: application/json" \
-H "X-HMAC-Signature: $SIGNATURE" \
-d "$MESSAGE"
Establish connection with an HMAC client (for example, with Ruby)
require 'openssl/hmac'
require 'mac-address'
# HMAC Client
class Hmac
def self.call
key = secret_key
mac_address = MacAddress.address
halt 404, 'Mac Address not found' if mac_address.nil?
timestamp = Time.now.to_i
message = "#{mac_address}:#{timestamp}"
mac = calc_digest(key, message)
{ signature: mac, timestamp: timestamp, mac_address: mac_address }
end
def self.secret_key
ENV['API_DB_KEY'] || raise('Missing API_DB_KEY')
end
def self.calc_digest(key, message)
OpenSSL::HMAC.hexdigest('sha256', key, message)
end
end
This chapter will mostly cover Debian-type distributions.
Setting up nftables Firewall Rules For Debian-type Distributions
Before diving into configurations, you might want to check if nftables is already installed and active on your Debian system.
- Check if nftables is Installed
dpkg -l | grep nftables
- If it's installed, you'll see an entry like:
ii nftables 0.9.8-3 amd64 Netfilter nf_tables userspace utility
- If it's not installed, install it using:
sudo apt update && sudo apt install nftables
- Check if nftables is running
sudo systemctl status nftables
Expected output if running:
● nftables.service - Netfilter Tables Loaded: loaded (/lib/systemd/system/nftables.service; enabled; vendor preset: enabled) Active: active (exited) since …
If it is inactive or stopped, you can start and enable it:
sudo systemctl enable --now nftables
Step 1: Defining a Firewall
These following commands will:
-
Define a Firewall
-
Create a new table named
filter
in the IPv4(ip
) family. -
Create a chain inside
filter
to process incoming traffic (input
). -
It sets the hook to "input" (i.e., traffic directed at this machine).
-
Priority 0 means it runs after other kernel hooks.
-
sudo nft add rule ip filter input drop
Drops all incoming traffic by default. This means no connections are allowed unless explicitly permitted later. -
sudo nft list ruleset -a
Displays the current ruleset, including handle numbers, which are useful if you need to modify or delete specific rules. -
sudo nft delete rule ip filter input handle 2
Deletes the rule with handle 2 (you need to check the handle number in your setup).
sudo nft add table ip filter
sudo nft add chain ip filter input {type filter hook input priority 0\;}
sudo nft add rule ip filter input drop
sudo nft list ruleset -a
sudo nft delete rule ip filter input handle 2
Step 2: Enable Specific Ports
These following commands:
- Allows SSH (port 22) connections if they are:
- New (first time a connection is made).
- Established (continuing an existing session).
inet
supports both IPv4 and IPv6 in one go.- Opens ports 22 (SSH), 80 (HTTP), and 443(HTTPS).
sudo nft add rule inet filter input tcp dport 22 ct state new,established accept
sudo nft add rule inet filter input tcp dport { 22, 80, 443 } ct state new,established accept
Step 3: Save & Persist the Firewall
- Save the current firewall rules into a file named
firewall.config
. - Reload the firewall from the saved configurations.
sudo nft list ruleset > firewall.config
sudo nft -f firewall.config
Reloads the firewall from the saved configuration.
- If you want to persist the rules across reboots, enable the systemd service:
sudo systemctl enable nftables.service
Avoiding the Network Cut-off Problem
Firewall misconfiguration can lock you out if you're SSH-ing into a remote server. Here’s how to avoid getting locked out:
- Always Allow SSH First Before you apply the drop-all rule, make sure to allow SSH connections first:
sudo nft add rule inet filter input tcp dport 22 ct state new,established accept
Then you can safely run:
sudo nft add rule ip filter input drop
-
Have a Backup Terminal
- Open a second SSH session before applying firewall rules.
- If something goes wrong, you can restore settings from the backup session.
-
Use a "Grace Period" Rule Instead of locking yourself out immediately, you can set a temporary rule that auto-expires:
sudo nft add rule ip filter input tcp dport 22 accept timeout 5m
This allows SSH access for 5 minutes, giving you time to fix mistakes before the rule disappears.
🔍 CLI Tools to See Background Services (the Cool Girl Terminal Way)
🧙♀️ ps aux
This is the classic spell for peeking into the underworld of processes.
ps aux
- Lists all processes.
- USER, PID, %CPU, %MEM, and the command path.
- Pipe it to less or grep for sanity.
Example: See what’s using Postgres
ps aux | grep postgres
top
/ htop
(More Visual)
top
: Built-in, real-time process overview.
htop
: Fancy, colored, scrollable version. (You will want this.)
htop
Install it with:
sudo apt install htop # Debian-based
# or
sudo xbps-install -S htop # Void Linux
Use F10 to exit, arrow keys to scroll, and F9 to send kill signals like a Linux grim reaper.
🧼 List Only Services
🚫 systemd:
If you're on a systemd-based distro (not Void, so skip this if you're on musl Void), use:
systemctl list-units --type=service
☠️ runit (Void Linux)
If you're using Void: you’re blessed. You get runit, not that systemd drama.
To list services:
sv status /var/service/*
Each service will say run
if active.
You can stop services with:
sudo sv stop <service>
Start them:
sudo sv start <service>
This chapter will mostly cover PostgreSQL.
PostgreSQL Local Setup Guide
Use the postgres superuser to create a new user, a new database and manage their permissions. For all future database operations and for production, only use the created my_project_user.
The following guide might seem a little over-engineered for a casual app, but it will ensure a level of security conform to production level applications.
Step 1: Login as superuser
sudo -i -u postgres psql
if it fails, you may need to restart Postgres with:
/etc/init.d/postgresql restart
- The default Postgres installation comes with a superuser called postgres.
- We use this account to set up new users and databases.
You can fill in with your own informations, store them in a file (such as setup.sql
) and use them in production.
Step 2: Create a New Database, Two New Users and A Separate Schema
- Inside the Postgres shell (
psql
) run (or better: write into yoursetup.sql
file):
-- Create a database user (replace with a strong password) and a temporary admin
CREATE USER temp_admin WITH PASSWORD 'temp_admin_password';
CREATE USER my_project_user WITH PASSWORD 'supersecurepassword';
-- Create the database and assign ownership to the user
GRANT my_project_user TO temp_admin;
CREATE DATABASE my_project_db OWNER temp_admin;
GRANT CONNECT ON DATABASE my_project_db TO my_project_user;
-- If you want isolation from the default public schema, create a custom schema:
CREATE SCHEMA my_project_schema AUTHORIZATION my_project_user;
ALTER DATABASE my_project_db SET search_path TO my_project_schema;
GRANT USAGE ON SCHEMA my_project_schema TO my_project_user;
- This ensures that your database is not owned by the postgres superuser.
- The my_project_user will have full control over my_project_db, but no power over system-wide databases.
- From here, this guide assumes you have created my_project_schema.
Step 3: Restrict Dangerous Permissions
By default, new users can create or drop objects inside the project schema. We don’t want that.
-- Explicitly grant CREATE on schema
GRANT CREATE ON SCHEMA my_project_schema TO my_project_user;
-- Explicitly remove DROP privileges on existing tables
REVOKE DROP ON ALL TABLES IN SCHEMA my_project_schema FROM my_project_user;
ALTER DEFAULT PRIVILEGES FOR ROLE my_project_user IN SCHEMA my_project_schema
REVOKE DROP ON TABLES FROM my_project_user;
- This prevents accidental database-wide modifications.
- The user will still be able to read and modify existing tables.
Step 4: Enforce Security Best Practices
You should prevent the user from becoming a superuser, creating other databases and creating new users.
ALTER USER my_project_user WITH NOSUPERUSER NOCREATEDB NOCREATEROLE;
Step 5: Allow CRUD Operations
-- Grant CRUD operations to the user, and ensure it has access to future tables as well
ALTER DEFAULT PRIVILEGES FOR ROLE my_project_user IN SCHEMA my_project_schema
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO my_project_user;
Step 6: Grant Usage on Sequences (Critical for Auto Increments)
ALTER DEFAULT PRIVILEGES FOR ROLE my_project_user IN SCHEMA my_project_schema
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO my_project_user;
Step 7: Drop temp_admin
Since at this point, temp_admin has only been used to create a new database, it still has full ownership and is a security risk. You should reassign everything and then delete it. If you want, you can always keep it and modify its permissions separately, but this is a pragmatic and secure solution.
-- Reassign all objects owned by temp_admin to my_project_user
REASSIGN OWNED BY temp_admin TO my_project_user;
-- Remove any remaining privileges
DROP OWNED BY temp_admin;
-- Finally, delete the user
DROP USER temp_admin;
Step 8: Exit and Verify Setup
\l
: List all databases
\du
: List all users and their roles
\q
: Exit Postgres shell
- Show a user's privilege:
SELECT * FROM information_schema.role_table_grants
WHERE grantee='my_project_user';
Step 9: Connect as the New User
If you have created a setup.sql
file with the informations above and filled in with your own data, you can import it into Postgres with this simple command:
psql -U postgres -f setup.sql
Now test logging into your database as the newly created user:
psql -U my_project_user -d my_project_db
Troubleshooting
- Delete database/user (if you messed up, can happen):
DROP DATABASE my_project_db;
DROP USER my_project_user;
- If Postgres refuses to drop a database because it's in use, force disconnect users before deleting:
SELECT pg_terminate_backend (pid)
FROM pg_stat_activity
WHERE datname='my_project_db';
This correctly finds active connections and terminates them.
- If Postgres refuses to drop the user because they still own objects, you might need to do this before dropping the user:
REASSIGN OWNED BY my_project_user TO project_admin;
DROP OWNED BY my_project_user;
DROP USER my_project_user;
- Find Which Database a User Owns
SELECT datname, pg_catalog.pg_get_userbyid(datdba) AS owner
FROM pg_database;
Add the User to your backend .env File
DATABASE_URL=postgres://my_project_user:supersecurepassword@localhost/my_project_db
- This keeps credentials outside of the codebase.
- Use environment variables instead of hardcoding credentials.
Rate Limiting With PostgreSQL
If you have a small app, you do not need to setup an entire new Redis instance. You can instead build your own 'poor mans Redis', with unlogged tables (faster writes and no WAL overhead) and automatic cleanup with a cron job. In this guide, you will learn how to add a rate limiting feature directly onto PostgreSQL, which is useful to greatly reduce the risk of brute force attacks.
These queries are meant to be run from your backend application file, not from psql.
Language Compatibility Notice
- This SQL syntax ($1, $2, etc...) is compatible with Ruby, JavaScript, and Go.
- If using Python (psycopg2), replace $1 with %s.
- If using Java (JDBC), use ? placeholders instead.
- Regardless of the language, make sure to use parametrized queries to prevent SQL injection.
1. The Rate-Limiting Table
Since this is login-related, we can use an UUID identifier and timestamps.
CREATE UNLOGGED TABLE login_attempts (
session_id UUID DEFAULT gen_random_uuid(), -- Secure, unique session tracking
username TEXT NOT NULL,
attempt_count INT DEFAULT 1,
first_attempt TIMESTAMP DEFAULT now(),
PRIMARY KEY (session_id, username) -- Prevent duplicate session-user pairs
);
- Unlogged -> Faster writes, no WAL overhead.
- UUID session identifiers are more reliable than tracking IP addresses -> no risk of blocking users with shared IP, or letting botnets or spoof IPs pass.
2. When a Login Attempt Happens
Now, inserting into this table will automatically generate a secure, unique session identifier.
INSERT INTO login_attempts (username, attempt_count)
VALUES ($1, 1)
ON CONFLICT (username)
DO UPDATE SET
attempt_count = login_attempts.attempt_count + 1
first_attempt = CASE
WHEN login_attempts.first_attempt <= now() - INTERVAL '20 minutes'
THEN now()
ELSE login_attempts.first_attempt
END;
- If it’s a new user, it gets inserted.
- If it already exists, it updates only if the time window hasn’t expired.
- If it has expired, the row stays the same (so it doesn’t increment forever).
3. Checking If the UUID is Blocked
Before processing a login attempt, check if the UUID should be blocked.
SELECT attempt_count FROM login_attempts
WHERE username = $1
AND first_attempt > now() - INTERVAL '20 minutes';
If attempt_count > 5, deny the login request.
4. Automatically Cleaning Up Old Records
- Once an IP ages out of the 20-minute window, we don’t need to track it anymore.
- This step requires a PostgreSQL extension, pg_cron, which you can find here: pg_cron
- Then, you might want to alter your default database configuration file (which you have hopefully created first by following this guide.
ALTER USER my_project_user SET cron.job_run_as_owner = true;
- Set up the pg_cron extension:
CREATE EXTENSION pg_cron;
CREATE OR REPLACE FUNCTION cleanup_old_attempts() RETURNS VOID AS $$
DELETE FROM login_attempts WHERE first_attempt < now() - INTERVAL '20 minutes';
$$ LANGUAGE sql;
-- Auto clean up of old attempts, every 5 minutes
SELECT cron.schedule('*/5 * * * *', $$SELECT cleanup_old_attempts()$$);
- Keeps the table lightweight instead of storing old attempts forever.
- Runs every 5 minutes, but you can tweak as needed.
For deployment
Google Cloud SQL supports pg_cron, but you have to manually enable it since it's disabled by default.
- Go to Google Cloud Console
- Navigate to your PostgreSQL instance
- Enable pg_cron extension
- Go to Configuration -> Flags
- Add a new flag:
shared_preload_libraries = 'pg_cron'
- Click 'Save Changes & Restart the instance'.
How to Boot Up Redis
1.Installing Redis on Debian
Add the repository to the APT index, update it, and install Redis:
sudo apt-get install lsb-release curl gpg
curl -fsSL https://packages.redis.io/gpg | sudo gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
sudo chmod 644 /usr/share/keyrings/redis-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/redis-archive-keyring.gpg] https://packages.redis.io/deb $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/redis.list
sudo apt-get update
sudo apt-get install redis
- Then enable and start the service:
sudo systemctl enable redis
sudo systemctl start redis
2. Basic Configuration (to Avoid Chaos)
- Redis has a bad habit of storing everything in RAM, so if you don’t configure it properly, it could eat all your memory and crash your system. (A very unforgiving trait.)
- Edit /etc/redis/redis.conf and set some sanity limits:
maxmemory 256mb
maxmemory-policy allkeys-lru
Explanation:
- Limits Redis to 256MB so it doesn’t consume your entire system.
- Uses allkeys-lru policy, meaning it will automatically remove the least recently used keys once the memory limit is reached.
3. Connecting to Redis
- After installing, you can test it by running:
redis-cli ping
-
If it replies with PONG, congratulations—you've awakened another beast.
-
Set and retrieve a value:
SET spell "fireball"
GET spell
→ Should return "fireball" (instant, no SQL involved).
4. Securing Redis (Because It Trusts Too Much)
-
By default, Redis binds to all interfaces, meaning anyone could connect if they know the IP. That’s bad.
-
Limit Redis to localhost: Edit /etc/redis/redis.conf and change:
# bind 127.0.0.1 ::1
Keep this enabled by default
protected-mode yes
- Set a strong password: Set a password (optional, overkill for local but useful for staging/prod):
requirepass supersecurepassword
🔐 If you do this, don't forget to connect with:
Redis.new(password: ENV['REDIS_PASSWORD'])
- Restart Redis for changes to apply:
sudo systemctl restart redis
Confirm it's only bound to localhost:
sudo ss -tlnp | grep redis
-
SPACE + K
= syntax documentation -
SPACE + e
= open nvim-tree -
dd
+p
= cut and paste,yy
+p
= copy and paste -
SHIFT + V
= Select a block of text.gc
= Comment out / uncomment
Create a new repository from the command line
echo "# http_server" >> README.md
git init
git add README.md
git commit -m "first commit"
git branch -M main
git remote add origin git@github.com:theflyoccultist/http_server.git
git push -u origin main
Connect to a new repository
-
Open a Terminal
Navigate to the directory where you want to clone the repository.
cd /path/to/your/directory
-
Clone the Repository
Run the following command:
git clone https://github.com/theflyoccultist/kepler-rss-feed.git
Or, if using SSH:
git clone git@github.com:theflyoccultist/kepler-rss-feed.git
-
Navigate into the Repository
After cloning, navigate into the repository folder:
cd kepler-rss-feed
.gitignore a file that has already been pushed
echo debug.log >> .gitignore
git rm --cached debug.log
git commit -m "Start ignoring debug.log"
Change the URL of a repository:
git remote -v
Then, you can set it with:
git remote set-url origin <NEW_GIT_URL_HERE>
Remove a Git commit which has not yet been pushed
git reset HEAD^
YAML Cheatsheet
What is YAML
- Data serializaion language (like XML and JSON)
- Standard format to transfer data
- Extensions : .yaml and .yml
- YAML is a superset of JSON: any valid JSON file is also a valid YAML file
- Data structures defined in line separation and indentation
YAML Use Cases
- Docker-compose, Ansible, Kubernetes and many more
Key value pairs
app: user-authentication
port: 9000
# A comment
version: 1.7
# A second comment
- For strings, you can use either double quotes, single quotes or no quotes at all. If you use \n, you have to use double quotes or YAML don't recognize it.
Objects
microservice:
app: user-authentication
port: 9000
version: 1.7
- The space has to be the exact same for each attribute between objects. You can use an online YAML validator because it is sensitive about those spaces.
Lists & Boolean
microservice:
- app: user-authentication
port: 9000
version: 1.7
deployed: false # yes and no, on and off works too
versions:
- 1.9
- 2.0
- 2.1 # You can use lists inside of list items, always align them.
- app: shopping-cart
port: 9002
versions: [2.4, 2.5, "hello"]
# You can use arrays instead, and have a mix of numbers and strings.
microservices:
- user-authentication
- shopping-cart
Boolean pitfalls:
yes: true # Interpreted as boolean true
no: false # Interpreted as boolean false
on: true # Also interpreted as true
off: false # Also interpreted as false
If you actually want "yes", "no", "on" and "off" as strings, quote them:
user-input: "yes" # String, not a boolean
Use !!str, !!int and !!bool for Explicit Types
Sometimes YAML thinks it knows what you mean. Force it to behave.
bad-example: 00123 # YAML assumes this is an octal number (!!!)
good-example: !!str "00123" # Now it's a string, not octal
Real Kubernetes YAML Configuration Example
- Key-value pairs
- metadata: object
- labels: object
- spec: object
- containers: list of objects
- ports: list
- volumeMounts: list of objects
apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app: nginx
spec:
containers:
- name: nginx-container
image: nginx
ports:
- containerPort: 80
volumeMounts:
- name: nginx-vol
mountPath: /usr/nginx/html
- name: sidecar-container
image: curlimages/curl
command: ["/bin/sh"]
args: ["-c", "echo Hello from the sidecar container; sleep 300"]
Multi Line Strings
multilineString: |
this is a multiline String
and this is the next line.
next line
singlelineString: >
this is a single line String
that should be all on one line.
some other stuff
- Use | pipes if you want YAML to interpret this as multi line text. The line breaks will stay.
- Greater than sign > will be interpreted as a single line.
Real Kubernetes examples
apiVersion: v1
kind: ConfigMap
metadata:
name: mosquito-config-file
data:
mosquito.conf: |
log_dest stdout
log_type all
log_timestamp true
listener 9001
- You can put a whole shell script inside a YAML file.
command:
- sh
- -c
- |
http () {
local path="${1}"
set -- -XGET -s --fail
curl -k "$@" "http://localhost:5601${path}"
}
http "/app/kibana"
Environment Variables
- You can access them using a dollar sign inside your YAML configuration.
command:
- /bin/sh
- -ec
- >-
mysql -h 127.0.0.1 -u root -p$MYSQL_ROOT_PASSWORD -e 'SELECT 1'
Placeholders
- Instead of directly defining values, you can put placeholders with double brackets. It gets replaced using a template generator.
apiVersion: v1
kind: Service
metadata:
name: {{ .Values.service.name }}
spec:
selector:
app: {{ .Values.service.app }}
ports:
- protocol: TCP
port: {{ .Values.service.port }}
targetPort: {{ .Values.service.targetport }}
YAML Anchors & Aliases (DRY Principle)
YAML lets you reuse values using anchors (&) and aliases (*)
default-config: &default
app: user-authentication
port: 9000
version: 1.7
microservice:
- <<: *default # Reuses the default config
deployed: false
- app: shopping-cart
port: 9002
version: 2.4
Merge Keys (Combine Multiple Defaults)
Anchors can also be merged into objects:
common-config: &common
logging: true
retries: 3
extra-config:
<<: *common # Merges the common config
retries: 5 # Overrides specific values
Multiple YAML documents
- This is especially useful when you have multiple components for one service. Separate them with three dashes.
apiVersion: v1
kind: ConfigMap
metadata:
name: mosquito-config-file
data:
mosquito.conf: |
log_dest stdout
log_type all
log_timestamp true
listener 9001
---
apiVersion: v1
kind: Secret
metadata:
name: mosquito-secret-file
type: Opaque
data:
secret.file: |
cbdfdfg654fgdfg6f5sb132v1f6sg854g6s8g66IYUHGFKJHGVfd21=
- In Kubernetes, you can use both YAML or JSON, but YAML is cleaner and more readable.
YAML Linting Tools
A CLI tool: yamllint
kubectl apply --dry-run=client -f file.yaml
(Validates YAML syntax for Kubernetes)
SQL Cheatsheet - Part One (Fundamentals)
What is SQL?
- Structured Query Language
- A language to interact with data.
How is data saved?
- In tables, within the database
Imagine the database as a library
- Table: one of the bookshelves
- Data: A book
- When we want to retrieve a book, we use SQL.
Learn the SQL fundamentals properly and you will be a powerful engineer.
In PostgreSQL:
"double quotes"
→ for table and column names (identifiers)'single quotes'
→ for values (string comparisons, data, etc)
We will work with these two tables.
table name: kimetsu
name | kokyu | feature |
---|---|---|
静岡 アダメ | 地獄の呼吸 | 突進 |
竜宮城 | 炎の呼吸 | 眉毛の二段 |
岡山 悟 | 水の呼吸 | 天然 |
大豆の子 | 竹 | |
鱗滝 | 水の呼吸 | 師匠 |
table name: eva
name | kawaii | role |
---|---|---|
レイ | 10 | パイロット |
アスカ | 3 | パイロット |
ゆい | 6 | |
ミサト | 4 | 作戦部長 |
- The entire thing: table
- Vertical line: a column
- Horizontal line: a record
SELECT
: displays the desired columns from the specified table
SELECT "name", "feature" -- column name
FROM kimetsu; -- table name
name | feature |
---|---|
静岡 アダメ | 突進 |
竜宮城 | 眉毛の二段 |
岡山 悟 | 天然 |
大豆の子 | 竹 |
鱗滝 | 師匠 |
AS
: renames the desired columns
SELECT "name" AS "名前", "feature" as "特徴"
FROM kimetsu;
名前 | 特徴 |
---|---|
静岡 アダメ | 突進 |
竜宮城 | 眉毛の二段 |
岡山 悟 | 天然 |
大豆の子 | 竹 |
鱗滝 | 師匠 |
When you don’t have the energy to type out column names (but still want results fast). Only do this on small tables unless you hate your DBA
SELECT *
FROM kimetsu;
name | kokyu | feature |
---|---|---|
静岡 アダメ | 地獄の呼吸 | 突進 |
竜宮城 | 炎の呼吸 | 眉毛の二段 |
岡山 悟 | 水の呼吸 | 天然 |
大豆の子 | 竹 | |
鱗滝 | 水の呼吸 | 師匠 |
DISTINCT
: How to hide duplicate data within a column
SELECT "kokyu"
FROM kimetsu;
kokyu |
---|
地獄の呼吸 |
炎の呼吸 |
水の呼吸 |
水の呼吸 |
SELECT DISTINCT "kokyu"
FROM kimetsu;
kokyu |
---|
地獄の呼吸 |
炎の呼吸 |
水の呼吸 |
WHERE
: Retrieve entries where kawaii is more than 5
- (Remember, kawaii is subjective, and only a personal opinion.)
WHERE
works with records.
SELECT "name", "kawaii"
FROM eva
WHERE "kawaii" > 5;
name | kawaii |
---|---|
レイ | 10 |
ゆい | 6 |
AND
: Add more conditions to your WHERE
record query
SELECT *
FROM eva
WHERE "kawaii" > 5 AND "role" = 'パイロット';
name | kawaii | role |
---|---|---|
レイ | 10 | パイロット |
OR
: The record appeals to either those conditions
SELECT *
FROM eva
WHERE "kawaii" > 5 OR "role" = 'パイロット';
name | kawaii | role |
---|---|---|
レイ | 10 | パイロット |
アスカ | 3 | パイロット |
ゆい | 6 |
BETWEEN
SELECT *
FROM eva
WHERE "kawaii" BETWEEN 4 AND 6;
name | kawaii | role |
---|---|---|
ゆい | 6 | |
ミサト | 4 | 作戦部長 |
IN
, NOT IN
SELECT *
FROM eva
WHERE "role" IN ('パイロット', '作戦部長');
name | kawaii | role |
---|---|---|
レイ | 10 | パイロット |
アスカ | 3 | パイロット |
ミサト | 4 | 作戦部長 |
SELECT *
FROM eva
WHERE "role" NOT IN ('パイロット', '作戦部長');
name | kawaii | role |
---|---|---|
ゆい | 6 |
LIKE
: For Searching Data
- This matches anything starting with ア.
SELECT *
FROM eva
WHERE "name" LIKE 'ア%';
- Full pattern matching:
SELECT *
FROM eva
WHERE "name" LIKE 'アス_'; -- _ matches a single character
name | kawaii | role |
---|---|---|
アスカ | 3 | パイロット |
IS NULL
/ IS NOT NULL
: Look For Empty Data / Not Empty Data
SELECT *
FROM eva
WHERE "role" IS NULL;
name | kawaii | role |
---|---|---|
ゆい | 6 |
LIMIT
: When You Don't Want To Query The Entire Column
- SQL result rows start at 1 when displayed, but
LIMIT
andOFFSET
are 0-based. SoLIMIT 2 OFFSET 0
returns the first 2 rows. - When there is a lot of data in the column, SQL will slow down or freeze. Use LIMIT to avoid that.
SELECT *
FROM eva
LIMIT 2;
name | kawaii | role |
---|---|---|
レイ | 10 | パイロット |
アスカ | 3 | パイロット |
ORDER BY
: Sort
SELECT *
FROM eva
ORDER BY "kawaii";
name | kawaii | role |
---|---|---|
アスカ | 3 | パイロット |
ミサト | 4 | 作戦部長 |
ゆい | 6 | |
レイ | 10 | パイロット |
SELECT *
FROM eva
ORDER BY "kawaii" DESC;
name | kawaii | role |
---|---|---|
レイ | 10 | パイロット |
ゆい | 6 | |
ミサト | 4 | 作戦部長 |
アスカ | 3 | パイロット |
- SQL queries can be written in lowercase, but prefer uppercase to differenciate between keywords and column / table names. It will reduce errors.
- Insert a new line after each query to improve readability.
- Always use
LIMIT
in prod, instead of asterisks, for faster queries and to reduce server load.
SQL Cheatsheet - Part Two (GROUP BY
)
GROUP BY
is fundamental to perform aggregation functions (集計関数)- When it throws an error, it's scary
- When it works for some unknown reason, it's even scarier
- Always wrap your aggregation targets in parentheses.
AVG("age")
, notAVG "age"
. PostgreSQL will absolutely lose it otherwise.
We will work with this table
table name: members
name | created_day | channel | age |
---|---|---|---|
エレン | 2021-02-13 | web | 27 |
こういち | 2021-02-13 | ad | 27 |
さゆり | 2021-02-15 | ad | 27 |
上谷 | 2021-02-15 | ad | 33 |
あかり | 2021-02-16 | web | 24 |
COUNT
: Counts The Number of Records
SELECT COUNT("name")
FROM members
WHERE "created_day" = '2021-02-13';
count |
---|
2 |
GROUP BY
: Groups the same records together
SELECT "created_day", COUNT("name")
FROM members
GROUP BY "created_day";
created_day | count |
---|---|
2021-02-13 | 2 |
2021-02-15 | 2 |
2021-02-16 | 1 |
SELECT "created_day", "channel", COUNT("name")
FROM members
GROUP BY "created_day";
This will throw an error, because the system doesn't know if 2021-02-13
in created_day
corresponds to ad
, or web
in the column channel
.
SELECT "created_day", "channel", COUNT("name")
FROM members
GROUP BY "created_day", "channel";
created_day | channel | count |
---|---|---|
2021-02-13 | web | 1 |
2021-02-13 | ad | 1 |
2021-02-15 | ad | 2 |
2021-02-16 | web | 1 |
Aggregation functions: (集計関数)
Aggregates values
COUNT
: number of recordsAVG
: the average valueMAX
: the maximumMIN
: the minimumSUM
: the total
SELECT "created_day", AVG("age"), MAX("age")
FROM members
GROUP BY "created_day";
created_day | avg | max |
---|---|---|
2021-02-13 | 27 | 27 |
2021-02-15 | 30 | 33 |
2021-02-16 | 24 | 24 |
SQL Fundamentals - Part Three (JOIN
)
- How to retrieve data from several tables?
- The solution is to use table joins (テーブル結合)
- Scary, but very important: since a lot of the times in prod, data is stored between multiple tables
- Know the difference between
INNER JOIN
/OUTER JOIN
In the future, when civilizations will live between several planets of the Solar System and exchange overwhelming amounts of data between each other, you won't be able to survive without knowing SQL.
We will work with those tables
table name: martians
id | name |
---|---|
1 | ハリー |
2 | ハーマイオニー |
3 | ロン |
4 | ダンブルドア |
5 | ヴォルデモート |
table name: histories
id | martians_id | planet |
---|---|---|
1 | 3 | 地球 |
2 | 1 | 木星 |
3 | 4 | 土星 |
4 | 5 | 海王星 |
INNER JOIN
: Assemble two tables into one
- Use table aliases (
AS m
,AS h
) to keep it classy.
SELECT *
FROM martians
AS m
INNER JOIN "histories" AS h
ON m.id = h.martians_id;
id | name | id | martians_id | planet |
---|---|---|---|---|
1 | ハリー | 2 | 1 | 木星 |
3 | ロン | 1 | 3 | 地球 |
4 | ダンブルドア | 3 | 4 | 土星 |
5 | ヴォルデモート | 4 | 5 | 海王星 |
Using SELECT
, only keep the columns you need
SELECT m.name, h.planet
FROM martians
AS m
INNER JOIN "histories" AS h
ON m.id = h.martians_id;
m.name | h.planet |
---|---|
ハリー | 木星 |
ロン | 地球 |
ダンブルドア | 土星 |
ヴォルデモート | 海王星 |
If your actual column names include uppercase, spaces, or non-ASCII characters: wrap them in "quotes" to avoid the wrath of PostgreSQL.
SELECT m."名前", h."惑星"
LEFT OUTER JOIN
Beware: when performing a JOIN
operation, unmatching records will dissapear.
This is what you should do instead:
SELECT m.name, h.planet
FROM martians
AS m
LEFT OUTER JOIN "histories" AS h
ON m.id = h.martians_id;
m.name | h.planet |
---|---|
ハリー | 木星 |
ハーマイオニー | NULL |
ロン | 地球 |
ダンブルドア | 土星 |
ヴォルデモート | 海王星 |
This will add a null value to unmatched values.
- NULL values will break WHERE conditions unless you explicitly use IS NULL.
- If your query drops records like it’s ghosting you, check your join type.
INNER JOIN
only loves perfect matches.LEFT OUTER JOIN
accepts everyone, even if they’re broken (NULLs and all). - In a real environment,
INNER JOIN
is used more often to avoid querying noise and null values.
RIGHT OUTER JOIN
Return all rows from the right table, and the matching rows from the left. If there's no match, left table values become NULL.
SELECT m.name, h.planet
FROM martians
AS m
RIGHT OUTER JOIN "histories" AS h
ON m.id = h.martians_id;
m.name | h.planet |
---|---|
ハリー | 木星 |
ロン | 地球 |
ダンブルドア | 土星 |
ヴォルデモート | 海王星 |
In this example, it gives the same result as INNER JOIN
because all martians_id values match an existing martian.
To really see the effect of RIGHT OUTER JOIN
, you’d need a record in "histories"
with a martians_id
that doesn’t exist in "martians"
.
FULL OUTER JOIN
Return all rows from both tables, matching where possible, and filling in NULL
where not.
SELECT m.name, h.planet
FROM martians
AS m
FULL OUTER JOIN "histories" AS h
ON m.id = h.martians_id;
m.name | h.planet |
---|---|
ハリー | 木星 |
ハーマイオニー | NULL |
ロン | 地球 |
ダンブルドア | 土星 |
ヴォルデモート | 海王星 |
This behaves exactly like LEFT OUTER JOIN
here because histories doesn’t contain any records without a matching martians_id
. Add a rogue one to see NULL
in m.name
.
💡 JOIN ORACLE SAYS:
INNER JOIN
: only love with conditions.LEFT OUTER JOIN
: keeps left table's ghosts.RIGHT OUTER JOIN
: brings right table’s strays.FULL OUTER JOIN
: a big weird family reunion where nobody’s left out.
SQL Fundamentals - Part Three (CASE
)
- What if you wanted logical operators in SQL, like in every other programming language? (
if... else
) - Knowing how to use this is what differenciates newbies and pros.
We will work with this table
table name: populations
pref_name | population |
---|---|
京都 | 300 |
大阪 | 900 |
福岡 | 500 |
佐賀 | 100 |
- Do not underestimate SQL, do not resort to using Excel for your tables. SQL has everything you need, and you just have skill issues.
How to use:
SELECT CASE WHEN condition THEN value
WHEN condition THEN value
ELSE value END
FROM table_name
For these operations, it's important to think of these operations as having two steps (or more)
Use case:
-- Step 1
SELECT
CASE WHEN "pref_name" IN ('京都', '大阪') THEN '関西'
WHEN "pref_name" IN ('福岡', '佐賀') THEN '九州'
ELSE NULL
END AS "district",
SUM("population")
FROM populations
-- Step 2
GROUP BY
CASE WHEN "pref_name" IN ('京都', '大阪') THEN '関西'
WHEN "pref_name" IN ('福岡', '佐賀') THEN '九州'
ELSE NULL
END;
Step 1:
district | population |
---|---|
関西 | 300 |
関西 | 900 |
九州 | 500 |
九州 | 100 |
Step 2:
district | population |
---|---|
関西 | 1200 |
九州 | 600 |
- With this, you can use SQL like a real programming language
- Using
GROUP BY
andSUM
together: very powerful - You are no more a database newbie, you will be intermediate.
Window Functions
- Let you perform calculations across rows related to the current row, without collapsing them like GROUP BY.
Example: Ranking cities by population without losing the full dataset
SELECT
"pref_name",
"population",
RANK() OVER (ORDER BY "population" DESC) AS "rank"
FROM populations
;
pref_name | population | rank |
---|---|---|
大阪 | 900 | 1 |
福岡 | 500 | 2 |
京都 | 300 | 3 |
佐賀 | 100 | 4 |
Notice: No GROUP BY, no data loss, just vibes and rankings.
CTEs (Common Table Expressions)
- Think of them like temporary named subqueries—great for breaking down complex queries or recursive stuff.
Example: Clean up a CASE
mess first using a CTE
WITH regional_pop AS (
SELECT
CASE
WHEN "pref_name" IN ('京都', '大阪') THEN '関西'
WHEN "pref_name" IN ('福岡', '佐賀') THEN '九州'
ELSE '不明'
END AS "region",
"population"
FROM populations
)
SELECT "region", SUM("population") AS "total_population"
FROM regional_pop
GROUP BY "region";
SQL Fundamentals - Part Five (subqueries)
- A subquery is a query inside of a query.
- Ever wanted to perform a
SELECT
inside of aSELECT
? Well, you actually can. - You start becoming a query magician.
We will work with this table
table name: items
name | category | price |
---|---|---|
iPhone 12 | スマホ | 100000 |
Pixel 5 | スマホ | 80000 |
Xperia 511 | スマホ | 90000 |
ルンバ980 | 掃除機 | 50000 |
Dyson V10 | 掃除機 | 40000 |
バルミューダC | 掃除機 | 60000 |
Display items where the price is higher than average
SELECT AVG("price")
FROM items
;
-- 70000
SELECT *
FROM items
WHERE "price" >= 70000;
This is fine, but it can be done in a single query like this:
SELECT *
FROM items
WHERE price >= (SELECT AVG("price")
FROM items
);
name | category | price |
---|---|---|
iPhone 12 | スマホ | 100000 |
Pixel 5 | スマホ | 80000 |
Xperia 511 | スマホ | 90000 |
Yeah, we are querying a SELECT
inside of a SELECT
. That's what a subquery is.
Display items where the price is higher than average, but for each category
SELECT *
FROM items
WHERE price >= (SELECT AVG("price")
FROM items
GROUP BY "category");
This will return an error, because it will return two averages. The solution is to use aliases to discern them.
SELECT *
FROM items
AS i1
WHERE i1."price" >= (
SELECT AVG(i2."price")
FROM items
AS i2
WHERE i1."category" = i2."category"
);
name | category | price |
---|---|---|
iPhone 12 | スマホ | 100000 |
Xperia 511 | スマホ | 90000 |
ルンバ980 | 掃除機 | 50000 |
バルミューダC | 掃除機 | 60000 |
- Subqueries will make your life easier, otherwise you have to write queries one by one.
🧠 Subquery Tips:
- Use subqueries when filtering against aggregated or correlated data.
- Correlated subqueries reference the outer query—use aliases to stay sane.
- Subqueries inside
WHERE
,SELECT
, or evenFROM
are all valid and powerful. - Avoid unnecessary subqueries in production—they can destroy performance.
- If you are getting errors, try writing the function without subqueries.
- SQL: Practice makes muscle memory.
Bash
Basically impossible to escape from if you are using Linux, you'd be using it everyday for all sorts of stuff.
Step-by-Step Bash Completion Check-Up 💅
Verify the package is installed:
dpkg -l | grep bash-completion
If nothing shows up:
sudo apt install bash-completion
Reload your .bashrc:
source ~/.bashrc
Test it: Try typing something like:
git ch<TAB><TAB>
You should see suggestions like checkout, cherry-pick, etc.
Or try:
ssh <TAB><TAB>
And see if it lists known hosts.
🔍 Basic Grep Guide
- Search for a word in all .md files
grep "keyword" *.md
- Search recursively through directories
grep -r "keyword" .
- Ignore case
grep -i "keyword" filename.md
- Show line numbers
grep -n "keyword" filename.md
- Combine: recursive, case-insensitive, line numbers
grep -rin "keyword" .
- Use regular expressions (careful—this is where it gets spicy)
grep -E "foo|bar" file.md
How to test if your rate limiting works:
for i in {1..200}; do curl -s -o /dev/null -w "%{http_code}\n" http://localhost:4567; done
1..200: number of requests http://localhost:4567: the URL that needs to be tested
Sort of mini projects done in Bash, it is worth adding because despite the language's limitations, you can do quite a lot with it.
🐚 Bash Week — FizzBuzz but Cursed (PlingPlangPlong Edition)
This is an advanced FizzBuzz-style exercise, adapted for Bash with O(1) performance. No loops. No Python crutches. Just raw shell logic.
Description:
For a given input, if it's divisible by:
- 3 → output "Pling"
- 5 → output "Plang"
- 7 → output "Plong"
If none of the above, print the number itself.
Initial logic:
This simple program checks if the input number is equal to a modulo of either 3, 5 or 7. This operation however does not take the case where there's several true cases.
#!/usr/bin/env bash
if [ $(("$1" % 3)) -eq 0 ]; then
echo "Pling"
elif [ $(("$1" % 5)) -eq 0 ]; then
echo "Plang"
elif [ $(("$1" % 7)) -eq 0 ]; then
echo "Plong"
else
echo "$1"
fi
New Version:
#!/usr/bin/env bash
sound=""
(($1 % 3 == 0)) && sound+="Pling"
(($1 % 5 == 0)) && sound+="Plang"
(($1 % 7 == 0)) && sound+="Plong"
echo "${sound:-$1}
Notes:
- Uses string concatenation to combine results from multiple modulo checks.
- Uses Bash parameter expansion ${sound:-$1} to fallback to the number if sound is empty.
echo "${sound: -$1}"
-
It’s Bash's way of saying: “If sound is unset or null, use $1 instead.”
-
It’s lazy evaluation like Python’s x if x else y, but uglier and more prone to being misread after midnight.
-
C equivalent:
x ? x : y
🧪 Bash Week — Hamming Distance Spell (Char-by-Char Comparison, Final Form)
It calculates the Hamming distance (number of differing characters) between two equal-length strings.
Bash Spell:
#!/usr/bin/env bash
if [[ $# -ne 2 ]]; then
echo "Usage: $0 <string1> <string2>"
exit 1
elif [[ ${#1} -ne ${#2} ]]; then
echo "strands must be of equal length"
exit 1
else
count=0
for ((i = 0; i < ${#1}; i++)); do
a="${1:$i:1}"
b="${2:$i:1}"
if [[ "$a" != "$b" ]]; then
((count++))
fi
done
echo "$count"
fi
Notes:
- Input validation ensures exactly two args and equal string length.
- Uses Bash string slicing to compare characters by index.
- Avoids off-by-one or miscounting bugs from early exits.
- Ideal for scripting challenges, interviews, or shell-based logic tasks.
Bash Week - Bob's Invocation (with Regular Expressions)
This is basically a primitive version of an AI, with a different output depending on the text being inputted. It works as follows:
- The input ends with a question mark: answers: "Sure."
- The input is in uppercase: answers "Whoa, chill out!"
- The input is silence (either nothing or spaces): answers "Fine, be that way!"
- The input is both a question and in uppercase: answers "Calm down, I know what I'm doing!"
#!/usr/bin/env bash
input="$1"
trimmed_input="${input//[^a-zA-Z]/}"
trimmed_input2=$(tr -d ' \t\r' <<<"$input")
is_uppercase=false
is_question=false
is_silence=false
if [[ "$trimmed_input" =~ ^[[:upper:]]+$ ]]; then
is_uppercase=true
fi
if [[ "$trimmed_input2" == *\? ]]; then
is_question=true
fi
if [[ -z "$trimmed_input2" ]]; then
is_silence=true
fi
if [[ "$is_silence" == true ]]; then
echo "Fine. Be that way!"
elif [[ "$is_uppercase" == true && "$is_question" == true ]]; then
echo "Calm down, I know what I'm doing!"
elif [[ "$is_uppercase" == true ]]; then
echo "Whoa, chill out!"
elif [[ "$is_question" == true ]]; then
echo "Sure."
else
echo "Whatever."
fi
Bash Week - Scrabble Score Counter
Using cases, this will take a word as an input and calculate its value if played in Scrabble. Handles edge cases like any non alphabetic characters: in that case, no point is counted.
For example, the word "cabbage" is worth 14 points:
3 points for C
1 point for A
3 points for B
3 points for B
1 point for A
2 points for G
1 point for E
#!/usr/bin/env bash
i=${1,,}
if [[ ! "$i" =~ [a-z] ]]; then
echo 0
exit 0
fi
total=0
for ((j = 0; j < ${#i}; j++)); do
char="${i:j:1}"
case "$char" in
[aeioulnrst]) ((total += 1)) ;;
[dg]) ((total += 2)) ;;
[bcmp]) ((total += 3)) ;;
[fhvwy]) ((total += 4)) ;;
[k]) ((total += 5)) ;;
[jx]) ((total += 8)) ;;
[qz]) ((total += 10)) ;;
*) ((total += 0)) ;;
esac
done
echo "$total"
Bash Week - Armstrong Numbers
An Armstrong number is a number that is the sum of its own digits each raised to the power of the number of digits.
For example:
9 is an Armstrong number, because 9 = 9^1 = 9
10 is not an Armstrong number, because 10 != 1^2 + 0^2 = 1
153 is an Armstrong number, because: 153 = 1^3 + 5^3 + 3^3 = 1 + 125 + 27 = 153
154 is not an Armstrong number, because: 154 != 1^3 + 5^3 + 4^3 = 1 + 125 + 64 = 190
There are no ternary operators in Bash like there can be in C. In the code below, there is an alternate way to write them, while respecting bash's syntax.
#!/usr/bin/bash
result=0
for ((i = 0; i < ${#1}; i++)); do
power=$((${1:i:1} ** ${#1}))
result=$((result + power))
done
[ "$1" == "$result" ] && echo true || echo false
C
Very old, very fast and minimal, basically never goes out of style.
enum.c
This file, enum.c
, is a simple C program that demonstrates the use of an enumeration type. Here's a breakdown of the code:
-
Header Inclusion:
#include <stdio.h>
The
stdio.h
library is included for input and output functions, specifically for usingprintf
. -
Enumeration Declaration:
enum month{jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec};
An enumeration type
month
is defined, representing the months of the year. The values in the enumeration are implicitly assigned integer values starting from 0 (jan
= 0,feb
= 1, ...,dec
= 11). -
Function Definition:
enum month get_month(enum month m) { return(m); }
The function
get_month
takes an argument of typeenum month
and simply returns the same value. It's a minimal example to show how an enumeration can be passed to and returned from a function. -
Main Function:
int main() { printf("%u\n", get_month(apr)); return 0; }
The
main
function:- Calls
get_month
with theapr
enumeration value (which corresponds to 3, assuming 0-based indexing), - Prints the returned value as an unsigned integer (
%u
format specifier). - Returns 0 to indicate successful execution.
- Calls
Output:
When this program is run, it will output:
3
This corresponds to the integer value of the apr
enumeration.
Purpose:
This program is essentially a learning exercise to demonstrate the basics of declaring and using enumerations in C. It introduces how to:
- Create an enumeration,
- Pass an enumerated value to a function,
- Return an enumerated value from a function, and
- Print the integer representation of an enumerated value.
weekday.c
Purpose
This C program demonstrates the use of enum
, switch
, and case
constructs in C by working with days of the week. It includes functions to get the next and previous day and prints the corresponding day names.
Code Explanation
-
Enum Definition:
enum day {sun, mon, tue, wed, thu, fri, sat};
Defines an enumerated typeday
to represent days of the week.
-
print_day Function:
- Takes an enum
day
value as input and prints the corresponding day name using aswitch
statement. If the input is invalid, it prints an error message.
- Takes an enum
-
next_day Function:
- Takes an enum
day
value as input and computes the next day based on modulo arithmetic.
- Takes an enum
-
previous_day Function:
- Takes an enum
day
value as input and computes the previous day based on modulo arithmetic.
- Takes an enum
-
main Function:
- Demonstrates how to use the enumerated type and the functions:
- Initializes today as
fri
. - Prints the current day.
- Prints an invalid day (to demonstrate error handling).
- Prints the next and previous days.
- Initializes today as
- Demonstrates how to use the enumerated type and the functions:
Example Usage
Here’s how the program would behave:
enum day today = fri;
print_day(today); // Outputs: friday
print_day(7); // Outputs: 7 is an error
print_day(next_day(today)); // Outputs: saturday
print_day(previous_day(today)); // Expected Output: thursday
Output Example
When you compile and run the program:
friday
7 is an error
saturday
thursday
Complete Code
#include <stdio.h>
enum day {sun, mon, tue, wed, thu, fri, sat};
void print_day (enum day d) {
switch(d) {
case sun: printf("sunday"); break;
case mon: printf("monday"); break;
case tue: printf("tuesday"); break;
case wed: printf("wednesday"); break;
case thu: printf("thursday"); break;
case fri: printf("friday"); break;
case sat: printf("saturday"); break;
default: printf("%d is an error", d);
}
}
enum day next_day (enum day d) {
return (d + 1) % 7;
}
enum day previous_day (enum day d) {
return (d + 6) % 7;
}
int main() {
enum day today = fri;
print_day(today);
printf("\n");
print_day(7);
printf("\n");
print_day(next_day(today));
printf("\n");
print_day(previous_day(today));
return 0;
}
Key Learning Points
- Enumerations (
enum
) are useful for defining named constants. switch
andcase
statements simplify multi-branch conditional logic.- Be cautious of operator precedence when performing arithmetic operations.
employee.c : Employee Salary and SSN Generator
This program assigns salaries to employees in various departments and generates random Social Security Numbers (SSNs) for them.
Overview
- This project calculates salaries for employees in different departments.
- It also generates random SSNs for each employee.
Code Explanation
1. Headers and Libraries
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
stdio.h
: Used for input/output functions likeprintf
.stdlib.h
: Provides functions likerand
for generating random numbers.time.h
: Used to seed the random number generator with the current time.
2. Departments and Salaries
enum departments { HR, SALES, RESEARCH, SOFTWARE, EXECUTIVE };
const int SALARIES[] = {70000, 60000, 120000, 180000, 100000};
#define SALARY_OVER rand() % 10000 + 1
const char *DEPARTMENT_NAMES[] = {"HR", "Sales", "Research", "Software", "Executive"};
- The
departments
enum lists all departments. - The
SALARIES
array provides base salaries for each department. SALARY_OVER
adds a random bonus between 1 and 10,000.DEPARTMENT_NAMES
maps department names to their respective enum values.
3. SSN Generation
#define SSN_MAX 999999999
#define SSN_MIN 100000000
#define SSN ((rand() % (SSN_MAX - SSN_MIN + 1)) + SSN_MIN)
- Generates a random SSN between
100000000
and999999999
.
4. Processing Departments
void process_departments() {
for (int department = HR; department <= EXECUTIVE; department++) {
printf("SSN: %d\t", SSN);
printf("Salary for %s: %d\n", DEPARTMENT_NAMES[department], (SALARIES[department] + SALARY_OVER));
}
}
- Iterates through all departments.
- Prints a random SSN and the salary (including a random bonus) for each department.
5. Main Function
int main()
{
srand(time(0));
process_departments();
printf("\n---- Second Run ----\n\n");
process_departments();
return 0;
}
- Seeds the random number generator with the current time.
- Calls
process_departments
twice, simulating output for 10 employees (two runs of 5 departments).
Sample Output
The program's output will look something like this:
SSN: 123456789 Salary for HR: 71000
SSN: 987654321 Salary for Sales: 60500
SSN: 564738291 Salary for Research: 121000
SSN: 192837465 Salary for Software: 181000
SSN: 847362514 Salary for Executive: 101000
---- Second Run ----
SSN: 234567890 Salary for HR: 72000
SSN: 876543210 Salary for Sales: 61200
SSN: 473829165 Salary for Research: 119500
SSN: 928374651 Salary for Software: 183000
SSN: 847362514 Salary for Executive: 100500
Key Features
- Random SSN generation ensures unique identifiers for employees.
- Random salary bonuses simulate real-world variability in salaries.
weight_generator.c
// Generate random weight numbers within a range and assign to elephants
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define MAX_ELEPHANT_SEAL_WT_MALE 8800
#define MIN_ELEPHANT_SEAL_WT_MALE 4400
#define RANGE 4400
#define POPULATION 1000
#define WEIGHT_OVER rand() % RANGE
#define WEIGHT WEIGHT_OVER + MIN_ELEPHANT_SEAL_WT_MALE
#define FILL for (i = 0; i < POPULATION; i++) \
data[i] = WEIGHT
void print_data (int d[], int size) {
int i;
for (i = 0; i < size; i++) {
printf("%d\t", d[i]);
if ((i + 1) % 10 == 0) printf("\n");
}
}
int main () {
int i;
int data [POPULATION];
srand(time(0));
FILL;
print_data(data, POPULATION);
printf("\n\n");
return 0;
}
Overview
This program generates random weights for a population of elephant seals, based on pre-defined weight ranges. It utilizes macros to simplify the weight calculation and prints the generated weights in a tabular format.
Code Details
Key Macros
MAX_ELEPHANT_SEAL_WT_MALE
: Defines the maximum weight for a male elephant seal (8800 lbs).MIN_ELEPHANT_SEAL_WT_MALE
: Defines the minimum weight for a male elephant seal (4400 lbs).RANGE
: The range of weights (4400
lbs, calculated asMAX - MIN
).POPULATION
: The number of elephant seals in the population (1000
seals).WEIGHT_OVER
: Generates a random weight offset within the range.WEIGHT
: Calculates the actual weight by adding the offset to the minimum weight.FILL
: A macro to populate thedata
array with random weights.
Functions
void print_data(int d[], int size)
:- Prints the elements of the provided array (
d
) in rows of 10. - Parameters:
d[]
: The array of weights.size
: The size of the array.
- Prints the elements of the provided array (
main()
- Initializes an array (
data
) to store the weights of the population. - Seeds the random number generator using the current time (
srand(time(0))
). - Fills the
data
array with random weights using theFILL
macro. - Prints the generated weights using the
print_data
function.
Usage
- Compile the program using a C compiler, e.g.,
gcc weight_generator.c -o weight_generator
. - Run the program:
./weight_generator
. - The output will display 1000 weights in rows of 10, representing the weights of the elephant seals.
Example Output
4402 5000 6000 4800 7600 8800 7000 4600 5800 5400
...
Dependencies
- Standard C libraries:
<stdio.h>
: For input/output functions.<stdlib.h>
: For random number generation.<time.h>
: For seeding the random number generator.
Additional Notes
- The program is designed specifically for male elephant seals, as indicated by the defined weight range.
- The use of macros simplifies the code but can make debugging more challenging.
- The population size (
POPULATION
) and other constants can be adjusted as needed.
card_deck.c
This file, card_deck.c
, is a C program that simulates shuffling a deck of cards, drawing 7 cards 1 million times, and analyzing the probabilities of various hand outcomes. Here's a breakdown:
-
Deck Setup:
- Defines suits (
CLUB
,DIAMOND
,HEART
,SPADE
) and ranks (Ace
toKing
) of cards. - Creates an array of 52 cards (
deck
) and initializes it with all possible combinations of suits and ranks.
- Defines suits (
-
Operations on the Deck:
- Initialization: Fills the deck with cards in order.
- Shuffling: Randomly shuffles the deck using the Fisher-Yates algorithm.
-
Hand Analysis:
- Simulates drawing 7 cards (
hand
) repeatedly. - Analyzes the drawn cards to detect patterns like:
- Four of a kind
- Full house
- Three of a kind
- Two pairs
- Single pair
- No pairs
- Updates counters for each type of hand.
- Simulates drawing 7 cards (
-
Probability Calculation:
- After 1 million draws, calculates the probabilities of each pattern (e.g., four of a kind, full house) and prints the results.
- Ensures the probabilities sum up to 1 as a sanity check.
-
Other Details:
- Uses global counters to tally hand outcomes.
- Employs
rand()
for randomness and initializes it with the current time usingsrand(time(NULL))
.
The program provides insights into the likelihood of various poker hands from random draws of a shuffled deck.
// This project shuffles a deck of cards and draws 7 cards 1 million times.
// It then calculates the probability of getting a pair, two pairs, three of a kind, full house, and four of a kind.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
// Define suits
enum suit_card { CLUB, DIAMOND, HEART, SPADE };
const char *SUIT[] = { "CLUB", "DIAMOND", "HEART", "SPADE" };
// Define pips
enum pips_card { PIP_A, PIP_2, PIP_3, PIP_4, PIP_5, PIP_6, PIP_7, PIP_8, PIP_9, PIP_10, PIP_J, PIP_Q, PIP_K };
const char *PIPS[] = { "Ace", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine", "Ten", "Jack", "Queen", "King" };
#define NUM_PIPS PIP_K
#define DECK_SIZE 52
#define DRAW_SIZE 7
#define DRAW_COUNT 1000000
// Card structure with pip and suit
struct card {
enum suit_card suit;
enum pips_card pip;
} deck [52];
// Initialize the deck of cards
void initialize_deck() {
int index = 0;
// This loop outputs the entire card deck in order
for (int suit = CLUB; suit <= SPADE; suit++) {
for (int pip = PIP_A; pip <= NUM_PIPS; pip++) {
deck[index].suit = suit;
deck[index].pip = pip;
index++;
}
}
}
// Shuffle the deck of cards
void shuffle_deck() {
int i;
for (i = DECK_SIZE - 1; i > 1; i--) {
int j = rand() % DECK_SIZE; // Pick a random index
// Swap deck[i] and deck[j]
struct card temp = deck[i];
deck[i] = deck[j];
deck[j] = temp;
}
}
// I gotta access those at the main() function too, that's why I put them here
int four, full_house, three, two_pairs, two, no_pair;
// This function checks if the hands contains a pair, a three or four and adds it to the counter.
void analyze_hand(struct card hand[], int number_draws) {
int pairs = 0, three_of_a_kind = 0, four_of_a_kind = 0;
int rank_count[NUM_PIPS] = {0}; // Count occurencies of each rank
// Increment the rank count for each card in hand
for (int i = 0; i < number_draws; i++) {
rank_count[hand[i].pip]++;
}
for (int i = 0; i < NUM_PIPS; i++) {
if (rank_count[i] == 2) {
pairs++;
} else if (rank_count[i] == 3) {
three_of_a_kind++;
} else if (rank_count[i] == 4) {
four_of_a_kind++;
}
}
// This logic groups each draw, from the luckiest to the unluckiest
if (four_of_a_kind > 0) {
four++;
} else if (three_of_a_kind && pairs > 0) {
full_house++;
} else if (three_of_a_kind) {
three++;
} else if (pairs > 1) {
two_pairs++;
} else if (pairs == 1) {
two++;
} else {
no_pair++;
}
}
int main(void) {
srand(time(NULL));
struct card hand[DRAW_SIZE];
initialize_deck();
// Draw cards one million times
for (int draw = 0; draw < DRAW_COUNT; draw++) {
shuffle_deck();
for (int i = 0; i < DRAW_SIZE; i++) {
hand[i] = deck[i];
}
analyze_hand(hand, DRAW_SIZE);
}
// Printing all the probabilities here
float four_probability = (float)four / DRAW_COUNT;
printf("Probability of four of a kind : %.6f\n", four_probability);
float full_house_probability = (float)full_house / DRAW_COUNT;
printf("Probability of full house : %.6f\n", full_house_probability);
float three_probability = (float)three / DRAW_COUNT;
printf("Probability of three of a kind : %.6f\n", three_probability);
float two_pair_probability = (float)two_pairs / DRAW_COUNT;
printf("Probability of two pairs : %.6f\n", two_pair_probability);
float pair_probability = (float)two / DRAW_COUNT;
printf("Probability of a pair : %.6f\n", pair_probability);
float no_pair_probability = (float)no_pair / DRAW_COUNT;
printf("No pair : %.6f\n", no_pair_probability);
// Added this just to check that it's equal to 1
float total = four_probability + full_house_probability + three_probability + two_pair_probability + pair_probability + no_pair_probability;
printf("Total : %.6f\n", total);
return 0;
}
Bubble Sort Example in C
This program demonstrates the implementation of the Bubble Sort algorithm in C, which is a simple sorting algorithm used to arrange elements in ascending order.
Overview
- Bubble Sort is a comparison-based algorithm that repeatedly steps through the array, compares adjacent elements, and swaps them if they are in the wrong order.
- Its time complexity is (O(n^2)) in the worst and average cases, making it inefficient for large datasets.
Code Explanation
1. Headers and Libraries
#include <stdio.h>
stdio.h
is included for input/output operations usingprintf
andscanf
.
2. Swap Function
void swap(int* arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
- A helper function to swap two elements in an array.
- Parameters:
arr
: The array in which elements will be swapped.i
andj
: The indices of the elements to swap.
- Uses a temporary variable to perform the swap.
3. Bubble Sort Implementation
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
// Last i elements are already sorted
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1])
swap(arr, j, j + 1);
}
}
}
- The sorting function takes an array
arr
and its sizen
as parameters. - Outer loop:
- Runs (n-1) times because, with each pass, the largest unsorted element "bubbles up" to its correct position.
- Inner loop:
- Compares adjacent elements and swaps them if out of order.
- Reduces the number of comparisons in each subsequent pass since the last
i
elements are already sorted.
4. Main Function
int main() {
int arr[] = { 6, 0, 3, 5 };
int n = sizeof(arr) / sizeof(arr[0]);
// Calling bubble sort on array arr
bubbleSort(arr, n);
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
return 0;
}
- Array
arr
is initialized with unsorted integers{6, 0, 3, 5}
. - The size of the array is calculated as
sizeof(arr) / sizeof(arr[0])
. - The
bubbleSort
function is called to sort the array. - A loop is used to print the sorted array.
Sample Output
For the given array {6, 0, 3, 5}
, the output after sorting will be:
0 3 5 6
Key Features
- Modular Design:
- The sorting logic is encapsulated in the
bubbleSort
function. - The
swap
function enhances code readability and reusability.
- The sorting logic is encapsulated in the
- Simplicity:
- Bubble Sort is easy to implement and understand, making it suitable for educational purposes.
Limitations
- Inefficient for large datasets due to its (O(n^2)) complexity.
- Better sorting algorithms like Merge Sort or Quick Sort are more suitable for performance-critical applications.
Possible Improvements
- Optimization:
- Add a flag to check if any swaps were made in the current pass. If no swaps are made, the array is already sorted, and the algorithm can terminate early.
- User Input:
- Allow the user to input the array dynamically instead of hardcoding it.
Full Code:
// C program for implementation of Bubble sort
#include <stdio.h>
void swap(int* arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
void bubbleSort(int arr[], int n) {
for (int i = 0; i < n - 1; i++) {
// Last i elements are already in place, so the loop
// will only num n - i - 1 times
for (int j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1])
swap(arr, j, j + 1);
}
}
}
int main() {
int arr[] = { 6, 0, 3, 5 };
int n = sizeof(arr) / sizeof(arr[0]);
// Calling bubble sort on array arr
bubbleSort(arr, n);
for (int i = 0; i < n; i++)
printf("%d ", arr[i]);
return 0;
}
The file double_space_file.c
is a C program designed to take an input file, double-space its contents by inserting an additional blank line between each line of text, and then write the double-spaced output to another file.
Explanation of the Code:
1. Header Files
# include <stdio.h>
# include <stdlib.h>
#include <stdio.h>
: Provides functionalities for input/output operations like reading from or writing to files.#include <stdlib.h>
: Provides utilities for memory allocation, process control, and other helper functions likeexit()
.
2. print_file()
Function
void print_file(FILE *fptr) {
int c;
rewind(fptr);
while ((c = getc(fptr)) != EOF) {
putc(c, stdout);
}
}
- This function prints the contents of a file (
FILE *fptr
) to the standard output (terminal). rewind(fptr)
resets the file pointer to the beginning of the file.getc(fptr)
reads characters one by one, andputc(c, stdout)
prints them to the terminal.
3. double_space()
Function
void double_space(FILE *ifp, FILE *ofp) {
int c;
rewind(ifp);
while ((c = getc(ifp)) != EOF) {
putc(c, ofp);
if (c == '\n') {
putc('\n', ofp);
}
}
}
- This function reads from an input file (
ifp
) and writes to an output file (ofp
). - For every newline character (
\n
) found, it writes an extra newline character to the output file, effectively double-spacing the content.
4. main()
Function
int main (int argc, char *argv[]) {
FILE *ifp, *ofp;
if (argc != 3) {
fprintf(stderr, "Usage: <input file> <output file>\n");
exit(1);
}
ifp = fopen(argv[1], "r");
ofp = fopen(argv[2], "w");
if (ifp == NULL || ofp == NULL) {
fprintf(stderr, "Error opening files\n");
exit(1);
}
printf("My input file is: %s\n", argv[1]);
print_file(ifp);
printf("\n");
double_space(ifp, ofp);
printf("My output file is: %s\n", argv[2]);
print_file(ofp);
printf("\n");
fclose(ifp);
fclose(ofp);
return 0;
}
-
Command-line Arguments:
- The program expects two arguments: the input file name (
argv[1]
) and the output file name (argv[2]
). - If the number of arguments is incorrect, it prints a usage message and exits.
- The program expects two arguments: the input file name (
-
File Handling:
fopen(argv[1], "r")
: Opens the input file in read mode.fopen(argv[2], "w")
: Opens the output file in write mode.- If either file fails to open, an error message is displayed.
-
Workflow:
- Print the contents of the input file to the terminal using
print_file()
. - Double-space the input file's contents into the output file using
double_space()
. - Print the contents of the output file to the terminal.
- Close both files to release resources.
- Print the contents of the input file to the terminal using
-
Example Usage:
./double_space_file input.txt output.txt
- Reads
input.txt
, double-spaces its content, and writes the result tooutput.txt
.
- Reads
Summary:
This program is a simple command-line tool demonstrating file handling in C. It showcases how to read from and write to files, as well as how to manipulate the content. It also shows how to use C for a CLI utility.
fileio_rational.c
// Given a file with integer numbers, this program will use the first number to determine the array length.
// Then, it will regroup every couple next numbers as a rational number and perform operations on them.
#include <stdio.h>
#include <stdlib.h>
// Define the rational number structure
typedef struct rational {
double num; // Numerator
double den; // Denominator
struct rational *next;
} rational;
// Function to create a new rational number
rational* create_rational (int num, int den) {
if (den == 0) {
fprintf(stderr, "0 is not an anthorized Denominator\n");
exit(1);
}
rational* new_rational = (struct rational*)malloc ( sizeof(rational) );
if (new_rational == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(1);
}
new_rational -> num = num;
new_rational -> den = den;
new_rational -> next = NULL;
return new_rational;
}
// Function to loop through the rest of the numbers and pair them as rational numbers
rational* pair_rational_numbers(FILE* file) {
int num, den;
rational* head = NULL;
rational* tail = NULL;
// Assuming the first number is already handled
while (fscanf(file, "%d %d", &num, &den) == 2) {
rational* new_rational = create_rational(num, den);
if (head == NULL) {
head = new_rational;
tail = new_rational;
} else {
tail -> next = new_rational; // Link the new node to the end
tail = new_rational; // Move the tail pointer to the new node
}
}
return head;
}
// Print the list of rational numbers to the console
void print_rational_list(rational* head) {
rational* current = head;
while (current != NULL) {
printf("%f/%f\n", current -> num, current -> den);
current = current -> next;
}
}
// Helper function to simplify the rational numbers
int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
void simplify_rational(rational* r) {
int divisor = gcd(r -> num, r -> den);
r -> num /= divisor;
r -> den /= divisor;
if (r -> den < 0) {
r -> num = -r -> num;
r -> den = -r -> den;
}
}
// Perform operations on the rational numbers
rational* addition (rational* head) {
if (head == NULL) return NULL;
int total_num = head -> num;
int total_den = head -> den;
head = head -> next;
while (head != NULL) {
total_num = (total_num * head -> den) + (total_den * head -> num);
total_den = total_den * head -> den;
head = head -> next;
}
rational* total = create_rational(total_num, total_den);
simplify_rational(total);
return total;
}
rational* substraction(rational* head) {
if (head == NULL) return NULL;
int total_num = head -> num;
int total_den = head -> den;
head = head -> next;
while (head != NULL) {
total_num = (total_num * head -> den) - (total_den * head -> num);
total_den = total_den * head -> den;
head = head -> next;
}
rational* total = create_rational(total_num, total_den);
simplify_rational(total);
return total;
}
rational* multiplication(rational* head) {
if (head == NULL) return NULL;
int total_num = head -> num;
int total_den = head -> den;
head = head -> next;
while (head != NULL) {
total_num = total_num * head -> num;
total_den = total_den * head -> den;
head = head -> next;
}
rational* total = create_rational(total_num, total_den);
simplify_rational(total);
return total;
}
rational* division(rational* head) {
if (head == NULL) return NULL;
int total_num = head -> num;
int total_den = head -> den;
head = head -> next;
while (head != NULL) {
total_num = total_num * head -> den;
total_den = total_den * head -> num;
head = head -> next;
}
rational* total = create_rational(total_num, total_den);
simplify_rational(total);
return total;
}
rational* average(rational* head, int size) {
if (head == NULL || size == 0) return NULL;
rational* sum = addition(head);
sum -> den *= size;
simplify_rational(sum);
return sum;
}
//Write the result of those operations to the file
void write_result_to_file(FILE* ofp, rational* add_result, rational* sub_result, rational* mult_result, rational* div_result, rational* avg_result) {
fprintf(ofp, "Addition: %f/%f\n", add_result -> num, add_result -> den);
fprintf(ofp, "Substraction: %f/%f\n", sub_result -> num, sub_result -> den);
fprintf(ofp, "Multiplication: %f/%f\n", mult_result -> num, mult_result -> den);
fprintf(ofp, "Division: %f/%f\n", div_result -> num, div_result -> den);
fprintf(ofp, "Average: %f/%f\n", avg_result -> num, avg_result -> den);
}
//Write the result of those operations to the console
void write_result_to_console(rational* add_result, rational* sub_result, rational* mult_result, rational* div_result, rational* avg_result) {
printf("Addition: %f/%f\n", add_result -> num, add_result -> den);
printf("Substraction: %f/%f\n", sub_result -> num, sub_result -> den);
printf("Multiplication: %f/%f\n", mult_result -> num, mult_result -> den);
printf("Division: %f/%f\n", div_result -> num, div_result -> den);
printf("Average: %f/%f\n", avg_result -> num, avg_result -> den);
}
// Good to free the memory
void free_list(rational* head) {
rational* current = head;
while (current != NULL) {
rational* next = current -> next;
free(current);
current = next;
}
}
int main (int argc, char* argv[]) {
FILE *ifp, *ofp;
if (argc != 3) {
fprintf(stderr, "Usage: <filename> <filename>\n");
exit(1);
}
ifp = fopen(argv[1], "r");
if (ifp == NULL) {
fprintf(stderr, "Can't open input file %s\n", argv[1]);
exit(1);
}
ofp = fopen(argv[2], "w");
if (ofp == NULL) {
fprintf(stderr, "Can't open output file %s\n", argv[2]);
exit(1);
}
printf("Reading from %s and writing to %s\n", argv[1], argv[2]);
// Skip the first number
int first_num;
fscanf(ifp, "%d", &first_num);
printf("First number (array size): %d\n", first_num);
rational* head = NULL;
head = pair_rational_numbers(ifp);
printf("Printing the list of rational numbers\n");
print_rational_list(head);
printf("Performing calculations...\n");
rational* add_result = addition(head);
rational* sub_result = substraction(head);
rational* mult_result = multiplication(head);
rational* div_result = division(head);
rational* avg_result = average(head, first_num);
write_result_to_file(ofp, add_result, sub_result, mult_result, div_result, avg_result);
write_result_to_console(add_result, sub_result, mult_result, div_result, avg_result);
printf("Calculations written on the output file. Closing the program\n");
free_list(head);
free(add_result);
free(sub_result);
free(mult_result);
free(div_result);
free(avg_result);
fclose(ifp);
fclose(ofp);
return 0;
}
This program processes a file containing integers. It interprets the first integer as the array size and then pairs subsequent integers as rational numbers (fractions). Here’s an example of how the input and output would look:
Example Input File (e.g., input.txt
)
3
1 2
3 4
5 6
Explanation:
- The first number
3
indicates the array size (3 rational numbers). - The numbers
1 2
,3 4
, and5 6
are paired as rational numbers:1/2
3/4
5/6
Example Output File (e.g., output.txt
)
Addition: 38/24
Substraction: -10/24
Multiplication: 15/48
Division: 48/120
Average: 19/24
Explanation:
- Addition: Sum of all rational numbers.
- Subtraction: Result of subtracting all rational numbers in sequence.
- Multiplication: Product of all rational numbers.
- Division: Result of dividing all rational numbers in sequence.
- Average: Sum of all rational numbers divided by the size (3 in this case).
Console Output
When running the program, you would see:
Reading from input.txt and writing to output.txt
First number (array size): 3
Printing the list of rational numbers
0.500000/1.000000
0.750000/1.000000
0.833333/1.000000
Performing calculations...
Addition: 38/24
Substraction: -10/24
Multiplication: 15/48
Division: 48/120
Average: 19/24
Calculations written on the output file. Closing the program
Command to Run
./program input.txt output.txt
Make sure to replace program
with the compiled executable name.
Simple Linked list
//A simple example of a linked list in C.
//This example creates a linked list of integers from an array of integers.
#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>
typedef struct list
{
int data;
struct list *next;
} list;
int is_empty(const list *l){ return (l == NULL); }
list* create_list (int d)
{
list* head = malloc ( sizeof(list) );
head -> data = d;
head -> next = NULL;
return head;
}
list* add_to_front( int d, list* h )
{
list* head = create_list(d);
head -> next = h;
return head;
}
list* array_to_list(int d[], int size)
{
list* head = create_list(d[0]);
int i;
for (i = 1; i < size; i++)
{
head = add_to_front(d[i], head);
}
return head;
}
void print_list (list *h, char *title)
{
printf ("%s\n", title);
while (h != NULL) {
printf ("%d:", h -> data);
h = h -> next;
}
}
int main()
{
list list_of_int;
list* head = NULL;
int data[6] = { 2, 3, 5, 7, 8, 9 };
head = array_to_list( data, 6 );
print_list(head, "data[6] made into a 6 element list");
printf("\n\n");
return 0;
}
This program demonstrates the creation and usage of a simple linked list in C. Here's an example of how the program works:
Example Input
The program itself uses the following array of integers as input:
int data[6] = { 2, 3, 5, 7, 8, 9 };
Example Output
The program prints the following list:
data[6] made into a 6 element list
9:8:7:5:3:2:
Memory Management Note
The program uses malloc
in the create_list
function to allocate memory for nodes, but it does not free the memory after its use. This can lead to memory leaks if the program is run repeatedly or in a larger application.
To fix this, you need to add a function to free the memory allocated for the linked list, like this:
void free_list(list *h) {
list *tmp;
while (h != NULL) {
tmp = h;
h = h->next;
free(tmp);
}
}
Then, you should call free_list(head);
before exiting main()
:
int main() {
list list_of_int;
list* head = NULL;
int data[6] = { 2, 3, 5, 7, 8, 9 };
head = array_to_list( data, 6 );
print_list(head, "data[6] made into a 6 element list");
printf("\n\n");
free_list(head);
return 0;
}
Working with Lists Example in C
This program demonstrates the creation, manipulation, and sorting of a linked list using the Bubble Sort algorithm. The list is initially populated with random numbers, converted into a linked list, and then sorted.
Overview
- Generates an array of random integers.
- Converts the array into a linked list.
- Sorts the linked list using an adapted Bubble Sort algorithm.
Key Features
- Linked List Operations:
- Creation of nodes and linked lists.
- Adding elements to the front of the list.
- Conversion of an array to a linked list.
- Traversing and freeing a linked list.
- Sorting Mechanism:
- Bubble Sort algorithm adapted for linked lists.
- Random Number Generation:
- Generates random integers between 0 and 100.
Code Breakdown
1. Headers and Macros
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define SIZE 100
stdio.h
: Provides input/output functions likeprintf
.stdlib.h
: For memory allocation (malloc
) and random number generation (rand
).time.h
: Used to seed the random number generator with the system time.SIZE
: The size of the array and the linked list (100 elements).
2. Linked List Structure
typedef struct list {
int data;
struct list *next;
} list;
data
: Holds the value of the node.next
: Pointer to the next node in the list.
3. Node Creation
list* create_list(int d) {
list* head = (list*)malloc(sizeof(list));
head->data = d;
head->next = NULL;
return head;
}
- Allocates memory for a new node and initializes its data.
4. Adding Nodes to the List
list* add_to_front(int d, list* h) {
list* head = create_list(d);
head->next = h;
return head;
}
- Adds a new node to the front of the list.
5. Array to Linked List Conversion
list* array_to_list(int d[], int size) {
list* head = create_list(d[0]);
for (int i = 1; i < size; i++) {
head = add_to_front(d[i], head);
}
return head;
}
- Converts an array of integers into a linked list.
6. Freeing the List
void free_list(list *head) {
list* current = head;
list* next;
while (current != NULL) {
next = current->next;
free(current);
current = next;
}
}
- Traverses through the list and frees each node to prevent memory leaks.
7. Random Number Generation
int getRandomNumber(int min, int max) {
return rand() % (max - min + 1) + min;
}
- Generates a random integer between
min
andmax
.
8. Bubble Sort Adaptation
void bubbleSort(list* h) {
if (h == NULL || h->next == NULL) return;
list *i, *j;
for (i = h; i != NULL; i = i->next) {
for (j = i->next; j != NULL; j = j->next) {
if (i->data > j->data) {
swap(i, j);
}
}
}
}
- Sorts the linked list using the Bubble Sort algorithm.
- Compares adjacent nodes and swaps their data if they are out of order.
9. Swap Function
void swap(list* a, list* b) {
int temp = a->data;
a->data = b->data;
b->data = temp;
}
- Swaps the
data
fields of two nodes in the list.
10. Printing the List
void print_list(list *h) {
int count = 0;
while (h != NULL) {
printf("%d\t", h->data);
count++;
if (count % 5 == 0) printf("\n");
h = h->next;
}
}
- Prints the linked list's elements in rows of 5 for better readability.
11. Main Function
int main() {
srand(time(NULL));
list* head = NULL;
int data[SIZE];
for (int i = 0; i < SIZE; i++) {
data[i] = getRandomNumber(0, 100);
}
head = array_to_list(data, SIZE);
printf("\nBefore sorting\n");
print_list(head);
printf("\n");
bubbleSort(head);
printf("After Sorting\n");
print_list(head);
free_list(head);
return 0;
}
- Initializes an array with 100 random integers.
- Converts the array into a linked list.
- Prints the list before and after sorting.
- Frees the list at the end to release memory.
Sample Output
Before Sorting:
45 23 67 89 12
34 78 56 90 11
...
After Sorting:
1 2 5 6 9
10 11 12 14 15
...
Key Points
- Linked List Manipulation:
- Demonstrates creation, traversal, and memory management.
- Sorting Algorithm:
- Adapts Bubble Sort for linked lists, showcasing its flexibility.
- Random Number Handling:
- Generates randomized inputs to simulate real-world scenarios.
Limitations
- Inefficient Sorting:
- Bubble Sort has (O(n^2)) complexity, making it unsuitable for large datasets.
- Memory Overhead:
- The program uses dynamic memory allocation, which requires careful management to prevent leaks.
Possible Improvements
- Sorting Efficiency:
- Replace Bubble Sort with a more efficient algorithm like Merge Sort.
- Dynamic Size:
- Allow the user to specify the size of the list at runtime.
- Error Handling:
- Add checks for memory allocation and null pointers.
Doubly Linked List
// Given a list of integers, the program will sort the list using merge sort and remove any duplicates.
// It uses a doubly linked list to store the integers.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define SIZE 200
// Define the list node structure
typedef struct list {
int data; // Data stored in the node
struct list *next; // Pointer to the next node
struct list *prev; // Pointer to the previous node
} list;
// Function to create a new doubly linked list
struct list* create_list (int d)
{
struct list* newList = (struct list*)malloc( sizeof(struct list) );
if (newList == NULL) {
fprintf(stderr, "Memory allocation failed\n");
exit(1);
}
newList -> data = d;
newList -> next = NULL;
newList -> prev = NULL;
return newList;
}
// Function to add a new node with data 'd' to the front of the list 'h'
struct list* add_to_front(int d, list* h)
{
list* head = create_list(d);
head -> next = h;
if (h != NULL) {
h -> prev = head;
}
return head;
}
// Function to convert an array of integers into a linked list
struct list* array_to_list(int d[], int size)
{
if (d == NULL || size <= 0) {
fprintf(stderr, "Invalid array or size\n");
return NULL;
}
// Create the head of the list using the first element of the array
list* head = create_list(d[0]);
// Loop through the remaining elements of the array
for (int i = 1; i < size; i++)
{
// Add each element to the front of the list
head = add_to_front(d[i], head);
}
// Return the head of the linked list
return head;
}
// Function to generate random numbers between 0 and 100
int getRandomNumber(int min, int max) {
return rand() % (max - min + 1) + min;
}
struct list* split_list(struct list* head) {
struct list* slow = head;
struct list* fast = head;
while (fast != NULL && fast -> next != NULL) {
slow = slow -> next;
fast = fast -> next -> next;
}
if (slow != NULL && slow -> prev != NULL) {
slow -> prev -> next = NULL; // Break forward link
slow -> prev = NULL; // Break backward link
}
return slow; // Return the head of the second half
}
struct list* merge(struct list* head_a, struct list* head_b) {
if (head_a == NULL) return head_b;
if (head_b == NULL) return head_a;
struct list* result = NULL;
// Compare data and recursively merge
if (head_a -> data < head_b -> data) {
result = head_a;
result -> next = merge(head_a -> next, head_b);
if (result -> next != NULL) {
result -> next -> prev = result; // Update backward link
}
} else {
result = head_b;
result -> next = merge(head_a, head_b -> next);
if (result -> next != NULL) {
result -> next -> prev = result;
}
}
return result;
}
struct list* merge_sort(struct list* head) {
if (head == NULL || head -> next == NULL) return head;
struct list* second_half = split_list(head); // Split the list into two halves
// Recursively sort half
struct list* left_sorted = merge_sort(head);
struct list* right_sorted = merge_sort(second_half);
return merge(left_sorted, right_sorted);
}
// This function unlinks the duplicate node in next pointer.
struct list* remove_duplicates(struct list* head) {
if (head == NULL || head -> next == NULL) return head;
struct list* current = head;
while (current != NULL && current -> next != NULL) {
if (current -> data == current -> next -> data) {
struct list* duplicate = current -> next;
current -> next = duplicate -> next;
if (duplicate -> next != NULL) {
duplicate -> next -> prev = current;
}
free(duplicate);
} else {
current = current -> next;
}
}
return head;
}
// Print the numbers in rows of 5
void print_list (struct list *head) {
if (head == NULL || head -> next == NULL) return;
int count = 0;
while (head != NULL) {
printf("%d\t", head -> data);
count ++;
if (count % 5 == 0) printf("\n");
head = head -> next;
}
}
void free_list(struct list *head) {
list* current = head;
list* next;
// Traverse the list and free each node
while (current != NULL) {
next = current -> next; // Save the pointer to the next node
free(current); // Free the current node
current = next; // Move to the next node
}
}
int main() {
srand(time(NULL));
int data[SIZE];
for (int i = 0; i < SIZE; i++) {
data[i] = getRandomNumber(0, 49);
}
// Convert the array to a doubly linked list
struct list* head = array_to_list(data, SIZE);
printf("Before Sorting:\n");
print_list(head);
printf("\n");
// Perform merge sort on the list, and remove duplicates
head = merge_sort(head);
remove_duplicates(head);
printf("After Sorting and Removing Duplicates:\n");
print_list(head);
free_list(head); // Free allocated memory
return 0;
}
Example Input and Output for a Doubly Linked List Program
Example Input
Let's assume this doubly linked list program supports typical operations like adding, deleting, and traversing nodes. Here’s a sample input sequence:
- Add 10 to the list.
- Add 20 to the list.
- Add 30 to the list.
- Traverse the list forward.
- Traverse the list backward.
- Delete 20 from the list.
- Traverse the list forward again.
Example Output
Step-by-Step Output:
-
Adding 10 to the list:
List: 10
-
Adding 20 to the list:
List: 10 <-> 20
-
Adding 30 to the list:
List: 10 <-> 20 <-> 30
-
Traversing forward:
Forward: 10 -> 20 -> 30
-
Traversing backward:
Backward: 30 -> 20 -> 10
-
Deleting 20 from the list:
List: 10 <-> 30
-
Traversing forward again:
Forward: 10 -> 30
Merge Sort in Doubly Linked Lists
How It Works
Merge sort is a divide-and-conquer algorithm that splits the list into smaller sublists, sorts them individually, and then merges them back together in sorted order. With doubly linked lists, this algorithm can be implemented efficiently because of the bidirectional traversal and the ability to split and merge nodes easily.
Key Functions:
-
split_list
This function divides the doubly linked list into two halves. The midpoint is typically found using a slow and fast pointer approach. -
merge
This function takes two sorted sublists and merges them into a single sorted list, maintaining the order.
Advantages of Merge Sort in Doubly Linked Lists:
- Stability: Maintains the relative order of equal elements.
- Efficiency: Performs well on linked lists as it doesn't require random access to elements.
- Recursive Nature: Leverages the recursive structure of merge sort for easy implementation.
Example Input and Output for Merge Sort
Input:
Unsorted list: 30 <-> 10 <-> 20 <-> 50 <-> 40
Output:
Sorted list: 10 <-> 20 <-> 30 <-> 40 <-> 50
Advantages of a Doubly Linked List Over a Singly Linked List
-
Bidirectional Traversal:
The biggest advantage is the ability to traverse both forwards and backwards. This makes certain algorithms and operations easier to implement. -
Easier Deletion:
Deleting a node is simpler because you have a pointer to the previous node, so there's no need to traverse the list to find it. -
Flexibility in Insertion:
Insertion after or before a given node is straightforward as you can access both the next and previous pointers.
Drawbacks of a Doubly Linked List Compared to a Singly Linked List
-
Increased Memory Usage:
Each node requires an extra pointer (prev
), which doubles the memory used for pointers. -
Reduced Performance:
Due to the extra pointer, operations like insertion and deletion involve slightly more overhead for managing theprev
pointer. -
Complexity:
The implementation is more complex, especially when handling edge cases like inserting or deleting the first or last node.
Summary
While a doubly linked list provides more flexibility with bidirectional traversal and easier node deletion/insertion, it comes at the cost of increased memory usage and slightly reduced performance. It's an excellent choice for scenarios where frequent backward traversal is needed or when node deletion happens often.
Binary Tree
// This program opens and reads a file of integer pairs into an array, with the first integer telling it how many to read.
// It places these values into a binary tree structure. Walks the tree inorder and prints the values onto the screen.
#include <stdio.h>
#include <stdlib.h>
// Define the structure for a binary tree node
typedef struct TreeNode {
int value;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
// Function to create a new tree node
TreeNode* create_node(int value) {
TreeNode* new_node = (TreeNode*)malloc(sizeof(TreeNode));
if (!new_node) {
fprintf(stderr, "Memory allocation failed\n");
exit(1);
}
new_node->value = value;
new_node->left = new_node->right = NULL;
return new_node;
}
// Function to insert a value into the binary tree
TreeNode* insert(TreeNode* root, int value) {
if (root == NULL) {
return create_node(value);
}
if (value < root->value) {
root->left = insert(root->left, value);
} else {
root->right = insert(root->right, value);
}
return root;
}
// Function to perform inorder traversal and print the values
void inorder_traversal(TreeNode* root) {
if (root != NULL) {
inorder_traversal(root->left);
printf("%d ", root->value);
inorder_traversal(root->right);
}
}
void free_tree(TreeNode* root) {
if (root != NULL) {
free_tree(root->left);
free_tree(root->right);
free(root);
}
}
int main(int argc, char* argv[]) {
FILE *ifp;
if (argc != 2) {
fprintf(stderr, "Usage: <input_filename>\n");
exit(1);
}
ifp = fopen(argv[1], "r");
if (ifp == NULL) {
fprintf(stderr, "Can't open input file %s\n", argv[1]);
exit(1);
}
printf("Reading from %s\n", argv[1]);
// Read the first number to determine the array size
int array_size;
fscanf(ifp, "%d", &array_size);
printf("Array size: %d\n", array_size);
// Create the binary tree and insert values
TreeNode* root = NULL;
for (int i = 0; i < array_size; i++) {
int value;
fscanf(ifp, "%d", &value);
root = insert(root, value);
}
// Perform inorder traversal and print the values
printf("Inorder traversal of the binary tree:\n");
inorder_traversal(root);
printf("\n");
free_tree(root);
// Close the input file
fclose(ifp);
return 0;
}
Binary Tree Definition
A binary tree is a data structure in which each node has at most two children, referred to as the left child and the right child. It is commonly used for organizing data for quick access, insertion, and deletion.
Example Input and Output
Based on your code, the program reads pairs of integers from a file, creates a binary tree, and prints the values in-order.
Example Input File (input.txt
):
5
30 10 50 20 40
Explanation of Input:
- The first number (
5
) specifies the number of integers to insert into the binary tree. - The subsequent numbers (
30
,10
,50
,20
,40
) are inserted into the binary tree.
Output:
Reading from input.txt
Array size: 5
Inorder traversal of the binary tree:
10 20 30 40 50
Explanation of Output:
- The in-order traversal prints the values in ascending order: left subtree → root → right subtree.
ncurses (new curses)
Programming library for creating textual user interfaces (TUIs) that work across a wide variety of terminals.
References:
📦 Setup: Makefile
all: intro
intro: intro.c
gcc -o output intro.c -lncurses
Simple and essential. Without -lncurses
, nothing works and the curses gods laugh at you.
✨ Chapter 1: Hello World (But Make It Terminal)
Concepts introduced:
initscr()
,printw
,refresh
,getch
, andendwin
- Cursor positioning with
move()
- Replacing
printf
withprintw
because this is now awindowedworld.
// Hello world - moving curses
#include <ncurses.h>
int main(int argc, char **argv) {
// Initialize the screen
// sets up memory and clears the screen
initscr();
int x, y;
x = y = 10;
// moves the cursor to the specified location
// ncurses works with y, then x axis
move(y, x);
// prints string(const char *)
printw("Hello World");
// refreshes the screen to match what's in memory
refresh();
// what's for user input, returns int value of that key
int c = getch();
// clears the screen
clear();
mvprintw(0, 0, "%d", c);
getch();
// deallocates memory and ends ncurses
endwin();
return 0;
}
🧱 Chapter 2: Your First Box
Concepts introduced:
- Basic window creation with
newwin
- Drawing a border with
box()
- Printing inside a window with
mvwprintw
// Basics of windows
#include <ncurses.h>
int main(int argc, char **argv) {
initscr();
int height = 10, width = 20, start_y = 10, start_x = 10;
WINDOW *win = newwin(height, width, start_y, start_x);
refresh();
box(win, 0, 0);
mvwprintw(win, 1, 1, "this is my box");
wrefresh(win);
getch();
endwin();
return 0;
}
📦 Chapter 3: Dialog Box with Custom Borders
Concepts introduced:
cbreak()
,raw()
, andnoecho()
(user input control)- Custom borders with
wborder()
- ASCII fun with corner and edge characters
// Display a dialog box in ncurses
#include <ncurses.h>
int main(int argc, char **argv) {
/* NCURSES START */
initscr();
cbreak(); // lets you exit the program with Ctrl + C. Default behavior
raw(); // takes all input as raw input
noecho(); // user input does not show up on screen
int height = 10, width = 20, start_y = 10, start_x = 10;
WINDOW *win = newwin(height, width, start_y, start_x);
refresh();
char c = '+'; // workaround if you don't know ASCII values
char space = ' ';
// box(win, (int)c, 104); // these are ASCII values
// a more fine tuned box
int left = 103, right = 103, top = 104;
int bottom = (int)space;
int tlc = (int c), trc = (int)c, blc = bottom, brc = bottom;
wborder(win, left, right, top, bottom, tlc, trc, blc, brc);
mvwprintw(win, 2, 2, "my box");
wrefresh(win);
getch();
getch();
endwin();
/* NCURSES END */
return 0;
}
🎨 Chapter 4: Attributes and Colors
Concepts introduced:
has_colors()
,start_color()
,init_pair()
COLOR_PAIR()
,attron
,attroff
- Changing color definitions with
init_color()
- Text attributes like
A_BOLD
,A_BLINK
,A_REVERSE
// Attributes and colors
#include <ncurses.h>
int main(int argc, char **argv) {
/* NCURSES START */
initscr();
noecho();
if (!has_colors()) {
printw("No colors detected");
getch();
return -1;
}
start_color();
init_pair(1, COLOR_CYAN, COLOR_MAGENTA); // pair number 1
/*
* COLOR_PAIR(n)
* COLOR_BLACK 0
* COLOR_RED 1
* COLOR_GREEN 2
* COLOR_YELLOW 3
* COLOR_BLUE 4
* COLOR_MAGENTA 5
* COLOR_CYAN 6
* COLOR_WHITE 7
*/
// To change colors
if (can_change_color()) {
printw("Can change color");
init_color(COLOR_CYAN, 123, 122, 138);
}
attron(COLOR_PAIR(1));
printw("Test");
attroff(COLOR_PAIR(1));
/*
A_NORMAL Normal display (no highlight)
A_STANDOUT Best highlighting mode of the terminal.
A_UNDERLINE Underlining
A_REVERSE Reverse video
A_BLINK Blinking
A_DIM Half bright
A_BOLD Extra bright or bold
A_PROTECT Protected mode
A_INVIS Invisible or blank mode
A_ALTCHARSET Alternate character set
A_CHARTEXT Bit-mask to extract a character
COLOR_PAIR(n) Color-pair number n
*/
getch();
endwin();
/* NCURSES END */
return 0;
}
📋 Chapter 5: The Beginnings of a Menu
Concepts introduced:
- More
WINDOW
geometry functions:getyx
,getbegyx
,getmaxyx
- Laying the groundwork for interactive menu systems
// Build a menu with ncurses
#include <ncurses.h>
int main(int argc, char **argv) {
initscr();
noecho();
cbreak();
int y, x, yBeg, xBeg, yMax, xMax;
WINDOW *win = newwin(10, 20, 10, 10);
getyx(stdscr, y, x);
getbegyx(win, yBeg, xBeg);
getmaxyx(stdscr, yMax, xMax);
mvprintw(yMax / 2, xMax / 2, "%d %d", yBeg, xBeg);
// printw("%d %d %d %d %d %d", y, x, yBeg, xBeg, yMax, xMax);
// make sure the program waits before exiting
getch();
endwin();
return 0;
}
🧾 Chapter 6: Reading User Input in Pure C (No C++ Sorcery Allowed)
Concepts introduced:
getstr()
for reading strings from the user- Centered text with
mvprintw()
and screen dimensions viagetmaxyx()
- Classic C-style string handling (
char[]
andstrlen()
)
// Working with user input
#include <ncurses.h>
#include <string.h>
int main() {
char msg[] = "Enter a string";
char str[80]; // fixed-size buffer (because this is C, not a luxury resort)
int row, col;
initscr();
getmaxyx(stdscr, row, col);
mvprintw(row / 2, (col - strlen(msg)) / 2, "%s", msg);
getstr(str); // reads a line of text
mvprintw(LINES - 2, 0, "You entered: %s", str);
getch();
endwin();
return 0;
}
⚠️ Reminder:
getstr()
is a bit raw and assumes you won’t type more than 79 characters. If you do, chaos. Handle with care or replace withwgetnstr()
for actual safety.
🎮 Chapter 7: Building a Menu with Arrow Keys (Real Game Dev Vibes)
Concepts introduced:
- Menu system with
const char*
andkeypad()
- Highlighting selections with
A_REVERSE
- User selection handling with
KEY_UP
,KEY_DOWN
, and Enter (10
) - Fixed buffer index handling to prevent out-of-bounds disasters
#include <ncurses.h>
int main(int argc, char **argv) {
initscr();
noecho();
cbreak();
int yMax, xMax;
getmaxyx(stdscr, yMax, xMax);
WINDOW *menuwin = newwin(6, xMax - 12, yMax - 8, 5);
box(menuwin, 0, 0);
refresh();
wrefresh(menuwin);
keypad(menuwin, true);
const char *choices[3] = {"Walk", "Jog", "Run"};
int choice;
int highlight = 0;
while (1) {
for (int i = 0; i < 3; ++i) {
if (i == highlight)
wattron(menuwin, A_REVERSE);
mvwprintw(menuwin, i + 1, 1, "%s", choices[i]);
wattroff(menuwin, A_REVERSE);
}
choice = wgetch(menuwin);
switch (choice) {
case KEY_UP:
highlight--;
if (highlight < 0)
highlight = 0;
break;
case KEY_DOWN:
highlight++;
if (highlight > 2)
highlight = 2;
break;
default:
break;
}
if (choice == 10) // Enter key
break;
}
clear();
mvprintw(0, 0, "Your choice: %s", choices[highlight]);
refresh();
getch();
endwin();
return 0;
}
Chapter 8: A Rogue like Game Engine
- Movements, walls, boundaries, breadcrumbs.
This is the demo for a tiny rogue-like engine, a @
character leaving .
bread crumbs wherever it goes.
It is using several files with struct
which are the equivalent of constructors in C++. Methods becomes plain old functions.
Everything is pointer based.
player.h
#ifndef PLAYER_H
#define PLAYER_H
#include <ncurses.h>
typedef struct Player {
int yLoc, xLoc;
int yMax, xMax;
char character;
WINDOW *curwin;
} Player;
Player *create_player(WINDOW *win, int y, int x, char c);
void move_up(Player *p);
void move_down(Player *p);
void move_left(Player *p);
void move_right(Player *p);
int get_input(Player *p);
void display_player(Player *p);
#endif
player.c
Implementation
#include "player.h"
#include <stdlib.h>
Player *create_player(WINDOW *win, int y, int x, char c) {
Player *p = (Player *)malloc(sizeof(Player));
p->curwin = win;
p->yLoc = y;
p->xLoc = x;
getmaxyx(win, p->yMax, p->xMax);
keypad(win, TRUE);
p->character = c;
return p;
}
void move_up(Player *p) {
mvwaddch(p->curwin, p->yLoc, p->xLoc, '.');
p->yLoc--;
if (p->yLoc < 1)
p->yLoc = 1;
}
void move_down(Player *p) {
mvwaddch(p->curwin, p->yLoc, p->xLoc, '.');
p->yLoc++;
if (p->yLoc > p->yMax - 2)
p->yLoc = p->yMax - 2;
}
void move_left(Player *p) {
mvwaddch(p->curwin, p->yLoc, p->xLoc, '.');
p->xLoc--;
if (p->xLoc < 1)
p->xLoc = 1;
}
void move_right(Player *p) {
mvwaddch(p->curwin, p->yLoc, p->xLoc, '.');
p->xLoc++;
if (p->xLoc > p->xMax - 2)
p->xLoc = p->xMax - 2;
}
int get_input(Player *p) {
int choice = wgetch(p->curwin);
switch (choice) {
case KEY_UP:
move_up(p);
break;
case KEY_DOWN:
move_down(p);
break;
case KEY_LEFT:
move_left(p);
break;
case KEY_RIGHT:
move_right(p);
break;
default:
break;
}
return choice;
}
void display_player(Player *p) {
mvwaddch(p->curwin, p->yLoc, p->xLoc, p->character);
}
main.c
#include "player.h"
#include <ncurses.h>
#include <stdlib.h>
int main(int argc, char **argv) {
initscr();
noecho();
cbreak();
int yMax, xMax;
getmaxyx(stdscr, yMax, xMax);
WINDOW *playwin = newwin(20, 50, (yMax / 2) - 10, 10);
box(playwin, 0, 0);
refresh();
wrefresh(playwin);
Player *p = create_player(playwin, 1, 1, '@');
do {
display_player(p);
wrefresh(playwin);
} while (get_input(p) != 'x');
endwin();
free(p);
return 0;
}
Makefile
all: player
player: main.c player.c player.h
gcc -Wall -o output main.c player.c -lncurses
📚 Input Modes, Color Witchcraft, and Keyboard Sorcery
🧩 Chapter 9: Input Timing & Modes
Learn the nuanced differences between:
cbreak()
– reads input immediately, char by char (but still blocks).halfdelay(t)
– like cbreak, butgetch()
times out after tenths of a second.nodelay(stdscr, TRUE)
– makesgetch()
non-blocking entirely.timeout(ms)
–getch()
blocks for up toms
milliseconds.
#include <ncurses.h>
#include <stdbool.h>
int main(int argc, char **argv) {
initscr();
noecho();
// Input mode options (uncomment to test different ones)
cbreak(); // read instantly but still blocks
// halfdelay(10); // waits up to 1s (10 * 0.1s)
// nodelay(stdscr, TRUE); // never blocks
timeout(500); // wait 500ms max for input
int c;
while ((c = getch()) != 'x') {
printw("%d\n", c);
}
endwin();
return 0;
}
🎨 Chapter 10: Color and Attribute Combos
Level up your ncurses
glam with:
- Multiple color pairs
- Mixing colors with attributes like
A_REVERSE
- Creating
chtype
values with embedded style
#include <curses.h>
int main(int argc, char **argv) {
initscr();
if (!has_colors()) {
endwin();
printf("Color can't be used.\n");
return 1;
}
start_color();
init_pair(1, COLOR_YELLOW, COLOR_BLACK);
init_pair(2, COLOR_RED, COLOR_BLACK);
attron(A_REVERSE | COLOR_PAIR(2));
mvaddch(5, 5, 'a');
mvaddch(5, 6, 'b');
mvaddch(5, 7, 'c');
attroff(A_REVERSE | COLOR_PAIR(2));
// Color + attribute embedded in a single value
chtype c = '@' | A_REVERSE | COLOR_PAIR(1);
mvaddch(9, 5, c);
getch();
endwin();
return 0;
}
⌨️ Chapter 11: Ctrl Key Handling
Detect Ctrl + key combos like a boss
(e.g., for shortcuts or a baby nano
editor vibe)
#include <ncurses.h>
#define ctrl(x) ((x) & 0x1F)
// Shift detection? Sorry hun, not in this terminal's reality
int main(int argc, char **argv) {
initscr();
noecho();
char ch;
while ((ch = getch())) {
mvprintw(1, 0, "KEY NAME : %s - 0x%02x\n", keyname(ch), ch);
if (ch == ctrl('a')) {
mvprintw(0, 0, "Detected Ctrl+A!");
}
}
endwin();
return 0;
}
MenuBar with ncurses
Makefile
all: menu
menu: main.c menu.c submenu.c
gcc -Wall -o menu main.c menu.c submenu.c -lncurses
main.c
#include "menu.h"
#include "submenu.h"
#include <curses.h>
int main() {
initscr();
noecho();
curs_set(0);
int yMax, xMax;
getmaxyx(stdscr, yMax, xMax);
WINDOW *win = newwin(yMax / 2, xMax / 2, yMax / 4, xMax / 4);
box(win, 0, 0);
const char *file_items[] = {"open", "save", "quit", NULL};
const char *edit_items[] = {"cut", "copy", "paste", NULL};
const char *opt_items[] = {"settings", "lorem", "ipsum", NULL};
Menu menus[] = {
{2, "file", 'f', file_items},
{7, "edit", 'e', edit_items},
{12, "options", 'o', opt_items},
};
int num_menus = sizeof(menus) / sizeof(Menu);
draw_menus(win, menus, num_menus);
wrefresh(win);
char ch;
int active_index = -1;
while ((ch = wgetch(win))) {
unhighlight_all(win, menus, num_menus);
for (int i = 0; i < num_menus; i++) {
if (ch == menus[i].trigger) {
highlight_menu(win, menus[i]);
clear_submenu_area(win);
active_index = i;
}
if (ch == ' ') {
if (active_index != -1) {
show_submenu(win, menus[active_index]);
wrefresh(win);
}
}
}
}
wrefresh(win);
endwin();
return 0;
}
Note the const char *...items
:
these are how you would do an array of strings in c.
Note bool submenu_open
:
menu.h
#ifndef MENU_H
#define MENU_H
#include <curses.h>
// top level menu label
typedef struct Menu {
int start_x;
const char *text;
char trigger;
const char **submenu;
} Menu;
typedef struct MenuItem {
const char *label;
int x;
} MenuItem;
void draw_menus(WINDOW *win, Menu menus[], int count);
void highlight_menu(WINDOW *win, Menu menu);
void unhighlight_all(WINDOW *win, Menu menus[], int count);
#endif // !_MENU_H_
Note how the structs are defined with typedef struct ... { ... }
. These are for how you would structure your memory.
menu.c
#include "menu.h"
#include <curses.h>
void draw_menus(WINDOW *win, Menu menus[], int count) {
for (int i = 0; i < count; i++) {
mvwprintw(win, 0, menus[i].start_x, "%s", menus[i].text);
}
}
void highlight_menu(WINDOW *win, Menu menu) {
wattron(win, A_STANDOUT);
mvwprintw(win, 0, menu.start_x, "%s", menu.text);
wattroff(win, A_STANDOUT);
}
void unhighlight_all(WINDOW *win, Menu menus[], int count) {
for (int i = 0; i < count; i++) {
mvwprintw(win, 0, menus[i].start_x, "%s", menus[i].text);
}
}
Take a look at the wattron
and wattroff
functions. Short for Window Attributes. This acts as a toggle.
submenu.h
#ifndef SUBMENU_h
#define SUBMENU_h
#include "menu.h"
#include <curses.h>
void show_submenu(WINDOW *win, Menu menu);
void clear_submenu_area(WINDOW *win);
#endif
This is here so that functions from submenu.c
can be called from different files, as long as they #include "submenu.h"
.
submenu.c
#include "submenu.h"
#include "menu.h"
#include <curses.h>
void show_submenu(WINDOW *win, Menu menu) {
int y = 1;
for (int i = 0; menu.submenu[i] != NULL; i++) {
mvwprintw(win, y++, menu.start_x, "--> %s", menu.submenu[i]);
}
}
void clear_submenu_area(WINDOW *win) {
int width = getmaxx(win);
for (int i = 1; i <= 5; i++) {
mvwprintw(win, i, 1, "%*s", width - 2, " ");
}
}
How to use the program:
- Press f for file, e for edit and o for options to highlight that menu item.
- Press the space key to display submenus.
- Press the menu item key again to hide the submenu.
ncurses Routines in Rapid Succession
Chapter 13: chgat
, mvchgat
, wchgat
, mvwchgat
chgat
style functions modify the attributes of a sequence of characters already printed on the screen, without reprinting them.
- Fast and visually smooth for highlighting and toggling styles without redrawing full lines.
Common Signature:
chgat(int n, attr_t attr, short color_pair, const void *opts);
n
: Number of characters to affect. Use-1
to go to end of line.attr
: Attribute flags likeA_BOLD
,A_REVERSE
, etc.color_pair
: The index of the color pair (from init_pair).opts
: AlwaysNULL
. Reserved for future cursed extensions.
The Family Tree:
Function | Context | Cursor Affects? | Uses a WINDOW ? | Coordinates? |
---|---|---|---|---|
chgat() | stdscr | Yes | No | Current pos |
mvchgat() | stdscr | Yes | No | Moves cursor |
wchgat() | custom window | Yes | Yes | Current pos |
mvwchgat() | custom window | Yes | Yes | Moves cursor |
💡 Practical Differences 🧼 chgat()
- Applies to the default window (stdscr)
- Starts at the current cursor position
- Doesn’t move the cursor, just modifies stuff starting from it
🚶 mvchgat(y, x, ...)
- Moves the cursor to (y, x) on stdscr
- Then applies the attribute change from there
- Returns the cursor to the position after the change
🪟 wchgat(win, ...)
- Applies to a specific WINDOW
- Uses the window's cursor position
- Use this when you’ve made custom windows (menus, popups, etc.)
🧭 mvwchgat(win, y, x, ...)
- Moves cursor to (y, x) within a specific window
- Applies changes from there
Highlighting a section of text:
mvprintw(0, 0, "This is a test line.");
mvchgat(0, 10, 4, A_REVERSE, 1, NULL); // highlights "test"
Using it in a custom window:
WINDOW *win = newwin(10, 30, 5, 5);
mvwprintw(win, 1, 2, "Item 1 Item 2 Item 3");
mvwchgat(win, 1, 2, 6, A_STANDOUT, 2, NULL); // highlights "Item 1"
Gotchas:
- These functions don’t change what’s written, only how it looks.
- If you overwrite the text afterward, the attributes are lost unless reapplied.
- You must call
refresh()
(orwrefresh()
) afterward to actually see the effect.
Chapter 14: clrtoeol
/ clrtobot
+ erase()
/ refresh()
The two terminal janitors:
clrtoeol()
cleans from the cursor to the end of the current line.clrtobot()
wipes everything from the cursor down to the bottom of the window, including the current line.
erase()
and clear()
may seem similar on the surface, but underneath, they are a little bit different.
erase()
: Clears the window's contents in memory, but does not force an immediate redraw of the screen.
- Works silently with your next
refresh()
orwrefresh()
. - Ideal for partial updates, flickerless redraws.
erase(); // marks the stdscr for clearing
refresh(); // now it actually shows the cleared screen
clear()
: Same as erase()
, but also tells curses to clear the physical screen completely on the next refresh()
.
- The terminal acts as if it was just launched.
- Can cause flickering
- Good for a fresh, guaranteed blank state.
clear(); // like erase() + "I said CLEAR DAMN IT"
refresh();
#include <ncurses.h>
int main(int argc, char **argv) {
initscr();
noecho();
refresh();
printw("Hello");
mvprintw(1, 0, "PwatPwat");
move(0, 1);
getch();
// clear routines
// to the end of line
clrtoeol();
getch();
mvprintw(2, 0, "To clean");
mvprintw(3, 0, "To clean");
mvprintw(5, 0, "To clean");
mvprintw(10, 0, "To clean");
move(2, 5);
getch();
// to the bottom
clrtobot();
getch();
// erase(); soft clear
// clear(); definitely clear
getch();
endwin();
return 0;
}
Chapter 15: leaveok
/ immedok
/ scrollok
leaveok(win, TRUE)
- Don't care where the cursor is, just leave it wherever.
- Prevents
ncurses
from moving the hardware cursor. - Skips cursor repositioning.
- It's fast.
immedok(win, TRUE)
- Every time I change the window, update it immediately.
- Forces
wrefresh(win)
automatically after everywaddch()
,mvwprintw()
, etc. - Slower, but more dynamic and automatic.
scrollok(win, TRUE)
- If we hit the bottom, just scroll the window down.
- Enables automatic scrolling when adding text past the bottom line.
- Useful for chat logs, terminal output windows, logs, etc.
- Requires enough space (and usually
idlok()
too for smooth scrolling).
#include <ncurses.h>
int main(int argc, char **argv) {
initscr();
noecho();
refresh();
WINDOW *win = newwin(5, 8, 10, 10);
box(win, 0, 0);
/*
// don't really need to know where the cursor is
leaveok(win, true);
wmove(win, 1, 2);
wgetch(win);
*/
/*
// refresh immediately
immedok(win, true);
waddch(win, 'a');
*/
/*
// allow continuous scrolling
scrollok(win, true);
int counter = 0;
while (true) {
chtype ch = (counter % 10) + '0';
waddch(win, ch);
wrefresh(win);
counter++;
}
*/
// clearok(WINDOW *, bool)
getch();
endwin();
return 0;
}
Chapter 16: Background Routines (bkgdset
/ bkgd
/ wbkgdset
/ wbkgd
)
Those are routines to manipulate the background of a named window.
// Background routines
#include <ncurses.h>
int main() {
initscr();
refresh();
clear();
if (!has_colors()) {
return 1;
}
start_color();
init_pair(1, COLOR_BLACK, COLOR_RED);
init_pair(2, COLOR_WHITE, COLOR_BLUE);
// setting attributes for the future
bkgdset(COLOR_PAIR(1));
addch('a');
refresh();
/*
bkgd('a'); fill the background
addch('u');
*/
/*
// bkgd vs wbkgd
bkgd(COLOR_PAIR(1));
refresh();
*/
WINDOW *win = newwin(10, 25, 10, 10);
wbkgdset(win, COLOR_PAIR(2));
wclear(win);
wrefresh(win);
/*
wbkgd(win, COLOR_PAIR(2));
box(win, 0, 0);
wrefresh(win);
*/
getch();
endwin();
return 0;
}
Chapter 17 Free Window memory with delwin()
:
newwin()
allocates memory for a WINDOW structure.delwin()
deallocates that memory, it's likefree()
for windows.
🧼 When to use delwin():
✅ Use it:
- When a window is no longer needed (e.g., after closing a dialog box).
- After calling wclear() and wrefresh() to visually wipe it off the screen.
- In well-structured code where windows are dynamically created and destroyed.
🚫 You can skip it:
- In small toy programs or examples where the window is static and only created once.
- If the program ends right after and the OS will clean up anyway (but that's lazy energy).
// Deleting Window Memory
// Increase security with delwin()
#include <curses.h>
#include <ncurses.h>
int main() {
initscr();
WINDOW *test_win = newwin(10, 25, 0, 0);
box(test_win, 0, 0);
refresh();
wrefresh(test_win);
getch();
wclear(test_win);
wrefresh(test_win);
// ensure all associated memory for WINDOW is deleted
// but, does not erase the visual portion on its own.
// Use wclear(win) and wrefresh(win) first to do so.
delwin(test_win);
refresh();
// wrefresh(test_win); // referencing it after it's been deleted can cause a
// segfault
getch();
endwin();
return 0;
}
More Cool Features
Chapter 18: Define Your Own Color Palettes
init_color(short color_number, short r, short g, short b)
uses values from 0 to 1000, not 255.- Custom colors are global: once you redefine
COLOR_GREEN
, that's what it means from now on.
#include <curses.h>
#include <ncurses.h>
int main() {
initscr();
// check for color support
if (!has_colors() || !can_change_color()) {
printw("Your terminal does not support colors.");
getch();
return 1;
}
start_color();
// Query how many you get
printw("COLORS : %d\n", COLORS);
printw("COLOR_PAIRS : %d\n", COLOR_PAIRS);
// Redefine COLOR_CYAN
init_color(COLOR_CYAN, 1000, 200, 800);
init_pair(1, COLOR_CYAN, COLOR_BLACK);
attron(COLOR_PAIR(1));
printw("Custom color magenta-ish cyan?\n");
attroff(COLOR_PAIR(1));
getch();
endwin();
return 0;
}
Chapter 19: Character Management (delch
, wdelch
, mvdelch
, mvwdelch
)
// routines for character management
#include <ncurses.h>
int main() {
initscr();
noecho();
printw("Hello from the underworld");
getch();
// Deletes a character, defined by move(y, x);
// move(0, 9)
// delch();
// wdelch(WINDOW);
// Achieves the same results as above
mvdelch(0, 9);
// performs within the window
// WINDOW *win = newwin(10, 25, 5, 5);
// box(win, 0, 0);
// refresh();
// wrefresh(win);
//
// mvwprintw(win, 1, 1, "hello this is more text");
// wgetch(win);
// mvwdelch(win, 1, 10);
// wgetch(win);
getch();
endwin();
return 0;
}
Chapter 20: Line Management (insertln
, deleteln
, insdelln
)
// function to add / remove lines
#include <curses.h>
int main() {
initscr();
noecho();
WINDOW *win = newwin(4, 25, 5, 5);
refresh();
wrefresh(win);
mvwprintw(win, 0, 0, "1 Line of Text here");
mvwprintw(win, 1, 0, "2 Line of Text here");
mvwprintw(win, 2, 0, "3 Line of Text here");
mvwprintw(win, 3, 0, "4 Line of Text here");
wmove(win, 1, 0);
wgetch(win);
// inserts a line wherever the cursor it. needs a wmove().
// Moves every other lines -1
// winsertln(win);
// deletes the line, again at the position determined by wmove()
// also moves other lines
wdeleteln(win);
// deletes or inserts lines, determined by the second int argument
// (positive int to add, negative int to remove).
winsdelln(win, -2);
// No more room for lines? the bottom ones gets deleted first.
wgetch(win);
endwin();
return 0;
}
C++ 🤖
Welcome to my C++ notes. Categorized for your sanity and mine.
Personal Story
Maybe it's the challenging aspects of this language that pulls me into it. Maybe it is its rich history; either way I have decided to become overly committed to its complex syntax, and the endless possibilities it offers. It is not my fault that every single crazy project I dream of (plugins for music production, compilers, game engines) all uses C++ for its performance and complete out-of-the-box tooling.
It is definitely a language to use with precaution, a language you could lose yourself into, for better or for worse.
Setting Up C++ on Linux
To get started with C++ development, you can install the essential tools via your package manager. On Ubuntu/Debian-based systems, run:
sudo apt update
sudo apt install build-essential
This installs both the C and C++ compilers (gcc and g++), along with useful build tools like make.
For debugging and memory checking, install:
sudo apt install gdb valgrind
Setting Up Language Server (LSP) for Neovim
If you use Neovim with Mason.nvim, you can install the clangd language server for better code intelligence:
- Open Neovim.
- Run
:Mason
and search forclangd
. - Install
clangd
from Mason's UI.
Updating g++ for New C++ Standards (e.g., C++26)
When a new C++ standard (like C++26) is released and you want to try it:
-
First, check if your distribution’s repositories have the updated g++ version:
sudo apt update sudo apt install g++-13 # replace 13 with the latest version available
-
If the latest version is not available, you might need to use a PPA (on Ubuntu), download from the official GNU Toolchain, or build from source.
-
You can check your g++ version with:
g++ --version
-
When compiling with a new standard, use the appropriate flag (e.g.,
-std=c++2b
or-std=c++26
when it’s officially supported):g++ -std=c++2b main.cpp -o main
🛡️ Debugging Tools for C++ You Should 100% Use
🧼 1. AddressSanitizer (ASan)
-
You already know the girl. She’s protective, loud, and dragging every invalid memory access by its wig.
-
Just compile with:
g++ -fsanitize=address -g -O1 your_code.cpp -o output
🔥 2. Valgrind (The Classic Queen)
-
She’s slower, but ultra-detailed. Great for memory leaks, use-after-free, uninitialized memory reads.
-
Use like:
valgrind ./output
- This flag tells Valgrind to trace the origin of unitialized values.
valgrind --track-origins=yes ./output
Bonus: combine with --leak-check=full
to get all the juicy info.
🧠 3. GDB (The Stoic Therapist)
- Not flashy, but powerful. If you need to pause execution mid-Dijkstra and stare at your pointers like a disappointed parent:
g++ -g your_code.cpp -o output
gdb ./output
Then in GDB:
run
bt # Backtrace on crash
info locals
- Common Commands:
- run – start execution
- break main – set breakpoint
- next, step – step through code
- print var, info locals – inspect variables
- bt – backtrace after crash
🌈 4. Godbolt (Optional, but Very Nerdy)
- Not for runtime bugs, but if you ever want to see how your C++ compiles to assembly, or compare optimizations. It’s fun for algorithm analysis.
🚀 5. Clang-Tidy
-
For static analysis. Lints C++ code and catches suspicious patterns before they explode.
-
Run like:
clang-tidy your_code.cpp -- -std=c++17
Bonus: Compilation Flags
- Always use
-Wall -Wextra -pedantic
for extra compiler warnings.
C++ Cheat Sheet 💀
1. Variables, Data Types & Initialization
C++ has multiple ways to declare variables, and some are better than others:
int x = 10; // Traditional C-style initialization
int y(20); // Constructor-style initialization
int z{30}; // C++11 Uniform Initialization (safer, avoids narrowing conversions)
std::cout << x << " " << y << " " << z << std::endl;
Prefer {} (uniform initialization) because it avoids weird implicit conversions:
double d{4.5};
int i{d}; // ❌ ERROR: Narrowing conversion not allowed!
2. Pointers and References
C++ inherits pointers from C, but adds references to make life slightly easier:
Pointers (Classic C-style)
int a = 10;
int* ptr = &a; // Pointer stores the memory address of 'a'
std::cout << *ptr << std::endl; // Dereference the pointer (output: 10)
References (C++ Feature)
int x = 42;
int& ref = x; // Reference to x (alias)
ref = 50; // Modifies x as well
std::cout << x << std::endl; // Output: 50
Use references instead of pointers when:
- You don’t need null values.
- You don’t want to manually dereference things.
🚨 Pointers are still useful for dynamic memory allocation & function arguments that can be null.
3. Dynamic Memory (Heap Allocation)
C++ lets you manually allocate and deallocate memory with new
and delete
(but later you should use smart pointers).
int* heapInt = new int(99); // Allocate an int on the heap
std::cout << *heapInt << std::endl; // Output: 99
delete heapInt; // Free memory (or you'll have a memory leak)
🚨 Memory Management Rules
- Every new must have a corresponding delete.
- Every new[] (array allocation) must have a corresponding delete[].
int* arr = new int[10]; // Allocating an array
delete[] arr; // Use delete[] for arrays!
In modern C++, prefer std::vector or smart pointers instead of raw new/delete.
4. Functions: Pass by Value, Reference, and Pointer
Pass by Value (Makes a Copy)
void changeValue(int num) {
num = 100; // This only changes the local copy
}
int x = 5;
changeValue(x);
std::cout << x; // Still 5 (copy was modified, not the original)
Pass by Reference (Modifies Original)
void changeRef(int& num) {
num = 100;
}
int x = 5;
changeRef(x);
std::cout << x; // Now it's 100
Pass by Pointer (Also Modifies Original)
void changePointer(int* num) {
*num = 100; // Dereferencing modifies the actual value
}
int x = 5;
changePointer(&x);
std::cout << x; // Output: 100
- Use references when modification is required.
- Pass by value when you want a copy (not for large objects).
- Use pointers when passing optional values (nullable).
5. const
: Read-Only Safety
const
makes sure your variables can’t be modified accidentally.
const int a = 10; // Can’t change a
a = 20; // ❌ ERROR!
const
with Pointers
const int* ptr = &a; // Pointer to constant int (data is immutable)
int* const ptr2 = &a; // Constant pointer (address is immutable)
const int* const ptr3 = &a; // Both data and address are immutable
🚨 If something doesn’t need to change, mark it const
!
(Especially function parameters, so you don’t accidentally modify them.)
6. Structs vs Classes
Structs in C++ are like classes, except their members are public by default.
struct Point {
int x, y;
};
class Point {
public:
int x, y;
};
- Use struct when you just need a simple data container.
- Use class when you need encapsulation, methods, and complex logic.
7. enum
and enum class
C++ has two types of enums, but only one is safe.
Traditional C-style Enum (Unsafe)
enum Color { RED, GREEN, BLUE };
Color c = RED;
🚨 Problem: Color values are just ints, so they can accidentally mix with unrelated values.
C++11 enum class (Scoped and Safer)
enum class Color { RED, GREEN, BLUE };
Color c = Color::RED;
- Prevents name conflicts.
- Doesn’t implicitly convert to int.
8. Input & Output (Stream Handling)
Basic I/O
#include <iostream>
using namespace std;
int main() {
string name;
cout << "Enter your name: ";
cin >> name;
cout << "Hello, " << name << "!" << endl;
}
🚨 Issue: cin >> name; only takes one word.
Reading Full Lines
string fullName;
getline(cin, fullName);
9. Arrays & Vectors
C-Style Array
int arr[5] = {1, 2, 3, 4, 5};
🚨 Problems: No size tracking, no bounds checking.
Modern Alternative: std::vector
#include <vector>
std::vector<int> vec = {1, 2, 3, 4, 5};
vec.push_back(6);
std::cout << vec[2]; // Safe indexing
- Automatically resizes.
- Safer than raw arrays.
10. The auto
Keyword
C++ lets you infer types automatically with auto:
auto num = 42; // Compiler infers int
auto pi = 3.14; // Compiler infers double
auto word = "hello"; // Compiler infers const char*
- Good for readability, especially with long types.
- Avoid using auto everywhere, it reduces code clarity.
Bonus: A Basic Program to Convert Miles to Kilometers
#include <iostream>
#include <limits>
using namespace std;
const double m_to_km = 1.60934;
// Inline function to convert miles to kilometers
inline double convert(double miles) {
return miles * m_to_km;
}
int main() {
double miles = -1;
while (miles != 0) {
cout << "Input distance in miles (0 to quit): ";
cin >> miles;
// Validate input
if (cin.fail()) {
cin.clear(); // Clear the error flag
cin.ignore(numeric_limits<streamsize>::max(), '\n'); // Ignore bad input
cout << "Invalid input. Please enter a number." << endl;
continue;
}
if (miles == 0) break;
cout << miles << " miles is equal to " << convert(miles) << " kilometers." << endl;
}
cout << "Bye!" << endl;
return 0;
}
- cin.fail() must be explicitly checked
- If a user enters non-numeric input, cin enters an error state and stops working.
- Without cin.clear() and cin.ignore(), the program will enter an infinite loop or crash.
- C++ doesn’t do automatic input validation, so you must babysit it manually.
- What inline really does
- inline suggests to the compiler "hey, just copy-paste this function where it's called instead of doing a full function call."
- This is only useful for very small functions (like our convert() function) because function calls have a small overhead.
- Modern compilers already optimize this automatically, so using inline manually is usually unnecessary.
Bonus: A Simple Program to Calculate A Sum
// C to C++ conversion
#include <iostream>
#include <vector>
const int N = 40;
inline void sum(int *p, int n, const std::vector<int>& d) {
*p = 0;
for (int i = 0; i < n; ++i) {
*p = *p + d[i];
}
}
int main() {
int accum = 0;
std::vector<int> data(N);
for (int i = 0; i < N; ++i) {
data[i] = i;
}
sum(&accum, N, data);
std::cout << "Sum is: " << accum << std::endl;
return 0;
}
- Why is const int better than #define?
- It has type safety (#define doesn’t care what type it is).
- It respects C++ scoping rules (macros don’t respect namespaces).
- Debuggers understand const variables, but they don’t track macros.
- Why use int p instead of return?
- This function modifies accum directly in memory via the pointer.
- This mimics how C functions typically modify values passed by reference.
- However, in modern C++, it’s cleaner to use int& p instead of int* p.
- Why pass std::vector
& d by const reference?
- Prevents unnecessary copying of the entire vector.
- const ensures the function can’t modify d accidentally.
- Without const, passing a vector by reference (vector
& d) allows modifications.
- Why std::vector
instead of a plain array?
- No need for malloc/free like in C.
- Dynamic resizing (though we’re not using it here).
- Memory safety: avoids out-of-bounds access if used properly.
- Why do we pass &accum instead of returning a value?
- C++ style would be to return int instead of modifying via pointer.
- This is still very C-like, since we’re explicitly using pointer manipulation.
C++ OOP Cheatsheet 💀
C++ is not a fully object-oriented language like Java—it gives you the choice of using OOP, procedural, or even template-based metaprogramming. However, when you do use OOP, C++ expects you to take responsibility (i.e., manual memory management, virtual destructors, and explicit inheritance rules).
Basic OOP Program
#include <iostream>
#include <string>
class Car {
private:
std::string brand;
int speed;
public:
// Constructor
Car(std::string b, int s) : brand(b), speed(s) {
std::cout << brand << " Car is being created.\n";
}
// Virtual destructor
virtual ~Car() {
std::cout << brand << " Car is being destroyed.\n";
}
// Method
void accelerate() {
speed += 10;
std::cout << brand << " is going " << speed << " km/h.\n";
}
// Getter for brand
std::string getBrand() const {
return brand;
}
// Getter for speed
int getSpeed() const {
return speed;
}
// Operator overloading
friend std::ostream& operator<<(std::ostream& os, const Car& c) {
os << c.brand << " at " << c.speed << " km/h";
return os;
}
};
// Single inheritance
class SportsCar : public Car {
public:
SportsCar(std::string b, int s) : Car(b, s) {
std::cout << b << " SportsCar is being created.\n";
}
void turboBoost() {
std::cout << "Boosting the " << getBrand() << "!\n";
}
// Destructor
~SportsCar() {
std::cout << getBrand() << " SportsCar is being destroyed.\n";
}
};
int main() {
Car myCar("beetle", 50);
myCar.accelerate();
myCar.accelerate();
SportsCar mySportsCar("Ferrari", 100);
mySportsCar.accelerate();
mySportsCar.turboBoost();
// Using the overloaded << operator to print Car objects
std::cout << myCar << std::endl;
std::cout << mySportsCar << std::endl;
return 0;
}
Basic Class Definition: Car
Key Takeaways
- Encapsulation
- brand and speed are private, meaning they cannot be accessed outside of the class.
- Access is provided through getter functions (getBrand(), getSpeed()).
- Constructors & Initialization Lists
- Car(std::string b, int s) : brand(b), speed(s) {} → This is a constructor with an initializer list.
- Instead of assigning values inside {} manually, we use direct initialization, which is faster and cleaner.
- Virtual Destructor (~Car())
- Why virtual? So that if you delete a SportsCar through a Car*, it calls the correct destructor.
- Without virtual, only Car's destructor would run, and SportsCar's destructor wouldn’t execute.
- Operator Overloading (<<)
- We define friend std::ostream& operator<<(...) to allow std::cout << myCar.
- Friend functions allow non-member functions to access private class members.
Inheritance: SportsCar Extends Car
Key Takeaways
- Single Inheritance (class SportsCar : public Car)
- SportsCar inherits all properties of Car but can add new behavior (turboBoost()).
- SportsCar(std::string b, int s) : Car(b, s) calls the base class (Car) constructor.
- Destructor Handling
- Why is the destructor not virtual? Since Car already has a virtual destructor, it ensures that deleting a SportsCar object correctly calls both destructors.
- Destruction order: SportsCar's destructor runs first, then Car's.
The main() Function
Key Takeaways
- Automatic Object Lifetime Management
- myCar and mySportsCar are stack-allocated.
- No need for delete because destructors run automatically when they go out of scope.
- Calling Methods on Objects
- myCar.accelerate(); increases the speed by 10.
- mySportsCar.turboBoost(); is only available in SportsCar.
- Overloaded << Operator Works as Expected
std::cout << myCar << std::endl;
→ Calls the overloaded << function.
OOP with Parameterized Constructors & Operator Overloading in C++
This program introduces parameterized constructors and expands on operator overloading, reinforcing core C++ OOP concepts. While it follows the same fundamental principles as the Car example, the use of constructors and the + operator overload adds complexity.
#include <iostream>
using namespace std;
class Point {
private:
double x, y;
public:
// Default constructor
Point() : x(0.0), y(0.0) {}
// Parameterized constructor
Point(double xVal, double yVal) : x(xVal), y(yVal) {}
// Getter for x
double getX() const {
return x;
}
// Setter for x
void setX(double v) {
x = v;
}
// Getter for y
double getY() const {
return y;
}
// Setter for y
void setY(double v) {
y = v;
}
// Overload the + operator
Point operator+ (const Point& p) const {
return Point(x + p.x, y + p.y);
}
// Overload the << operator for output
friend ostream& operator << (ostream& out, const Point& p) {
out << "(" << p.x << ", " << p.y << ")";
return out;
}
};
int main() {
Point a(3.5, 2.5), b(2.5, 4.5), c;
cout << "a = " << a << " b = " << b << endl;
c = a + b;
cout << "sum = " << c << endl;
return 0;
}
Class Definition: Point
Key Takeaways
Point() : x(0.0), y(0.0) {}
- Default Constructor (Point())
- This constructor initializes x and y to 0.0 by default.
- Why use an initializer list (: x(0.0), y(0.0)) instead of assignment in {}?
- Faster: Directly initializes members instead of assigning values later.
- Mandatory for const or reference members (not in this example, but useful elsewhere).
- Parameterized Constructor (Point(double xVal, double yVal))
Point(double xVal, double yVal) : x(xVal), y(yVal) {}
Allows creating a Point object with custom values.
- Getter & Setter Methods
double getX() const { return x; }
void setX(double v) { x = v; }
- const after getX() → Ensures getX() does not modify the object.
- Setters allow modification, while getters ensure read-only access.
- Operator Overloading
- Overloading the + Operator
Point operator+ (const Point& p) const {
return Point(x + p.x, y + p.y);
}
- Allows adding Point objects like a + b.
- Why return a Point object?
- Instead of modifying
this
, it creates a new Point.
- Instead of modifying
Example:
c = a + b;
Equivalent to c = Point(a.getX() + b.getX(), a.getY() + b.getY());
- Overloading the << Operator (Friend Function)
friend ostream& operator << (ostream& out, const Point& p) {
out << "(" << p.x << ", " << p.y << ")";
return out;
}
Allows printing Point objects using:
cout << a; // Outputs (3.5, 2.5)
Why is operator<< a friend function?
- It needs access to private members (x and y).
- Cannot be a member function because the first parameter must be ostream&.
The main() Function
int main() {
Point a(3.5, 2.5), b(2.5, 4.5), c;
cout << "a = " << a << " b = " << b << endl;
c = a + b;
cout << "sum = " << c << endl;
return 0;
}
- What Happens Here?
- Objects are created:
Point a(3.5, 2.5), b(2.5, 4.5), c;
- a is initialized with (3.5, 2.5), b with (2.5, 4.5), and c is (0.0, 0.0) by default.
Operator overloading is used:
c = a + b;
-
Triggers operator+() and stores the result in c.
-
Objects are printed using operator<<:
cout << "sum = " << c << endl;
- Calls operator<<() to format the output.
Output:
a = (3.5, 2.5) b = (2.5, 4.5)
sum = (6, 7)
Deep Copy Constructor
C++ has a doubly linked list in the std library, but you should know how they are implemented under the hood.
Why Do We Need a Deep Copy Constructor?
By default, when you copy an object in C++, the compiler performs a shallow copy, meaning:
- It copies the memory addresses instead of duplicating the data itself.
- If the copied object modifies the data, the original object also changes because they share the same memory.
- When one object is destroyed, the other might point to an invalid memory location (dangling pointers).
To avoid this nightmare, we manually implement a deep copy constructor. This ensures:
- Each object gets its own separate copy of the data.
- Deleting one object doesn’t affect the others.
- No accidental shared memory corruption.
#include <iostream>
using namespace std;
class list_element {
public:
int d;
list_element* next;
list_element(int n = 0, list_element* ptr = nullptr) : d(n), next(ptr) {}
};
class list {
public:
list() : head(nullptr), cursor(nullptr) {}
~list(); // Destructor to free memory
list(const list& lst); // Copy constructor
void prepend(int n); // Insert at front value n
int get_element() { return cursor->d; }
void advanced() { cursor = cursor->next; }
void print();
private:
list_element* head;
list_element* cursor;
};
// Destructor implementation
list::~list() {
while (head != nullptr) {
list_element* temp = head;
head = head->next;
delete temp;
}
}
// Deep copy constructor
list::list(const list& lst) {
if (lst.head == nullptr) {
head = nullptr;
cursor = nullptr;
} else {
cursor = lst.head;
list_element* h = new list_element(cursor->d);
list_element* previous = h;
head = h;
cursor = cursor->next;
while (cursor != nullptr) {
h = new list_element(cursor->d);
previous->next = h;
previous = h;
cursor = cursor->next;
}
cursor = head;
}
}
void list::prepend(int n) {
if (head == nullptr) // empty list case
cursor = head = new list_element(n, head);
else // add to front-chain
head = new list_element(n, head);
}
void list::print() {
list_element* h = head;
while (h != nullptr) {
cout << h->d << ", ";
h = h->next;
}
cout << "###" << endl;
}
int main() {
list a, b;
a.prepend(9); a.prepend(8);
cout << "list a" << endl;
a.print();
// Use the copy constructor
list c = a;
cout << "list c (copy of a)" << endl;
c.print();
for (int i = 0; i < 40; ++i)
b.prepend(i * i);
cout << "list b" << endl;
b.print();
return 0;
}
Class Definition
#include <iostream>
using namespace std;
class list_element {
public:
int d;
list_element* next;
list_element(int n = 0, list_element* ptr = nullptr) : d(n), next(ptr) {}
};
Each list_element stores:
- d (data).
- next (pointer to the next element).
The list
class
class list {
public:
list() : head(nullptr), cursor(nullptr) {}
~list(); // Destructor to free memory
list(const list& lst); // Copy constructor
void prepend(int n); // Insert at front value n
int get_element() { return cursor->d; }
void advanced() { cursor = cursor->next; }
void print();
private:
list_element* head;
list_element* cursor;
};
- Encapsulates a linked list.
- Implements deep copy via list(const list& lst).
- Destructor (~list()) manually frees memory.
Destructor: Preventing Memory leaks
list::~list() {
while (head != nullptr) {
list_element* temp = head;
head = head->next;
delete temp; // Free each element
}
}
- Ensures no memory leaks when an object is destroyed.
- Deletes every node one by one.
- Without this, dynamically allocated list_elements would never be freed.
The Deep Copy Constructor
list::list(const list& lst) {
if (lst.head == nullptr) {
head = nullptr;
cursor = nullptr;
} else {
cursor = lst.head;
list_element* h = new list_element(cursor->d); // Create first element
list_element* previous = h;
head = h;
cursor = cursor->next;
while (cursor != nullptr) {
h = new list_element(cursor->d); // Deep copy each node
previous->next = h;
previous = h;
cursor = cursor->next;
}
cursor = head;
}
}
How It Works
- Creates a new list_element for each node in the original list.
- Does NOT reuse pointers from the old list.
- Ensures the new object is an independent copy. What Happens If We Didn’t Do This?
list c = a; // Calls copy constructor
- Without a deep copy, c would just reuse a's pointers.
- Modifying c would modify a, which is unintended behavior.
- Deleting c would leave a with dangling pointers, causing undefined behavior.
Other Methods
- Prepend (Insert at Front)
void list::prepend(int n) {
if (head == nullptr) // Empty list case
cursor = head = new list_element(n, head);
else // Add new element at the front
head = new list_element(n, head);
}
-
Creates a new list_element and links it to the front of the list.
-
If the list is empty, initializes cursor.
-
Print the List
void list::print() {
list_element* h = head;
while (h != nullptr) {
cout << h->d << ", ";
h = h->next;
}
cout << "###" << endl;
}
-
Iterates over the list and prints each value.
-
Stops when nullptr is reached.
-
The main() Function
int main() {
list a, b;
a.prepend(9); a.prepend(8);
cout << "list a" << endl;
a.print();
// Use the copy constructor
list c = a;
cout << "list c (copy of a)" << endl;
c.print();
for (int i = 0; i < 40; ++i)
b.prepend(i * i);
cout << "list b" << endl;
b.print();
return 0;
}
- Creates multiple lists (a, b, c).
- Copies a into c using the deep copy constructor.
- Adds elements to b and prints everything.
Expected Output
list a
8, 9, ###
list c (copy of a)
8, 9, ###
list b
1521, 1444, 1369, ..., 0, ###
Why is list c identical to list a?
- Because it was deep copied, not shallow copied.
- Modifying c will not affect a, proving that they are independent lists.
C++ Rule of Three (or Five)
If a class manages dynamic memory, you MUST define these manually:
- Copy Constructor (list(const list&))
- Copy Assignment Operator (operator=)
- Destructor (~list())
- (Optional) Move Constructor (list(list&&))
- (Optional) Move Assignment Operator (operator=(list&&))
Without these, you get shallow copies, which can lead to:
- Memory leaks
- Double deletion errors
- Undefined behavior
C++ Inheritance and Derived Classes
The Inheritance Mechanism
- Allows deriving new classes from existing base classes.
- Reuses existing code, avoiding tedious and error-prone duplication.
- Derived classes extend or alter base class functionality.
- Creates a hierarchy of related types sharing code and interface.
Base Class: student
#include <cstring>
#include <iostream>
class student {
public:
enum year { fresh, soph, junior, senior, grad };
student(char *nm, int id, double g, year x);
void print() const;
protected:
int student_id;
double gpa;
year y;
char name[30];
};
Derived Class: grad_student
class grad_student : public student {
public:
enum support { ta, ra, fellowship, other };
grad_student(char *nm, int id, double g, year x, support t, char *d, char *th);
void print() const;
protected:
support s;
char dept[10];
char thesis[80];
};
Constructors
student::student(char *nm, int id, double g, year x)
: student_id(id), gpa(g), y(x) {
strcpy(name, nm);
}
grad_student::grad_student(char *nm, int id, double g, year x, support t,
char *d, char *th)
: student(nm, id, g, x), s(t) {
strcpy(dept, d);
strcpy(thesis, th);
}
grad_student
constructor invokes the basestudent
constructor.- Base class constructed first.
student_id
andgpa
areprotected
, so accessible to derived class.
Print Functions
void student::print() const {
std::cout << name << " , " << student_id << " , " << y << " , " << gpa << std::endl;
}
void grad_student::print() const {
student::print();
std::cout << dept << " , " << s << std::endl << thesis << std::endl;
}
grad_student::print
reusesstudent::print
and adds extra info.
main()
Function
int main() {
student s("Mae Pohl", 100, 3.425, student::fresh), *ps = &s;
grad_student gs("Morris Pohl", 200, 3.2564, student::grad, grad_student::ta,
"Pharmacy", "Retail Pharmacies"), *pgs;
ps->print(); // student::print
ps = pgs = &gs;
ps->print(); // still student::print due to pointer type
pgs->print(); // grad_student::print
}
- Demonstrates polymorphism via pointer to base class.
ps
points to bothstudent
andgrad_student
objects.- Without
virtual
, the base class'sprint()
is called. pgs
calls the derived version because it's typed asgrad_student*
.
Benefits Recap
- Reuse of tested code.
- Reflects domain relationships.
- Allows treating derived types as base types.
- Simpler, more maintainable code.
Reminder: Mark that print()
function as virtual
if you want actual polymorphism, otherwise it’s just pretending like your last situationship.
🧙♀️ C++ Class Inheritance & Polymorphism – With a 3D Geometry Twist
🧱 Base Class – Point2D
class Point2D {
protected:
float x, y;
public:
Point2D(float x = 0, float y = 0) : x(x), y(y) {}
virtual void print() const {
std::cout << "Point2D(" << x << ", " << y << ")" << std::endl;
}
virtual ~Point2D() {} // Always virtual destructor for polymorphic base
};
protected
: accessible to subclasses, but hidden from the outside world like a locked diary.virtual
: magic keyword that enables polymorphism (late binding).- Destructor is virtual so your objects don’t leave memory corpses behind. 👻
🌌 Subclass – Point3D
class Point3D : public Point2D {
float z;
public:
Point3D(float x = 0, float y = 0, float z = 0) : Point2D(x, y), z(z) {}
void print() const override {
std::cout << "Point3D(" << x << ", " << y << ", " << z << ")" << std::endl;
}
};
public
inheritance: “yes, I’m extending the public interface, not hiding it.”override
: optional, but makes the compiler scream if you mess up a virtual override (bless her).
🧪 Polymorphism in Action
void describePoint(const Point2D& p) {
p.print();
}
int main() {
Point2D p2(1, 2);
Point3D p3(3, 4, 5);
describePoint(p2); // prints Point2D(1, 2)
describePoint(p3); // prints Point3D(3, 4, 5) – polymorphism magic!
}
- This is the power move: a
Point2D
reference holds aPoint3D
object, but still calls the right method. - No casting. No mess. Just vibes. 🎩✨
💀 What If You Forget virtual
?
If you remove virtual
from Point2D::print()
, then the method won’t get overridden at runtime — you’ll always call the base version. This is what we call... a tragic plot twist.
🔮 When to Use This
Use Case | Inheritance? |
---|---|
You need multiple related types that behave differently | Yes |
You want to generalize with base class pointers or references | Yes |
You’re sharing behavior across unrelated classes | No, use composition/templates |
You don’t want to deal with destructor landmines | Use smart pointers, queen |
C++ Virtual Function Behavior
Virtual Function Selection
- Base classes typically define a
virtual
function. - Derived classes override these functions.
- Pointer to base can point at base or derived class objects.
- Function selected depends on object type, not pointer type.
- If no derived override exists, the base class function is used.
Virtual & Overloaded Function Selection
- Overloaded member functions are selected at compile-time based on signature.
- They can have different return types.
- Once a function is declared
virtual
, it stays virtual in derived classes. - The
virtual
keyword is not required in the redefinition, but it's clearer if included.
Example Code
#include <iostream>
#include <ostream>
class B {
public:
int i;
virtual void print_i() const { std::cout << i << " inside B" << std::endl; }
};
class D : public B {
public:
void print_i() const { std::cout << i << " inside D" << std::endl; }
};
int main() {
B b;
B *pb = &b; // point at a B object
D f;
f.i = 1 + (b.i = 1);
pb->print_i(); // Call B::print_i()
pb = &f; // point at D object
pb->print_i(); // Call D::print_i()
}
Output
1 inside B
2 inside D
Analysis
- First
pb->print_i()
callsB::print_i()
becausepb
points to aB
object. - Second
pb->print_i()
callsD::print_i()
becausepb
now points to aD
object. - Function selection happens dynamically at runtime based on the actual object.
Object-Oriented Principles
- Abstract Data Types (ADTs), inheritance, and polymorphism allow treating different class objects through a common interface.
- Virtual functions enable run-time method resolution—true polymorphism.
- This dynamic dispatch is at the heart of OOP.
📐 C++ Shapes with OOP
🧠 Concept
In object-oriented design, shapes are abstractions made up of points and behaviors (like calculating area, or drawing). By using inheritance and polymorphism, we create a base Shape
class and specialize it into specific geometric forms like Triangle
, Circle
, etc.
💡 Why Use Inheritance Here?
- Shared interface via base class:
Shape
defines what a shape can do. - Specialization via subclasses: Each shape has its own way to draw and compute area.
- Allows dynamic polymorphism: store and manipulate shapes through base class pointers (
Shape*
), but still get the actual shape behavior.
🧱 Base Class: Shape
class Shape {
public:
virtual void draw() const = 0;
virtual float area() const = 0;
virtual ~Shape() {}
};
- Declares pure virtual methods = abstract class.
- No objects of
Shape
directly; it’s just a blueprint. - Destructor is virtual to ensure correct cleanup when using base pointers.
🔺 Triangle
- Defined by 3 points.
- Area is computed using Heron's formula.
- Inherits from
Shape
.
🔵 Circle
- Defined by center + radius.
- Area is
πr²
. - Also overrides
draw()
andarea()
.
🧪 Polymorphic Usage
std::vector<Shape*> scene;
scene.push_back(new Triangle(...));
scene.push_back(new Circle(...));
for (Shape* s : scene) {
s->draw();
std::cout << s->area();
}
This allows us to treat all shapes uniformly while letting each class decide how to behave. That’s real polymorphism energy.
Full code
#include <iostream>
#include <math.h>
#include <vector>
struct Point2D {
float x, y;
};
// virtual forces subclasses to implement
class Shape {
public:
virtual void draw() const = 0;
virtual float area() const = 0;
virtual ~Shape() {}
};
class Triangle : public Shape {
Point2D a, b, c;
float distance(const Point2D &p1, const Point2D &p2) const {
return std::hypot(p2.x - p1.x, p2.y - p1.y);
}
public:
Triangle(Point2D a, Point2D b, Point2D c) : a(a), b(b), c(c) {}
void draw() const override {
std::cout << "Drawing triangle between A, B, C.\n";
}
float area() const override {
// Heron’s formula because we fancy
float ab = distance(a, b);
float bc = distance(b, c);
float ca = distance(c, a);
float s = (ab + bc + ca) / 2;
return std::sqrt(s * (s - ab) * (s - bc) * (s - ca));
}
};
class Circle : public Shape {
Point2D center;
float radius;
public:
Circle(Point2D c, float r) : center(c), radius(r) {}
void draw() const override {
std::cout << "Drawing circle at (" << center.x << ", " << center.y
<< ") with radius " << radius << "\n";
}
float area() const override { return M_PI * radius * radius; }
};
int main() {
std::vector<Shape *> scene;
scene.push_back(new Triangle({0, 0}, {1, 0}, {0, 1}));
scene.push_back(new Circle({0, 0}, 5));
for (Shape *shape : scene) {
shape->draw();
std::cout << "Area: " << shape->area() << "\n";
}
// free memory
for (Shape *shape : scene)
delete shape;
}
Move semantics
-
In C++11 there is a sequential container class defined in
<array>
that specifies at compile time a fixed length array. -
Vectors: expandable. Has more features. But not without a cost.
-
Go back to basic C arrays? Discipline yourself to not get off by one errors, memory leaks and etc...
-
Use
std::array
! Maintain efficiency of raw arrays, but has now added functionality. -
It supports move semantics (And RAII): we will learn about those features.
template <class T, int n>
class my_container {
public:
my_container() { a = new T[n]; };
~my_container() { delete[] a; };
private:
T *a;
};
- However this code requires manual memory cleanup, uses raw pointers and so a lot to worry about. By using
std::array
you can get rid of those problems.
Some further constructions
template <class T, int n>
class my_container {
public:
my_container() { a = new T[n]; };
~my_container() { delete[] a; };
explicit my_container(T *b) : my_container() {
for (int i = 0; i < n; ++i)
a[i] = b[i];
}
explicit
suppresses automatic coercion.- Delegate construction and enhance code reuse.
my_container(const my_container &b) : my_container() {
for (int i = 0; i < n; ++i)
a[i] = b.a[i];
}
- Ordinary copy constructor - again with constructor delegation.
private:
T *a;
};
Typical constructors for a class:
- Default : void signature
- Conversion constructor: single argument (we use explicit)
- Copy constructor:
const class Type&
Move constructor
my_container(my_container &&b) noexcept {
a = b.a;
b.a = nullptr;
}
Contents are "moved" not copied.
noexcept
: I don't do anything that will create an exception.- Shallow copy : b disappears.
- This operation doesn't slow down even with a large number of datasets. Constant time algorithm.
- Use of
&&
to mean anrvalue
address.
You might want to overload the = operator:
my_container &operator=(my_container &&b) noexcept {
a = b.a;
b.a = nullptr;
return *this;
}
std::move() static_cast<T&&>(t)
- destructive assignment- More efficient because all the assignments are referential.
Efficient swap with move semantics
We know that my_container temp won't be reused later, so it can disappear. Perfect use for move semantics.
void swap(my_container &b) {
my_container temp = std::move(b);
b = std::move(*this);
*this = std::move(temp);
}
🧪 PART 2: The Deeper Arcana of Move Semantics
💫 1. std::move
— The Blessing of Transfer
Despite the name, std::move
does not move anything.
What it really does is cast a spell that tells the compiler:
“Hey, I solemnly swear I won't use this object anymore. You can steal its soul now.”
So instead of:
T a = b; // Copy
You go:
T a = std::move(b); // Move. b is now a shell of its former self.
🔮 Without std::move
, your move constructor never even gets called. Because C++ won’t move something unless you explicitly say "I’m done with it."
🦴 2. std::unique_ptr<T[]>
— The Sacred RAII Relic
You're tired of writing this, aren’t you?
a = new T[n];
delete[] a;
It’s giving ✨trauma✨.
Instead, you do:
#include <memory>
std::unique_ptr<T[]> a;
a = std::make_unique<T[]>(n);
- Automatically deletes the memory when the object goes out of scope.
- You can still move it, but you can’t copy—because it’s unique, like your trauma and your playlist.
Move constructor becomes dead simple:
my_container(my_container&& other) noexcept = default;
my_container& operator=(my_container&& other) noexcept = default;
Let the STL do the heavy lifting while you sip iced coffee like a grown-up.
🧼 3. std::exchange
— The Elegant Memory Swap Spell
Instead of this clunky mess:
a = b.a;
b.a = nullptr;
You say:
a = std::exchange(b.a, nullptr);
Which means:
"Set
a
tob.a
, and resetb.a
tonullptr
, all in one sexy move."
Perfect for move constructors and move assignment.
🧝♀️ 4. Pro-Level Move Assignment: The Real Grownup Version
my_container& operator=(my_container&& other) noexcept {
if (this != &other) {
delete[] a;
a = std::exchange(other.a, nullptr);
}
return *this;
}
- Safe
- Clean
- Self-assignment-proof
- Doesn’t leak
- Doesn’t double-free
This is C++ that smells like sandalwood, not burnt malloc.
🏁 What Your Glow-Up Class Might Look Like
template <typename T, int n>
class my_container {
public:
my_container() : a(std::make_unique<T[]>(n)) {}
explicit my_container(const T* b) : my_container() {
std::copy(b, b + n, a.get());
}
my_container(const my_container& other) : my_container() {
std::copy(other.a.get(), other.a.get() + n, a.get());
}
my_container& operator=(const my_container& other) {
if (this != &other) {
std::copy(other.a.get(), other.a.get() + n, a.get());
}
return *this;
}
// Move semantics: default is enough when using unique_ptr
my_container(my_container&&) noexcept = default;
my_container& operator=(my_container&&) noexcept = default;
void swap(my_container& other) noexcept {
std::swap(a, other.a);
}
private:
std::unique_ptr<T[]> a;
};
Just look at her. She doesn’t even need a destructor anymore. She’s efficient. She’s safe. She’s modern.
👑 TL;DR – The Mystic Moves
Concept | Incantation | What It Does |
---|---|---|
std::move(obj) | "Take this, I’m done with it." | Enables move constructor/assign |
std::unique_ptr | "I will own this alone." | RAII for heap memory |
std::exchange() | "Take this, and replace it with that." | Swaps a value and returns old |
= default | "I trust the STL gods to handle this." | Autogenerate move/copy properly |
You want me to walk you through adapting your current class with unique_ptr
line by line next? Or maybe go further into move-only types and real STL container internals? Just say the magic word, sorceress 🧙♀️✨
Introduction to Templates in C++ 💀
Templates are one of C++’s most powerful and frightening features. They let you write generic code that can work with multiple data types without duplication. But they also have the potential to turn your code into an unreadable mess of cryptic compiler errors if misused.
In these examples, we’ll look at two ways templates are used:
- Function Templates – Making a single function that works with multiple types.
- Class Templates – Making a class that can handle multiple data types dynamically.
// Function templates: subtract()
#include <iostream>
#include <vector>
const int N = 40;
template <typename T>
T substract(const T data[], int n, T s = 0) {
for (int i = 0; i < n; ++i) {
s = s - data[i];
}
return s;
}
int main() {
std::cout << "template for substract()" << std::endl;
int a[] = {1,2,3};
double b[] = {2.1, 2.2, 2.3};
std::cout << substract(a, 3) << std::endl;
std::cout << substract(b, 3) << std::endl;
return 0;
}
Key Takeaways
- template
– This is the template declaration.
- T is a placeholder for any data type (int, double, float, etc.).
- When calling substract(), the compiler automatically replaces T with the correct type.
- Function works for multiple data types
- substract(a, 3) works for int.
- substract(b, 3) works for double.
- Why templates instead of function overloading?
- Without templates, you’d have to write separate functions for int, double, float, etc.
- Templates let you write the function once and use it for any compatible type.
- Is this just like inline?
- No, but function templates can be inlined by the compiler if they are small.
- The compiler generates a separate function for each unique type used.
- This means substract
and substract are compiled separately.
Templates Part Two
// Class Templates: Summable<T>
#include <iostream>
template <class T>
class Summable {
public:
T sum(const T data[], int size, T s = 0) {
for (int i = 0; i < size; ++i) {
s += data[i];
}
return s;
}
};
int main() {
Summable<int> intSummable;
int intData[] = {1, 2, 3, 4, 5};
int intSum = intSummable.sum(intData, 5);
std::cout << "Sum of int array: " << intSum << std::endl;
Summable<double> doubleSummable;
double doubleData[] = {1.1, 2.2, 3.3, 4.4, 5.5};
double doubleSum = doubleSummable.sum(doubleData, 5);
std::cout << "Sum of double array: " << doubleSum << std::endl;
return 0;
}
Key Takeaways
- template
makes Summable a generic class
- This class works with any type that supports the += operator.
- We create Summable
and Summable instances separately.
- Why use a class template instead of a function template?
- If we only needed a single sum() function, a function template is fine.
- But if we wanted to add more operations (like multiplication, average, etc.), then a class template organizes everything better.
- How does the compiler handle this?
- When you write Summable
, the compiler generates an int-specific version of the class. - When you write Summable
, the compiler generates a separate double version.
Functional Objects in STL Algorithms
- It's useful to have function objects to further leverage the STL library
- Numerical functions have built-in meaning using + or *, as well as user provided binary operators which could be passed in.
- Functors like
std::plus
,std::minus
,std::multiplies
, etc. are part of<functional>
. std::accumulate
takes a starting value and a binary operation to process each element.- You can plug in your own class with
operator()
to accumulate however you want. - It’s an elegant way to avoid lambda clutter in simple cases.
#include <functional>
#include <iostream>
#include <numeric>
int main() {
double v1[3] = {1.0, 2.5, 4.6}, sum;
sum = std::accumulate(v1, v1 + 3, 0.0, std::minus<double>());
std::cout << "sum = " << sum << "\n";
}
Expected output: -8.1
Generator Object & Integration
📝Function Object aka Functor
operator()
lets you treat an object like a function.- This is zero-parameter, meaning it's called like
g()
; no arguments. - Each call advances
x
and returnsx²
.
#include <algorithm>
#include <iostream>
#include <numeric>
#include <vector>
public:
gen(double x_zero, double increment) : x(x_zero), incr(increment) {}
double operator()() {
x += incr;
return x * x;
}
private:
double x, incr;
};
std::generate()
+ Functor
std::generate
takes a range and a function object.- It calls
g()
n
times, populating the vector with values like(Δx)², (2Δx)², ...
. - Then
accumulate
computes the average: approximating ∫x² dx.
double integrate(gen g, int n) {
std::vector<double> fx(n);
std::generate(fx.begin(), fx.end(), g);
return std::accumulate(fx.begin(), fx.end(), 0.0) / n;
}
This is basically doing numerical integration of f(x) = x²
over [Δx, 1]
by approximating the area under the curve using small slices.
Testing:
int main() {
const int n = 10000;
gen g(0.0, 1.0 / n);
std::cout << "Integration program x**2" << "\n";
std::cout << integrate(g, n) << "\n";
}
Generator Objects with operator()()
- A generator object is a class that maintains state and returns a value on each call to
operator()
. - Acts like a function but remembers where it left off.
- Commonly used in STL algorithms like
std::generate()
.
Usage in Numerical Integration
- Create a functor that simulates the progression of
x
in a functionf(x)
. - Call it repeatedly to generate the range
f(x₁), f(x₂), ..., f(xₙ)
. - Sum and average to estimate integrals (e.g. Riemann sum).
Function Adapters
Things like bind1st
, bind2nd
, ptr_fun
are deprecated nowadays. You needed to stack templates on templates just to multiply something by 2. They are now replaced by:
- Lambdas for inline operations
std::function
for polymorphic callablesstd::bind
if you're feeling retro (avoid at all costs)
Old version:
#include <functional>
auto f = std::bind(std::multiplies<>(), std::placeholders::_1, 2);
New version:
#include <algorithm>
#include <iostream>
template <class ForwIter>
void print(ForwIter first, ForwIter last, const char *title) {
std::cout << title << "\n";
while (first != last)
std::cout << *first++ << "\t";
std::cout << "\n";
}
int main() {
int data[3] = {9, 10, 11};
print(data, data + 3, "Original Values");
std::transform(data, data + 3, data, [](int x) { return x * 2; });
print(data, data + 3, "New values");
}
Ordered vs. Unordered Maps - When to Sort and When to Chaos
std::map
is a sorted associative container using a red-black tree, which means it's always in order, but inserts and lookups are logarithmic time (O(log n)
). Great if you need your data sorted or care about order.std::unordered_map
is based on a hash table, so you get averageO(1)
time for lookups and inserts, but no ordering. It's the gremlin that screams "fast but chaotic."
Both store key-value pairs (std::pair<const Key, T>
under the hood).
#include <iostream>
#include <map>
#include <ostream>
#include <string>
#include <unordered_map>
int main() {
std::map<unsigned long, std::string> worker;
std::unordered_map<unsigned long, unsigned> payroll;
unsigned total_pay = 0;
worker[99567800] = "Harold Fish";
payroll[99567800] = 67300;
worker[8567800] = "Philip Fish";
payroll[8567800] = 87300;
for (auto p = worker.begin(); p != worker.end(); ++p) {
std::cout << "name " << (*p).second << "\tid no." << (*p).first << "\n";
}
for (auto p = payroll.begin(); p != payroll.end(); ++p) {
total_pay += (*p).second;
}
std::cout << "Payroll totals $" << total_pay << "\n";
}
Expected output:
name Philip Fish id no.8567800
name Harold Fish id no.99567800
Payroll totals $154600
Non mutating algorithms: std::find
- Does not modify the contents of the container they work on.
- Typical operation is searching the container for a particular element and returning its position
template <class InputIter, Class T>
InputIter find(InputIter b, InputIter e, const T &t);
- This returns an iterator to the first matching element (or
end()
if not found), so checking against the end is crucial for sanity.
template <class InputIter, Class Predicate>
InputIter find(InputIter b, InputIter e, Predicate p);
- Finds position of first element that makes Predicate true in range b to e, otherwise position e is returned.
Another possibility:
template <class InputIter, Class Function>
void for_each(InputIter b, InputIter e, Function f);
- Apply f for each value found in range b to e.
std::for_each
is great for side effects, but in modern C++, it's usually replaced with range-based for loops or algorithms likestd::transform
orstd::accumulate
for actual data transformation.
find()
algorithm example
#include <algorithm>
#include <iostream>
#include <ostream>
#include <string>
int main() {
std::string words[5] = {"my", "hop", "mop", "hope", "cope"};
std::string *where;
where = std::find(words, words + 5, "hop");
std::cout << *++where << "\n";
std::sort(words, words + 5);
where = std::find(words, words + 5, "hop");
std::cout << *++where << "\n";
}
Expected output:
mop
hope
- Here, sort does a lexicographic sort. "cope" will end up first and "my" will end up last.
Lambda expressions: for_each()
function
Lambda expressions:
- Derived from Lisp
- Previously
for_each()
needs a function - Will use C++11 idea: write an unnamed function in place: a lambda expression
You can use it, fall in love with it even if you don't have Lisp experience - or in more modern terms, Haskell.
Old style for_each()
#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>
void incr(int &i) {
static int n = 1;
i = n++;
}
void outvec(int i) { std::cout << i << "\n"; }
int main() {
std::vector<int> v(6);
std::for_each(v.begin(), v.end(), incr);
std::for_each(v.begin(), v.end(), outvec);
}
Lambda in C++11
- Unnamed function
[](int i) { cout << i << "\n"; }
[]
Goes where the function object is required.(int i)
Parameters{ cout << i << "\n"; }
Executable
[](int n) { return n * 5.5; }
deduces the return value as double
[](int n) -> int { return ++n; }
explicit type declaration.
#include <algorithm>
#include <iostream>
#include <ostream>
#include <vector>
void incr(int &i) {
static int n = 1;
i = n++;
}
auto outvec = [](int i) { std::cout << i << "\n"; };
// auto outvec: (lambda) = [](int i) -> void { std::cout << i << "\n"; };
int main() {
std::vector<int> v(6);
std::for_each(v.begin(), v.end(), incr);
std::for_each(v.begin(), v.end(), outvec);
}
Bonus from the Void If you are still here: Chaining Lambdas
- Defining a lambda
- That returns another lambda
- That closes over
factor
#include <iostream>
int main() {
// make_multiplier is a function (a lambda) that takes one int: factor
auto make_multiplier = [](int factor) {
// It returns another function - also a lambda!
// That lambda takes int x and multiplies it by factor
return [factor](int x) {
return x * factor;
};
};
// times5 is literally this: [](int x) { return x * 5; }
auto times5 = make_multiplier(5);
// This is calling 10 * 5 -> 50
std::cout << times5(10) << "\n";
}
Expected output: 50
Bidirectional Iterator: is_palindrome.cpp
This algorithm demonstrates how to use bidirectional iterators, which allow traversal both forward and backward—a requirement for checking palindromes efficiently.
Key Traits of a Bidirectional Iterator:
- Must support both ++ and -- operators (aka "backing up the iterator").
- Used in algorithms like
std::reverse()
andstd::equal()
that walk from both ends inward.
Old-School Approach (is_palindrome
)
This version uses two pointers (first
, last
) that walk inward and compare characters:
#include <algorithm>
#include <cctype>
#include <iostream>
#include <regex>
#include <string>
template <typename Bidirectional>
bool is_palindrome(Bidirectional first, Bidirectional last) {
if (first == last)
return true;
--last;
while (first < last) {
if (*first != *last)
return false;
++first;
--last;
}
return true;
}
int main() {
std::string input;
std::cout << "Is the input a palindrome? ";
std::getline(std::cin, input);
// lowercase input
std::transform(input.begin(), input.end(), input.begin(), ::tolower);
// remove non-alphanumeric
input = std::regex_replace(input, std::regex(R"([^a-z0-9])"), "");
if (is_palindrome(input.begin(), input.end()))
std::cout << "Yes\n";
else
std::cout << "No\n";
}
How it works:
- This is the low-level “two-pointer” style, seen often in C-style code.
- It stops early if any characters differ.
- If the pointers meet (or cross), it returns true.
Compared to using a forward-only iterator (which would require repeatedly walking from the start to simulate --), this approach is linear instead of quadratic.
Input Normalization
Before checking, we normalize the input to handle mixed case and punctuation (e.g. "A man, a plan, a canal, Panama!"):
std::transform(input.begin(), input.end(), input.begin(), ::tolower);
input = std::regex_replace(input, std::regex(R"([^a-z0-9])"), "");
std::transform
applies::tolower
to each character. This is necessary because uppercase and lowercase letters have different values in memory, unlike scripting languages that hide this behind.to_lower()
or${1,,}
.std::regex_replace
strips out all non-alphanumeric characters.
Modern C++ One-Liner: std::equal
You can replace the entire is_palindrome function with one clean STL call:
std::equal(input.begin(), input.begin() + input.size() / 2, input.rbegin())
This:
- Compares the first half of the string with the reverse of the second half.
- Automatically handles “meeting in the middle” without manual pointer arithmetic.
#include <algorithm>
#include <cctype>
#include <iostream>
#include <regex>
#include <string>
int main() {
std::string input;
std::cout << "Is the input a palindrome? ";
std::getline(std::cin, input);
// lowercase input
std::transform(input.begin(), input.end(), input.begin(), ::tolower);
// remove non-alphanumeric
input = std::regex_replace(input, std::regex(R"([^a-z0-9])"), "");
// check for palindrome
if (std::equal(input.begin(), input.begin() + input.size() / 2,
input.rbegin()))
std::cout << "Yes\n";
else
std::cout << "No\n";
}
Summary
- You should still learn the manual ++/-- method—understanding the low-level behavior makes STL functions less magical and more powerful.
- But once you get it, use std::equal. It’s cleaner, easier to read, and less error-prone. Let the STL do the pointer math.
Dijkstra's Algorithm with Edge List (C++ Edition)
The graph is represented using an edge list, not an adjacency matrix, which is simpler to work with when dynamically generating graphs of varying densities.
💻 Key Concepts
- Graph Structure:
std::vector<Edge>
with anEdge
struct holding source, destination, and weight. - Dijkstra’s Algorithm: Classic implementation using a
distances[]
array and a greedy loop withminDistance()
helper. - Random Graph Generation: Custom
generateRandomGraph()
function adds edges based on a given density (e.g. 20% or 40%).
The Code
#include <ctime>
#include <iostream>
#include <limits>
#include <random>
#include <set>
#include <vector>
const int INF = std::numeric_limits<int>::max();
struct Edge {
int source;
int destination;
int weight;
Edge(int s, int d, int w) : source(s), destination(d), weight(w) {}
};
class Graph {
public:
Graph(int vertices) : vertices(vertices) {}
void addEdge(int source, int destination, int weight) {
edges.emplace_back(source, destination, weight);
edges.emplace_back(destination, source, weight);
}
int getVertices() const { return vertices; }
const std::vector<Edge> &getEdges() const { return edges; }
std::vector<int> Dijkstra(int source) {
std::vector<int> distances(vertices, INF);
std::vector<int> visited(vertices, 0);
distances[source] = 0;
for (int i = 0; i < vertices - 1; ++i) {
int u = minDistance(distances, visited);
if (u == -1)
break;
visited[u] = 1;
for (const auto &edge : edges) {
if (!visited[edge.destination] && edge.source == u) {
int newDistance = distances[u] + edge.weight;
if (newDistance < distances[edge.destination]) {
distances[edge.destination] = newDistance;
}
}
}
}
return distances;
}
private:
int vertices;
std::vector<Edge> edges;
int minDistance(const std::vector<int> &distances,
const std::vector<int> &visited) {
int minDist = INF;
int minIndex = -1;
for (int v = 0; v < vertices; ++v) {
if (!visited[v] && distances[v] <= minDist) {
minDist = distances[v];
minIndex = v;
}
}
return minIndex;
}
};
void generateRandomGraph(Graph &g, int vertices, double density, int minWeight,
int maxWeight) {
int maxEdges = vertices * (vertices - 1) / 2;
int targetEdges = static_cast<int>(density * maxEdges);
std::mt19937 rng(static_cast<unsigned int>(time(nullptr)));
std::uniform_int_distribution<int> vertexDist(0, vertices - 1);
std::uniform_int_distribution<int> weightDist(minWeight, maxWeight);
std::set<std::pair<int, int>> existingEdges;
while (static_cast<int>(existingEdges.size()) < targetEdges) {
int u = vertexDist(rng);
int v = vertexDist(rng);
if (u != v && existingEdges.find({u, v}) == existingEdges.end() &&
existingEdges.find({u, v}) == existingEdges.end()) {
int weight = weightDist(rng);
g.addEdge(u, v, weight);
existingEdges.insert({u, v});
}
}
}
int main() {
int vertices = 50;
Graph g(vertices);
double density = 0.2;
generateRandomGraph(g, vertices, density, 1, 10);
std::vector<int> distances = g.Dijkstra(0);
int sum = 0;
for (int i = 1; i < vertices; ++i) {
std::cout << "Distance from 0 to " << i << ": " << distances[i] << "\n";
sum += distances[i];
}
std::cout << "Average Path : " << static_cast<float>(sum) / 49.0 << "\n";
return 0;
}
Output a Random Graph in C++
Context: Refactor of an instructor-provided codebase to generate a random undirected graph with cost and color data. Original code was a memory-leaking, segfault-happy mess.
Generate a random graph using a 2D adjacency matrix, apply weights (cost) and labels (color) to edges, and export the result to a .txt file.
💥 Original Problems
- Used raw pointers (
new[]
) without anydelete[]
: memory leak central. - No structure—everything shoved into
main()
.
🔧 Refactor Goals
- Encapsulate logic in a Graph class.
- Use
std::vector<std::vector<T>>
for memory safety and clarity. - Organize code: generation, cost assignment, and file output as clean methods.
- Eliminate leaks and crashes; run clean under Valgrind.
⚙️ Inputs
int size
: graph size (number of nodes)double density
: probability of edge existence between nodesstd::string filename
: output filename for the graph data
#include <cstdlib>
#include <ctime>
#include <fstream>
#include <iostream>
#include <ostream>
#include <string>
#include <vector>
class Graph {
private:
double prob() { return (static_cast<double>(rand()) / RAND_MAX); }
public:
// Constructor
std::vector<std::vector<bool>> graph;
std::vector<std::vector<int>> color;
std::vector<std::vector<int>> cost;
int size;
double density;
Graph(int s, double d) : size(s), density(d) {
graph.resize(size, std::vector<bool>(size, false));
color.resize(size, std::vector<int>(size, 0));
cost.resize(size, std::vector<int>(size, 0));
}
// generate graph
void generate_graph() {
for (int i = 0; i < size; ++i)
for (int j = i; j < size; ++j)
if (i == j)
graph[i][j] = false;
else
graph[i][j] = graph[j][i] = (prob() < density);
}
// generate cost and color
void cost_and_color() {
for (int i = 0; i < size; ++i)
for (int j = i; j < size; ++j)
if (graph[i][j]) {
color[i][j] = color[j][i] = rand() % 3;
cost[i][j] = cost[j][i] = prob() * 30;
}
}
// write to a txt file
void output_file(const std::string &filename) {
std::ofstream outp(filename);
outp << size << "\n";
for (int i = 0; i < size; ++i)
for (int j = 0; j < size; ++j) {
if (graph[i][j])
outp << i << '\t' << j << '\t' << cost[i][j] << '\t' << color[i][j]
<< '\n';
}
}
};
int main(void) {
int size;
double density;
std::string filename;
// User input
std::cout << "graph size?" << "\n";
std::cin >> size;
std::cout << "graph density (0,1)?" << "\n";
std::cin >> density;
Graph g(size, density);
g.generate_graph();
g.cost_and_color();
std::cout << "file name?" << "\n";
std::cin >> filename;
g.output_file(filename);
std::cout << "done." << "\n";
}
Poker Monte Carlo Simulation: The Final Boss of Single Iterators (for now)
This simulation is a chaotic but disciplined demonstration of core C++ features, data structures, and algorithmic logic. Here's what it leverages:
-
enum class
: a scoped, type-safe alternative to traditional C-style enums.- Syntax:
enum class Identifier : integral_type { list };
- Example:
enum class Color { RED, GREEN, BLUE }; enum class Spotlight { RED, YELLOW, GREEN };
- In old-style enums,
RED
would be ambiguous if used in the same scope. - With
enum class
,Color::RED
andSpotlight::RED
are completely distinct. ::
is called the scope resolution operator.- By default, the underlying type is int; but you can specify something smaller like
short
orunsigned
(as long as it's integral).
- Syntax:
-
Custom
card
data type: modeled usingenum class
for suits and apips
class to represent values (1 to 13). Together, they form an easily extensible system for identifying and comparing cards. -
std::vector<T>
, dynamic arrays that shrink and grow as needed. Also a great workaround to avoid usingnew
anddelete
, something you would use if you're asking for problems. Vectors also provides iterator support for algorithms likestd::shuffle
andstd::sort
. -
Modern RNG:
std::random_shuffle()
is deprecated.- The correct modern approach is:
std::random_device rd; std::mt19937 g(rd()); std::shuffle(deck.begin(), deck.end(), g);
std::mt19937
is a Mersenne Twister engine, high-quality and widely used.std::random_device
provides a non-deterministic seed (if supported by the system).
std::map
:
To classify hands like pairs, three-of-a-kinds, full houses, and four-of-a-kinds, I used std::map
as a clean alternative to writing multiple functions for each case.
-
A
std::map<Key, Value>
stores key-value pairs, sorted by key in ascending order by default. -
In this simulation, the key is the pip value (card number), and the value is the count of how many times it appears in a hand.
-
This allows a single pass over the hand to collect all frequency data, which can then be used to identify any relevant hand:
- A key with a value of
4
→ Four of a Kind - A key with a value of
3
and another with2
→ Full House - Two keys with value
2
→ Two Pair - One key with value
2
→ One Pair
- A key with a value of
Example usage:
std::map<int, int> pip_counts;
for (const card& c : hand) {
pip_counts[c.get_pips().get_pips()]++;
}
- Unlike
unordered_map
, which is backed by a hash table and has no guaranteed order,std::map
keeps things sorted, which may help with debug output or extensions like detecting the highest kicker.
Using this map, a single classify_hand()
function was enough to identify all hand types based on how many identical pip values were found.
Suggestions for improvements
- Detecting royal flushes
- Analyzing average hand value
- Plotting probabilities as a histogram
#include <algorithm>
#include <array>
#include <cassert>
#include <iostream>
#include <map>
#include <ostream>
#include <random>
#include <vector>
enum class suit : short { SPADE, HEART, DIAMOND, CLUB };
std::ostream &operator<<(std::ostream &out, const suit &s) {
return out << static_cast<int>(s);
}
class pips {
public:
pips(int val) : v(val) { assert(v > 0 && v < 14); }
friend std::ostream &operator<<(std::ostream &out, const pips &p) {
out << p.v;
return out;
};
int get_pips() { return v; }
private:
int v;
};
class card {
public:
card() : s(suit::SPADE), v(1) {}
card(suit s, pips v) : s(s), v(v) {}
friend std::ostream &operator<<(std::ostream &out, const card &c);
const suit get_suit() { return s; }
pips get_pips() const { return v; }
private:
suit s;
pips v;
};
std::ostream &operator<<(std::ostream &out, const card &c) {
out << c.v << c.s;
return out;
}
void init_deck(std::vector<card> &d) {
const std::array<suit, 4> all_suits = {suit::SPADE, suit::HEART,
suit::DIAMOND, suit::CLUB};
for (const auto &s : all_suits) {
for (int p = 1; p < 14; ++p) {
d.emplace_back(s, pips(p));
}
}
}
void print(std::vector<card> &deck) {
for (auto cardval : deck)
std::cout << cardval << "\n";
}
void classify_hand(std::vector<card> &hand, int &pairs, int &trips,
int &quads) {
std::map<int, int> pip_count;
for (const card &c : hand) {
int pip = c.get_pips().get_pips();
pip_count[pip]++;
}
pairs = 0;
trips = 0;
quads = 0;
for (const auto &[pip, count] : pip_count) {
if (count == 2)
pairs++;
else if (count == 3)
trips++;
else if (count == 4)
quads++;
}
}
bool is_flush(std::vector<card> &hand) {
suit s = hand[0].get_suit();
for (auto p = hand.begin() + 1; p != hand.end(); ++p)
if (s != p->get_suit())
return false;
return true;
}
bool is_consecutive(int *pips) {
for (int i = 1; i < 5; ++i)
if (pips[i] != pips[i - 1] + 1)
return false;
return true;
}
bool is_straight(std::vector<card> &hand) {
int pips_v[5];
for (int i = 0; i < 5; ++i)
pips_v[i] = hand[i].get_pips().get_pips();
std::sort(pips_v, pips_v + 5);
// Ace high: 10-J-Q-K-A
if (pips_v[0] == 1 && pips_v[1] == 10)
return pips_v[1] == 10 && pips_v[2] == 11 && pips_v[3] == 12 &&
pips_v[4] == 13;
// regular straight
return is_consecutive(pips_v);
}
bool is_straight_flush(std::vector<card> &hand) {
return is_flush(hand) && is_straight(hand);
}
int main() {
std::vector<card> deck(52);
srand(time(nullptr));
init_deck(deck);
int how_many;
int high_card = 0, one_pair = 0, two_pair = 0, three_ofa_kind = 0,
four_of_akind = 0, full_house = 0;
int flush_count = 0, str_count = 0, str_flush_count = 0;
std::cout << "How many shuffles? ";
std::cin >> how_many;
std::random_device rd;
std::mt19937 g(rd());
for (int loop = 0; loop < how_many; ++loop) {
std::shuffle(deck.begin(), deck.end(), g);
std::vector<card> hand(deck.begin(), deck.begin() + 5);
int pairs = 0, trips = 0, quads = 0;
classify_hand(hand, pairs, trips, quads);
if (is_flush(hand))
flush_count++;
else if (is_straight(hand))
str_count++;
else if (is_straight_flush(hand))
str_flush_count++;
else if (quads == 1)
four_of_akind++;
else if (trips == 1 && pairs == 1)
full_house++;
else if (trips == 1)
three_ofa_kind++;
else if (pairs == 2)
two_pair++;
else if (pairs == 1)
one_pair++;
else
high_card++;
}
std::cout << "High Card: " << high_card << " out of " << how_many << "\n";
std::cout << "Pair: " << one_pair << " out of " << how_many << "\n";
std::cout << "Two Pairs: " << two_pair << " out of " << how_many << "\n";
std::cout << "Three of a kind: " << three_ofa_kind << " out of " << how_many
<< "\n";
std::cout << "Straights: " << str_count << " out of " << how_many << "\n";
std::cout << "Flushes: " << flush_count << " out of " << how_many << "\n";
std::cout << "Full House: " << full_house << " out of " << how_many << "\n";
std::cout << "Four of a kind: " << four_of_akind << " out of " << how_many
<< "\n";
std::cout << "Straight Flushes: " << str_flush_count << " out of " << how_many
<< "\n";
}
Kruskal's MST Implementation in C++
A Minimum Spanning Tree (MST) of a connected weighted undirected graph is a sub graph that connects all the vertices while minimizing the total edge weight. It has the following properties:
- It spans all vertices of the original graph.
- It contains no cycles.
- The sum of its edge weights is minimal among all spanning trees.
Key points about MST:
- It provides a way to connect all nodes in a network with the least amount of wire/cable.
- In computer networks, it helps in designing efficient communication networks.
- In transportation systems, it aids in finding the shortest path between multiple destinations.
History of Kruskal's Algorithm
Kruskal's algorithm was developed by Joseph Kruskal in 1956. It was one of the first algorithms to solve the Minimum Spanning Tree problem efficiently.
- Initially, it was designed for electrical engineering applications but later found widespread use in computer science.
My Implementation
To achieve this, I was given a pseudocode to start with.
- Create a forest F (set of trees) where each vertex in the graph is a separate tree.
- Create a set S containing all the edges in the graph.
While S is nonempty and F is not yet spanning:
- remove an edge with minimum weight from S
- if that edge connects two different trees, then add it to the forest, combining two trees into a single tree
- otherwise discard that edge.
- at the termination of the algorithm, the forest has only one component and forms a minimum spanning tree of the graph.
I have tried to keep the approach structured with class constructors, while the functions KruskalMST()
, add_edge()
and computeMST()
serves to execute them. The results are shown at the end thanks to the printMST()
function. The main()
function is just there to route it all together.
#include <algorithm>
#include <iostream>
#include <utility>
#include <vector>
// Initialize edges and operator overloading
struct Edge {
int u, v, weight;
bool operator<(const Edge &other) const { return weight < other.weight; }
};
class KruskalMST {
public:
// Constructor declarations
KruskalMST(int n);
void add_edge(int u, int v, int weight);
void computeMST();
void printMST();
private:
// Build trees
std::vector<Edge> edges;
std::vector<Edge> mst;
std::vector<int> parent, rank;
int num_vertices;
// Initialization
void make_set(int v) {
parent[v] = v;
rank[v] = 0;
}
// Path compression
int find_set(int v) {
if (v == parent[v])
return v;
return parent[v] = find_set(parent[v]);
}
// Union by rank
void union_sets(int a, int b) {
a = find_set(a);
b = find_set(b);
if (a != b) {
if (rank[a] < rank[b])
std::swap(a, b);
parent[b] = a;
if (rank[a] == rank[b])
rank[a]++;
}
}
};
void KruskalMST::add_edge(int u, int v, int weight) {
edges.push_back({u, v, weight});
if (u >= num_vertices || v >= num_vertices) {
std::cerr << "Invalid edge: " << u << " - " << v << "\n";
return;
}
}
void KruskalMST::computeMST() {
std::sort(edges.begin(), edges.end());
for (Edge e : edges) {
if (find_set(e.u) != find_set(e.v)) {
mst.push_back(e);
union_sets(e.u, e.v);
}
}
}
// Executing the algorithm itself
KruskalMST::KruskalMST(int n) {
num_vertices = n;
parent.resize(n);
rank.resize(n);
for (int i = 0; i < n; ++i)
make_set(i);
}
// Function to print results
void KruskalMST::printMST() {
int tot_weight = 0;
std::cout << "Edges in the MST are \n";
for (const Edge &e : mst) {
std::cout << e.u << "--" << e.v << " : " << e.weight << "\n";
tot_weight += e.weight;
}
std::cout << "\nTotal weight is: " << tot_weight << "\n";
};
// Testing above functions
int main() {
KruskalMST graph(9);
graph.add_edge(0, 3, 8);
graph.add_edge(2, 5, 5);
graph.add_edge(6, 1, 7);
graph.computeMST();
graph.printMST();
return 0;
🔍 DFS vs BFS: When to Use What
Feature | DFS | BFS |
---|---|---|
Search Style | Goes deep along a path | Explores all neighbors level by level |
Data Structure | Stack (or recursion) | Queue |
Memory Use | Lower in wide graphs | Higher in wide graphs |
Finds Shortest Path? | ❌ No (it may take the scenic route) | ✅ Yes (fewest moves) |
Easier to implement? | ✅ Often simpler for grid/graph traversal | ✅ Also easy with STL queue |
Good for... | Existence of a path | Finding optimal path length |
🧠 So for your Hex win check:
- You only care if a path exists between one side and the other.
- You don’t care how long the path is.
✅ So DFS is perfect:
- Fast
- Low overhead
- Easy to implement
- Works great for this use case
🧪 Your mdBook Entry Could Look Like:
DFS: Detecting a Path Between Hex Cells
This algorithm is used to determine whether a player has connected their respective sides of the Hex board (left-to-right or top-to-bottom).
Why DFS?
- We don't care how long the path is—just whether one exists.
- DFS is lightweight and simple for this use case.
- It allows us to “flood” from the border and check if the opposite side is reachable.
Code Sketch (using a stack)
std::stack<Point> stack;
std::vector<std::vector<bool>> visited(board_size, std::vector<bool>(board_size, false));
// Start from the player’s border cells
for (int i = 0; i < board_size; ++i)
if (border_cell[i] == player)
stack.push({x, y});
while (!stack.empty()) {
Point current = stack.top();
stack.pop();
// Skip visited
if (visited[current.x][current.y]) continue;
visited[current.x][current.y] = true;
// Check if we reached the goal edge
if (current.y == board_size - 1) return true;
// Explore neighbors
for (auto n : get_neighbors(current.x, current.y))
if (!visited[n.x][n.y] && cells[n.x][n.y] == player)
stack.push(n);
}
BFS Alternative
- Use a queue instead of a stack.
- Use it when path length matters (e.g. shortest route to victory).
- Slightly more memory-intensive in wide boards.
BFS in Graphs
Breadth-First Search explores a graph level-by-level using a queue. It’s useful for finding the shortest path or testing reachability.
C++ (Pointer-Based Version)
class Graph {
int V;
std::list<int>* adj;
// ... constructor, addEdge, etc.
void BFS(int start) {
bool* visited = new bool[V];
std::fill(visited, visited + V, false);
std::list<int> queue;
visited[start] = true;
queue.push_back(start);
while (!queue.empty()) {
int node = queue.front();
queue.pop_front();
// Process node...
for (auto neighbor : adj[node]) {
if (!visited[neighbor]) {
visited[neighbor] = true;
queue.push_back(neighbor);
}
}
}
delete[] visited;
}
};
⚠️ Gotchas:
std::vector<bool>
is not safe for pointer-like access.- Prefer
std::vector<char>
if you're doing raw pointer-style stuff in C++.
Alpha Beta Pruning
-
Leaf nodes are evaluated on a scale
-
Larger negative value in minimizer will win
-
Larger positive value in maximizer will win
-
Values are backed up by the tree : alternate max and min.
-
Maximizer : I can at least get alpha
-∞
-
Minimizer : I can at least get beta
+∞
-
Optimum is when best moves are being considered.
#include <algorithm>
#include <iostream>
#include <limits>
const int MAX = std::numeric_limits<int>::max();
const int MIN = std::numeric_limits<int>::min();
int minimax(int depth, int node_index, bool max_player, int values[], int alpha, int beta) {
if (depth == 3)
return values[node_index];
if (max_player) {
int best = MIN;
for (int i = 0; i < 2; i++) {
int val = minimax(depth + 1, node_index * 2 + i, false, values, alpha, beta);
best = std::max(best, val);
alpha = std::max(alpha, best);
if (beta <= alpha)
break;
}
return best;
} else {
int best = MAX;
for (int i = 0; i < 2; i++) {
int val = minimax(depth + 1, node_index * 2 + i, true, values, alpha, beta);
best = std::min(best, val);
beta = std::min(beta, best);
if (beta <= alpha)
break;
}
return best;
}
}
int main() {
int values[8] = {3, 8, 1, 3, 4, 8, 4, 6};
std::cout << "The optimal value is: "
<< minimax(0, 0, true, values, MIN, MAX) << std::endl;
return 0;
}
Polish Notation
- No parenthesis:
(3 + 4)
->+ 3 4
Example: (9 + 6) * (3 - 2)
-> 9 6 + 3 2 - *
A simple calculator for Polish notations:
#include <iostream>
#include <sstream>
#include <stack>
#include <string>
int evaluate_rpn(const std::string &expr) {
std::stack<int> stk;
std::istringstream iss(expr);
std::string token;
while (iss >> token) {
if (isdigit(token[0])) {
stk.push(std::stoi(token));
} else {
int b = stk.top();
stk.pop();
int a = stk.top();
stk.pop();
if (token == "+")
stk.push(a + b);
else if (token == "-")
stk.push(a - b);
else if (token == "*")
stk.push(a * b);
else if (token == "/")
stk.push(a / b);
}
}
return stk.top();
}
int main() {
std::cout << evaluate_rpn("3 5 2 * +") << std::endl;
return 0;
}
What’s Actually Happening in 3 5 2 * +
This is postfix notation. Think of it like:
“I’ll give you the numbers, and then tell you what to do with them.”
Step-by-step Breakdown
Expression: 3 5 2 * +
-
Read
3
→ push to the stack 🥞 Stack:[3]
-
Read
5
→ push to the stack 🥞 Stack:[3, 5]
-
Read
2
→ push to the stack 🥞 Stack:[3, 5, 2]
-
Read
*
→ pop2
and5
, multiply them → push result5 * 2 = 10
🥞 Stack:[3, 10]
-
Read
+
→ pop10
and3
, add them → push result3 + 10 = 13
🥞 Stack:[13]
Result = 13
You just evaluated 3 + (5 * 2)
without parentheses and without caring about precedence rules.
RPN implicitly does “inwards” operations first — the deepest nested expressions get evaluated earliest. But it doesn't track them using parentheses or operator precedence like infix notation. Instead, the stack handles that naturally, because operations only happen when operands are ready.
🔁 Infix → RPN Mental Gymnastics
Let’s take:
(4 + 2) * (3 - 1)
RPN version:
4 2 + 3 1 - *
Why?
4 2 +
→ gives you 63 1 -
→ gives you 2- Then
*
→6 * 2 = 12
The operators are applied only when their operands are present, and everything is processed left to right. No drama, no ambiguity, no parentheses.
💡 Why You Should Care
If you ever:
- Write a compiler or interpreter,
- Build a virtual machine (JITs for example),
- Design an AI rule engine,
- Or implement a Lisp-style scripting language,
…you’ll want RPN-style evaluation. It’s efficient, deterministic, and ridiculously elegant.
Why Ruby? Because I Had Enough of JavaScript.
I originally picked up Ruby out of pure JavaScript fatigue—I was tired of maintaining a monstrous Express + React project. It was easier to burn everything down and start fresh with a new language than continue suffering.
That turned out to be one of my best decisions. By switching to Ruby’s Sinatra framework, I replaced 5000 lines of bloated code with a single backend file and some clean HTML templates. Alongside Python, Ruby will continue to be my go-to for small web projects—whether that’s to avoid paying monthly for some overhyped SaaS tool or simply to reclaim time lost to unnecessarily complicated frameworks.
Ruby projects are also ridiculously easy to deploy, making it perfect for my DevOps journey—because what’s the point of studying infrastructure if I’m not constantly deploying things?
Coming from my C studies, Ruby feels like a different world—full of abstractions, letting you get away with absurdly concise one-liners. Debugging is a breeze, and for scripting and automation, it offers an interesting alternative to Python.
I had zero prior experience when I first picked it up, but that didn’t stop me from shipping projects with it. In this section of the grimoire, we’ll be discovering Ruby’s true power together—because it turns out, this language has more depth than it first appears.
Ruby Cheat Sheet 💎
1. Syntax and Data Types
Ruby is elegant and readable, but its syntax can be weird if you're coming from C, C++, or Python.
Variables and Data Types
name = "PwatPwat" # String
age = 1000 # Integer
height = 5.9 # Float
is_sassy = true # Boolean
languages = ["C", "Ruby", "HTMX"] # Array
skills = { "devops" => true, "systems" => true, "webdev" => "meh" } # Hash (like a dictionary)
- Variables don't need types.
- nil is Ruby's equivalent of null.
String Interpolation (Instead of Concatenation)
puts "Hello, my name is #{name} and I am #{age} years old."
- #{variable} injects the value directly.
- No need for " + ", which is a blessing.
Symbols (Efficient Strings)
:hello # Think of it as an immutable string
skills = { devops: true, systems: true, webdev: false }
puts skills[:devops] # Access a symbol key like this
- Symbols (:something) are immutable and more memory-efficient than strings.
2. Control Flow
If-Else Statements
if age > 18
puts "You're old enough."
elsif age == 18
puts "Just made it!"
else
puts "Too young."
end
- No parentheses needed around conditions.
- elsif instead of elseif.
Ternary Operator
puts age > 18 ? "Adult" : "Minor"
- Short and clean, just like Python’s ternary operator.
Unless (Because Ruby is Dramatic)
unless is_sassy
puts "You are being too serious today."
else
puts "Sass mode activated."
end
- Equivalent to if !is_sassy, but reads more naturally.
3. Loops
For Loop (But You Won’t Use It)
for i in 1..5
puts i
end
- 1..5 includes 5, while 1...5 excludes 5.
While Loop
i = 0
while i < 5
puts "Iteration #{i}"
i += 1
end
Times Loop (More Idiomatic)
5.times { |i| puts "Iteration #{i}" }
- Instead of a for loop, Ruby prefers .times.
Each Loop (Preferred Over For)
languages.each { |lang| puts "I know #{lang}" }
- The block { |var| ... } replaces a for-loop.
Map (Functional Approach)
squared_numbers = [1, 2, 3, 4].map { |num| num ** 2 }
puts squared_numbers.inspect # [1, 4, 9, 16]
- Modifies each element in the array.
4. Functions and Blocks
Defining a Function
def greet(name)
"Hello, #{name}!"
end
puts greet("PwatPwat") # "Hello, PwatPwat!"
- No return needed; Ruby returns the last evaluated expression automatically.
Default Arguments
def greet(name="Guest")
"Hello, #{name}!"
end
Lambda & Proc (If You Like Functional Stuff)
say_hello = -> { puts "Hello!" } # Lambda function
say_hello.call
- Similar to anonymous functions in JS.
5. File Handling
Reading a File
File.open("test.txt", "r") do |file|
puts file.read
end
Writing to a File
Copier le code
File.open("test.txt", "w") do |file|
file.puts "This is a new line"
end
6. Ruby Scripting Tricks
- If you ever use Ruby for system automation, here are some neat tricks:
Run Shell Commands
puts `ls -la` # Runs shell command
Argument Parsing (if running a script)
puts "Hello, #{ARGV[0]}!" # Run as `ruby script.rb PwatPwat`
Simple HTTP Request
require 'net/http'
puts Net::HTTP.get(URI("https://example.com"))
7. Object-Oriented Ruby (If You Like Pain)
class Person
attr_accessor :name, :age
def initialize(name, age)
@name = name
@age = age
end
def introduce
"Hi, I'm #{@name} and I'm #{@age} years old."
end
end
pwat = Person.new("PwatPwat", 1000)
puts pwat.introduce
- @name is an instance variable.
- attr_accessor generates getter/setter methods automatically
🧠 Ruby OOP Crash Course – For When You’re Tired of C++’s BS
Because sometimes you want to write OOP without declaring constructors like it’s a war crime.
🎭 Classes – Your Basic Drama Unit
class Witch
def initialize(name)
@name = name
end
def cackle
puts "#{@name} cackles wickedly. 🔮"
end
end
sabrina = Witch.new("Sabrina")
sabrina.cackle
initialize
is the constructor.@name
is an instance variable — tied to that specific girl (object).new
creates the object.- Cute and to the point.
🧘♀️ self
– Finding Yourself Spiritually and Contextually
class Mirror
def self.reflect
puts "I am looking at myself. 💅"
end
def reflect
puts "You're looking at an instance now. 👀"
end
end
Mirror.reflect # Class method
Mirror.new.reflect # Instance method
self.method
is a class method.- Just
def method
is an instance method. - You have to be explicit with
self.
for class methods, or Ruby’s like “nah fam”.
🫂 Instance vs Class Variables
class Cult
@@followers = 0
def initialize
@@followers += 1
end
def self.count_followers
@@followers
end
end
3.times { Cult.new }
puts Cult.count_followers # 3
@@variable
is shared across all instances — a class variable.- Use with caution: this can get messy if subclassing. Ruby has drama here.
📦 Modules – When You Just Wanna Mixin™ Some Behavior
module Flyable
def fly
puts "Zooming through the sky! 🕊️"
end
end
class Witch
include Flyable
end
Witch.new.fly
- Use
include
to mixin instance methods. - Use
extend
to add class methods from a module.
module Sassy
def roast
puts "Your code's so bad, it makes Perl look readable. 💅"
end
end
class Ruby
extend Sassy
end
Ruby.roast
🧱 When to Use Classes vs Modules
Use Case | Go With | Why |
---|---|---|
You’re building real objects (people, dragons, buttons) | Class | They’re blueprints for things |
You just want to slap on some extra behavior | Module | They’re like trait makeup kits |
You’re trying to share methods across multiple classes | Module | DRY, reusable, non-instantiable |
You need to instantiate something | Class | Modules don’t .new unless you're cursed |
🪄 Inheritance – Passing Down That Magic
class Being
def exist
puts "I exist. 🌌"
end
end
class Unicorn < Being
def sparkle
puts "✨ I sparkle with purpose ✨"
end
end
Unicorn.new.exist
Unicorn.new.sparkle
Class < ParentClass
for inheritance.- Ruby only supports single inheritance (no poly party), but you can fake it with modules.
💣 Extra Sass & Warnings
- Don’t overuse class variables (
@@
). They can leak like gossip in a small town. - Prefer modules for reusable behavior, especially if you don’t need state.
- Ruby is duck-typed — don’t obsess over class hierarchies like it’s Java.
- Everything is an object. Even classes. Even
nil
. So go wild (but not too wild).
💌 params[]
: Direct from the user
- Comes from the request itself: URL segments (
:id
), query strings (?foo=bar
), or form data. - You use it in the same route where the request is made.
- It's like checking the envelope of a letter to see who it's from. It's raw input.
Example:
params[:id] # from `/blog/42`
params[:search] # from `/search?search=cats`
💅 @instance_variable
: For sharing across templates
- Used to pass data to views (templates), or between routes if you're being fancy.
- You set it in the route/controller action and access it in the view (
erb
,haml
, whatever). - It’s like putting your lipstick on the table for others to use. You're saying, "Here, this is for the next part."
Example:
@article = fetch_api(...)
# later in the view: <%= @article['title'] %>
💡 Rule of Thumb
- Use
params[]
to get data from the request. - Use
@variables
to send data to the view or carry stuff along in your code. - Always guard against
nil
when dealing with external data. They're like unreliable exes — might show up, might ghost you.
If you ever get confused again, just ask yourself:
“Is this coming from the outside world? Or is this something I already put on the shelf for later?”
Ruby Functional Programming Cheat Sheet 💎
1. First-Class Functions (Because We’re Not Peasants)
In Ruby, functions are first-class citizens, which means you can:
- Assign them to variables
- Pass them around like objects
- Return them from other functions
Assigning Functions to Variables
def greet(name)
"Hello, #{name}!"
end
say_hi = method(:greet) # Grab the method as an object
puts say_hi.call("PwatPwat") # => "Hello, PwatPwat!"
JS developers would be confused because their language still doesn’t know what it wants to be.
2. Lambdas & Procs (Because We Hate Boilerplate)
Ruby has two types of anonymous functions: lambdas and procs.
Lambdas (Strict, Like a No-Nonsense Professor)
say_hello = -> (name) { puts "Hello, #{name}!" }
say_hello.call("Ruby") # => "Hello, Ruby!"
- Uses -> for defining
- Checks argument count (strict like C)
Procs (Laid-Back, Like a Sleepy Dev)
lazy_greet = Proc.new { |name| puts "Sup, #{name}." }
lazy_greet.call("you") # => "Sup, you."
- Uses Proc.new
- Doesn’t care about missing arguments (very chill, very Ruby)
3. Higher-Order Functions (Passing Functions Around Like Secrets)
Functions Taking Other Functions
def apply_twice(func, value)
func.call(func.call(value))
end
double = ->(x) { x * 2 }
puts apply_twice(double, 5) # => 20
- Passes the double function into apply_twice
- JavaScript developers sweating because they only just learned .map()
4. Functional Methods on Collections (Destroying Loops)
Ruby lets you replace loops with functional goodness.
Map Instead of a For-Loop
numbers = [1, 2, 3, 4]
doubled = numbers.map { |n| n * 2 }
puts doubled.inspect # => [2, 4, 6, 8]
Select Instead of Filtering in Loops
evens = numbers.select { |n| n.even? }
puts evens.inspect # => [2, 4]
Reduce Instead of Ugly Accumulators
sum = numbers.reduce(0) { |acc, num| acc + num }
puts sum # => 10
Node.js devs in shambles because .reduce() in JavaScript requires a PhD.
5. Currying (Because Why Not?)
You can partially apply functions like a Haskell god. Example: Making a Curried Adder
adder = -> (x) { -> (y) { x + y } }
add_five = adder.call(5)
puts add_five.call(10) # => 15
- adder.call(5) returns a new function waiting for y
- Node.js devs still writing .bind(this)
6. Composition (Stacking Functions Like a Boss)
Instead of nesting functions, compose them:
def compose(f, g)
->(x) { f.call(g.call(x)) }
end
double = ->(x) { x * 2 }
increment = ->(x) { x + 1 }
double_then_increment = compose(increment, double)
puts double_then_increment.call(5) # => 11
# double(5) → 10
# increment(10) → 11
- Elegant. Chaotic. Beautiful.
Final Verdict
- Ruby can go full functional
- Less boilerplate than JavaScript
- More readable than Haskell
- Shorter than Python
- Node.js devs now crying in async hell
Setup a Redis Rate Limiter with Ruby
The @limit and @period instance variables specify the maximum number of requests and the time period, respectively. We use the redis gem to create a new Redis client and increment the request count for each client.
class RateLimiter
def initialize(app, limit:, period:)
@app = app
@limit = limit
@period = period
@redis = Redis.new(password: ENV['REDIS_PASSWORD'])
end
def call(env)
req = Rack::Request.new(env)
ip = req.ip
key = "rate-limit:#{ip}"
count = @redis.incr(key)
@redis.expire(key, @period) if count == 1
return [429, { 'Content-Type' => 'text/plain' }, ['Rate Limit exceeded']] if count > @limit
@app.call(env)
end
end
Use SQLite3 as Your RateLimiter in Ruby
Sometimes, your project is small, scalability is a problem for later and you can't afford to deploy Redis separately either. In that case, you can always rely on SQLite3 to gatekeep users that might be making too many requests.
# frozen_string_literal: true
require 'dotenv/load'
require 'sqlite3'
# The @limit and @period instance variables specify
# the maximum number of requests and the time period,
# respectively.
class RateLimiter
def initialize(app, limit:, period:, db_path: 'data/rate-limiter.db')
@app = app
@limit = limit
@period = period
@db = SQLite3::Database.new(db_path)
create_table
end
def call(env)
req = Rack::Request.new(env)
ip = req.ip
current_time = Time.now.to_i
window_start = current_time - @period
@db.execute('DELETE FROM rate_limits WHERE timestamp < ?', window_start)
count = @db.get_first_value('SELECT COUNT(*) FROM rate_limits WHERE ip = ?', ip)
return [429, { 'content-type' => 'text/plain' }, ['rate limit exceeded']] if count > @limit
@db.execute('INSERT INTO rate_limits (ip, timestamp) VALUES (?, ?)', [ip, current_time])
@app.call(env)
end
private
def create_table
@db.execute <<-SQL
CREATE TABLE IF NOT EXISTS rate_limits (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ip TEXT NOT NULL,
timestamp INTEGER NOT NULL
);
SQL
end
end
Quick Server-Side Sinatra Setup
Minimal, elegant Ruby backend without JavaScript nonsense.
Project Structure
my_app/
├── app.rb
├── views/
│ └── index.erb
├── public/
│ └── style.css
├── Gemfile
└── config.ru
Step-by-Step
- Install Sinatra
gem install sinatra
- Basic
app.rb
Template
require 'sinatra'
get '/' do
erb :index
end
- Create Views
<!-- views/index.erb -->
<h1>Hello, world</h1>
- Run It
ruby app.rb
- Production Ready with Rack
# config.ru
require './app'
run Sinatra::Application
rackup config.ru
TODO
- Add custom routes
- Add environment config (
dotenv
) - Connect to Postgres
Resources
ERB Templates with HTMX
Using server-side rendered HTML fragments with zero JS frameworks.
Goal
Combine Ruby’s ERB templating system with HTMX to create reactive web pages without using JavaScript libraries.
Setup
HTML Template
<!-- views/index.erb -->
<div id="content">
<%= erb :partial, locals: { message: "Initial load" } %>
</div>
<button hx-get="/update" hx-target="#content" hx-swap="innerHTML">Click Me</button>
Route in Sinatra
get '/update' do
erb :partial, locals: { message: "Updated via HTMX!" }
end
Partial Template
<!-- views/_partial.erb -->
<p><%= message %></p>
Server Behavior
- On button click, HTMX sends GET request
- Server returns only the partial
- Target content is replaced with new HTML fragment
Advanced Use
- Use
hx-post
for form submissions - Load content into modals
- Trigger spinners using
hx-indicator
TODO
- Add CSRF protection
- Explore
htmx:configRequest
for headers - Integrate with sessions or user auth
Resources
🗂️ Clean Pagination in Sinatra (Backend-Controlled)
To avoid loading too many records and letting the UI handle paging, use SQL's LIMIT
and OFFSET
directly in your Sinatra route:
get '/' do
page = params[:page].to_i
page = 1 if page < 1
limit = 10
offset = (page - 1) * limit
@posts = db.execute("SELECT * FROM posts ORDER BY created_at DESC LIMIT ? OFFSET ?", [limit, offset])
@total_posts = db.get_first_value('SELECT COUNT(*) FROM posts')
@current_page = page
smart_template(:index)
end
And in your ERB view, display pagination buttons dynamically:
<div class="pagination">
<% total_pages = (@total_posts.to_f / 10).ceil %>
<% if @current_page > 1 %>
<button hx-get="/?page=<%= @current_page - 1 %>"
hx-target="#content"
hx-swap="innerHTML">Previous</button>
<% end %>
<% if @current_page < total_pages %>
<button hx-get="/?page=<%= @current_page + 1 %>"
hx-target="#content"
hx-swap="innerHTML">Next</button>
<% end %>
</div>
This ensures fast load times, clean UX, and a backend that acts like a proper gatekeeper of database sanity.
A full Python refresher.
To explore for Python Street Cred:
os, sys, pathlib, subprocess, logging, argparse/click/typer
paramiko, fabric, psutil, pyyaml/json
requests, dotenv, venv, black, pytest
User Input: This program asks the user to guess a number and gives feedback.
number = 7
user_input = input("Enter 'y' if you would like to play: ").lower()
if user_input == "y":
user_number = int(input("Guess our number: "))
if user_number == number:
print("you guessed correctly!")
elif abs(number - user_number) == 1:
print("You were off by one.")
else:
print("sorry, it's wrong!")
Age Calculation: This program converts age into months and seconds.
age = int(input("Enter your age:"))
months = age * 12
seconds = age * 365.25 * 24 * 60
print (f"{age} years old equals to {months} months and {seconds} seconds.")
Average Grade Calculation: Calculates average grades for an individual student and a class.
- Creates a variable called student, with a dictionary.
- The dictionary must contain three keys: 'name', 'school', and 'grades'.
- The values for each must be 'Jose', 'Computing', and a tuple with the values 66, 77, and 88.
student = {
"name" : "Jose",
"school" : "Computing",
"grades" : (66, 77, 88)
}
# Assume the argument, data, is a dictionary.
# Modify the grades variable so it accesses the 'grades' key of the data dictionary.
def average_grade(data):
grades = data["grades"]
return sum(grades) / len(grades)
# Given a list of students (a list of dictionaries), calculate the average grade received on an exam, for the entire class
# You must add all the grades of all the students together
# You must also count how many grades there are in total in the entire list
def average_grade_all_students(student_list):
total = 0
count = 0
for student in student_list:
grades = student["grades"]
total += sum(grades)
count += len(grades)
return total / count
Lists: Demonstrates list comprehension for filtering and modifying lists.
numbers = [1, 3, 5]
doubled = [x * 2 for x in numbers]
friends = ["samantha", "sylvie", "adam", "rain", "anna", "sultan"]
starts_s = [friend for friend in friends if friend.startswith('s')]
print(starts_s)
Dictionary: Iterates through a dictionary of student attendance and prints results.
student_attendance = {"Rolf": 96, "Bob": 80, "Anna": 100}
for student, attendance in student_attendance.items():
print(f"{student}: {attendance}")
Dictionary 2: Implements a simple username-password authentication system.
users = [
(0, "Bob", "password"),
(1, "Rolf", "bob123"),
(2, "Jose", "longpassword"),
(3, "username", "1234")
]
username_mapping = {user[1]: user for user in users}
username_input = input("Enter your username: ")
password_input = input("Enter your password: ")
_, username, password = username_mapping[username_input]
if password_input == password:
print("Your details are correct!")
else:
print("Your details are incorrect.")
Destructure A Variable: Shows variable unpacking with tuples and lists.
-
Unpacking with
_
to ignore middle value. -
head gets the first item.
-
*tail captures the rest into a list.
-
head gets the first item.
-
*tail (with a * splat operator) captures the rest into a list.
person = ("Bob", 42, "Mechanician")
name, _, profession = person
print(name, profession)
head, *tail = [1,2,3,4,5]
print(head)
print(tail)
For Loop: Demonstrates looping through a list with a for loop.
friends = ["Anna", "Bellatrix", "Ianou", "Alexis"]
for friend in friends:
print(f"{friend} is my friend.")
While Loop: Repeats until the user decides to stop playing the guessing game.
number = 7
while True:
user_input = input("Would you like to play? (Y/n)")
if user_input == "n":
break
user_number = int(input("Guess our number: "))
if user_number == number:
print("you guessed correctly!")
elif abs(number - user_number) == 1:
print("You were off by one.")
else:
print("sorry, it's wrong!")
Unpacking: Demonstrates unpacking keyword arguments with **kwargs.
def named(**kwargs):
print(kwargs)
def print_nicely(**kwargs):
named(**kwargs)
for arg, value in kwargs.items():
print(f"{arg}: {value}")
print_nicely(name= "bob", age= "25")
Functions with Arguments: Simple function demonstrating division with error handling.
def divide (dividend, divisor):
if divisor != 0:
return dividend/divisor
else:
return "you fool!"
divide (6, 0)
Functions with Arguments 2: Shows function parameter unpacking with *args and **kwargs.
def add(x, y):
return x + y
num = [3, 5]
add(*num)
numz = {"x": 15, "y": 26}
print(add(**numz))
def multiply(*argz):
total = 1
for arg in argz:
total = total * arg
return total
def apply(*argz, operator):
if operator == "*":
return multiply(*argz)
elif operator == "+":
return sum(argz)
else:
return "no valid operator detected."
print(apply(1, 3, 5, 7, operator="+"))
Mutable Parameters: Demonstrates how default mutable arguments can cause issues.
from typing import List, Optional
class Ztudent:
def __init__(self, name: str, grades: Optional[List[int]] = None):
self.name = name
self.grades = grades or []
def take_exam(self, result: int):
self.grades.append(result)
bob = Ztudent("Bob")
rolf = Ztudent("Rolf")
bob.take_exam(90)
print(bob.grades)
print(rolf.grades)
Lambda: Demonstrates lambda functions and map.
add = lambda x , y : x + y
print(add(8,5))
def double(x):
return x*2
sequence = [1, 3, 5, 9]
doubled = [double(x) for x in sequence]
doubled2 = list(map(double, sequence))
print (doubled2)
Decorators: Demonstrates a basic function decorator for access control.
user = {"username": "jose", "access_level": "admin"}
def get_admin_password():
return "1234"
def make_secure(func):
def secure_function():
if user["access_level"] == "admin":
return func()
else:
return f"no admin permissons for {user['username']}"
return secure_function
get_admin_password = make_secure(get_admin_password)
print(get_admin_password())
Decorators With Parameters: Enhances decorators to accept parameters.
user = {"username": "jose", "access_level": "admin"}
def make_secure(func):
def secure_function(*args, **kwargs):
if user["access_level"] == "admin":
return func(*args, **kwargs)
else:
return f"no admin permissons for {user['username']}"
return secure_function
@make_secure
def get_password(panel):
if panel == "admin":
return "1234"
elif panel == "billing":
return "super_secure_password"
print(get_password("billing"))
Decorators @: Uses the @decorator syntax to apply a decorator.
user = {"username": "jose", "access_level": "admin"}
def make_secure(func):
def secure_function():
if user["access_level"] == "admin":
return func()
else:
return f"no admin permissons for {user['username']}"
return secure_function
@make_secure
def get_admin_password():
return "1234"
print(get_admin_password())
Python OOP Cheat Sheet 🐍
1. Classes & Objects
Python keeps it simple. No header files, no manually managing memory, no need to worry about your constructor causing a nuclear meltdown.
class MyClass:
def __init__(self, name):
self.name = name # `self` is Python's way of saying "this"
def greet(self):
return f"Hello, {self.name}!"
obj = MyClass("PwatPwat")
print(obj.greet()) # Output: Hello, PwatPwat!
2. Encapsulation (a.k.a Hiding Your Shame)
Use underscores to suggest "please don’t touch this" (but Python won't stop you because it believes in free will).
class Secret:
def __init__(self):
self._semi_private = "This is a suggestion."
self.__truly_private = "This is a threat."
def reveal(self):
return self.__truly_private
obj = Secret()
print(obj._semi_private) # Can still access
print(obj.reveal()) # Use methods to access private attributes
Note: __truly_private gets name-mangled into _Secret__truly_private, but if you access it directly, Python will just sigh at you.
3. Inheritance (Because Writing Code Twice is for Losers)
Python lets you inherit from multiple parents, unlike some other languages that make you jump through hoops.
class Parent:
def speak(self):
return "I am the parent."
class Child(Parent):
def cry(self):
return "Waaa!"
kid = Child()
print(kid.speak()) # Output: I am the parent.
print(kid.cry()) # Output: Waaa!
Multiple Inheritance:
class Mom:
def trait(self):
return "Inherited from Mom."
class Dad:
def trait(self):
return "Inherited from Dad."
class Kid(Mom, Dad): # Mom's trait will be used first
pass
baby = Kid()
print(baby.trait()) # Output: Inherited from Mom.
Python follows the MRO (Method Resolution Order), which basically means it checks from left to right.
4. Composition (a.k.a "Instead of Inheriting, Just Contain It")
Instead of making everything an inheritance mess, composition lets you have objects inside other objects.
class Bookshelf:
def __init__(self, *books):
self.books = books
def __str__(self):
return f"Bookshelf with {len(self.books)} Books."
class Book:
def __init__(self, name):
self.name = name
def __str__(self):
return f"Book {self.name}"
book = Book("The land is inhospitable")
book2 = Book("Charli")
shelf = Bookshelf(book, book2)
print(shelf)
When to Use Composition?
- When you need "has-a" relationships (e.g., A Bookshelf has Books).
- When inheritance doesn’t make sense (e.g., A Bookshelf is not a Book).
- When you need modularity and reusability without making a family tree out of your classes.
5. Class Methods (@classmethod
)
A class method receives the class itself (cls) as the first argument instead of an instance. This lets you create alternative constructors.
class Book:
TYPEZ = ("hardcover", "paperback")
def __init__(self, name, book_type, weight):
self.name = name
self.book_type = book_type
self.weight = weight
def __repr__(self):
return f"<Book {self.name}, {self.book_type}, weiging {self.weight}g>"
@classmethod
def hardcover(cls, name, page_weight):
return cls(name, cls.TYPEZ[0], page_weight + 100)
@classmethod
def paperback(cls, name, page_weight):
return cls(name, cls.TYPEZ[1], page_weight)
book = Book.hardcover("Laurel Hell", 1600)
light = Book.paperback("the bottle", 400)
print(book, light)
Use @classmethod
when:
- You need alternative constructors (hardcover() and paperback() in this case).
- You want to modify class-level attributes rather than instance attributes.
Class Statis Method 2: Demonstrates class methods and static methods in store management.
class Store:
def __init__(self, name):
self.name = name
self.items = []
def add_item(self, name, price):
self.items.append({
'name': name,
'price': price
})
def stock_price(self):
total = 0
for item in self.items:
total += item['price']
return total
@classmethod
def franchise(cls, store):
return cls(store.name + " - franchise")
# Return another store, with the same name as the argument's name, plus " - franchise"
@staticmethod
def store_details(store):
return f"{store.name}, total stock price: {int(store.stock_price())}"
# Return a string representing the argument
# It should be in the format 'NAME, total stock price: TOTAL'
########################################################################################
store = Store("Test")
store2 = Store("Amazon")
store2.add_item("Keyboard", 160)
Store.franchise(store) # returns a Store with name "Test - franchise"
Store.franchise(store2) # returns a Store with name "Amazon - franchise"
Store.store_details(store) # returns "Test, total stock price: 0"
Store.store_details(store2) # returns "Amazon, total stock price: 160"
6. Using super() (Because You Actually Want Your Parent Class to Do Something)
super()
lets you call methods from a parent class without hardcoding the class name. This is useful when dealing with multiple levels of inheritance.
class Animal:
def __init__(self, name):
self.name = name
def speak(self):
return "Some generic animal sound."
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # Calls the __init__ from Animal
self.breed = breed
def speak(self):
return "Woof!" # Overrides the parent class method
dog = Dog("Rex", "Golden Retriever")
print(dog.name) # Output: Rex
print(dog.speak()) # Output: Woof!
7. The __repr__
Method (For When You Actually Care About Debugging)
__repr__
is like __str__
, but it's for developers, not users. It’s meant to return a string that recreates the object.
class Person :
def __init__(self, name, age):
self.name = name
self.age = age
# def __str__(self):
# return f"Person {self.name}; {self.age} years old."
def __repr__(self):
return f"<Person({self.name}, {self.age})>"
katheryn = Person("Katheryn", 44)
print(katheryn)
Use __repr__
when:
- You want a debugging-friendly representation of an object.
- You want repr(obj) to return something meaningful (instead of <Person object at 0x1234>).
- You’re passing objects around and need better logging.
8. Metaclasses & Decorators
Python allows modifying classes at runtime and using decorators to dynamically alter functions.
def add_greeting(cls):
cls.greet = lambda self: f"Hello from {self.__class__.__name__}!"
return cls
@add_greeting
class Person:
pass
p = Person()
print(p.greet()) # Output: Hello from Person!
- This injects a method into a class at runtime.
- Python also has metaclasses, which let you dynamically change how classes behave (but it's rarely needed).
Import Code: Demonstrates module imports and function usage.
from imports_mymodule import divide #or import imports_mymodule
print(divide(10,2))
Import Module: Shows how modules define reusable functions.
def divide(divident, divisor):
return divident/divisor
print("imports_mymodule.py: ", __name__)
Type Hinting: Uses type hints for better function and class documentation.
class Book:
TYPEZ = ("hardcover", "paperback")
def __init__(self, name: str, book_type: str, weight: int):
self.name = name
self.book_type = book_type
self.weight = weight
def __repr__(self) -> str:
return f"<Book {self.name}, {self.book_type}, weiging {self.weight}g>"
@classmethod
def hardcover(cls, name: str, page_weight: int) -> "Book":
return cls(name, cls.TYPEZ[0], page_weight + 100)
@classmethod
def paperback(cls, name: str, page_weight: int) -> "Book":
return cls(name, cls.TYPEZ[1], page_weight)
book = Book.hardcover("Laurel Hell", 1600)
light = Book.paperback("the bottle", 400)
print(book, light)
First Class Function: Demonstrates passing functions as arguments to other functions.
def divide(dividend, diviser):
if diviser == 0:
raise ZeroDivisionError("DIvisor cannot be 0.")
return dividend/diviser
def calculate(*values, operator):
return operator(*values)
result = calculate(20, 4, operator=divide)
print(result)
First Class Function 2: Uses function arguments to search in a list of dictionaries.
def search(sequence, expected, finder):
for elem in sequence:
if finder(elem) == expected:
return elem
raise RuntimeError(f"Could not find an element with {expected}.")
friends = [
{"name": "Rolf Lilia", "age": 28},
{"name": "Odin the Great", "age": 877},
{"name": "Nyarlathotep", "age": 8888811},
]
def get_friend_name(friend):
return friend["name"]
print(search(friends, "Nyarlathotep", get_friend_name))
Errors: Demonstrates exception handling with try-except-finally.
def divide(dividend, divisor):
if divisor == 0:
raise ZeroDivisionError("Divisor cannot be 0.")
return dividend / divisor
grades = []
print("Welcome to the average grade program.")
try:
average = divide(sum(grades), len(grades))
except ZeroDivisionError:
print("There are no grades yet in your lizt.")
else:
print(f"THe average grade is {average}.")
finally:
print("Thank you!")
Custom Errors: Creates a custom exception class for book page validation.
class TooManyPagesError(ValueError):
pass
class Book:
def __init__(self, name: str, page_count: int):
self.name = name
self.page_count = page_count
self.pages_read = 0
def __repr__(self):
return (
f"<Book {self.name}, read {self.pages_read} out of {self.page_count}>"
)
def read(self, pages: int):
if self.pages_read + pages > self.page_count:
raise TooManyPagesError(
f"You tried to read {self.pages_read + pages} pages, but this book only has {self.page_count} pages."
)
self.pages_read += pages
print(f"you have now read {self.pages_read} pages out of {self.page_count}.")
try:
pout = Book("pout guide", 70)
pout.read(80)
except TooManyPagesError as e:
print(e)
Birthday Paradox in Go
package main
import "fmt"
func birthdayProbability(n int) float64 {
probability := 1.0
for i := 0; i < n; i++ {
probability *= float64(365-i) / 365
}
return 1 - probability
}
func main() {
const iterations = 10000
fmt.Printf("People in the room \t Probability of 2 (or more) having the same birthday\n")
for n := 10; n <= 100; n += 10 {
totalProbability := 0.0
for i := 0; i < iterations; i++ {
totalProbability += birthdayProbability(n)
}
averageProbability := totalProbability / iterations
fmt.Printf("\t%d\t\t\t%f\n", n, averageProbability)
}
fmt.Printf("Total number of iterations : %d\n", iterations)
}
Key Concepts in Go
This example introduces:
- Loops: Both
for
loops are used for iterative calculations (one to compute probabilities and another for multiple iterations). - Floating-point Arithmetic: Calculations involve
float64
for precise probability computation. - Modularity: The function
birthdayProbability
is separated from the main logic for clarity and reusability.
Overview of the Birthday Paradox
The "birthday paradox" refers to the counterintuitive probability that in a group of just 23 people, there’s a greater than 50% chance that at least two of them share the same birthday.
Real-World Applications
- Cryptography:
- Hash collisions: The birthday paradox helps understand the likelihood of two different inputs producing the same hash.
- Simulation Models:
- Event overlaps in large datasets.
- Predicting collisions in distributed systems.
Code Explanation
func birthdayProbability(n int) float64 {
probability := 1.0
for i := 0; i < n; i++ {
probability *= float64(365-i) / 365
}
return 1 - probability
}
- This function calculates the probability of at least two people having the same birthday in a group of size
n
.
for n := 10; n <= 100; n += 10 {
totalProbability := 0.0
for i := 0; i < iterations; i++ {
totalProbability += birthdayProbability(n)
}
averageProbability := totalProbability / iterations
fmt.Printf("\t%d\t\t\t%f\n", n, averageProbability)
}
- The outer loop iterates through different group sizes (
n
). - The inner loop computes the probability multiple times (
iterations
) to average out randomness.
Example output:
People in the room Probability of 2 (or more) having the same birthday
10 0.116948
20 0.411438
30 0.706316
...
Rock-Paper-Scissors Game in Go
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strings"
"time"
)
func main() {
var move, machineMove, prevMove int
const (
rock = 0
paper = 1
scissors = 2
)
const (
cRock = 'R'
cPaper = 'P'
cScissors = 'S'
)
var cMove string
var draws, wins, machineWins int
var rounds int
reader := bufio.NewReader(os.Stdin)
fmt.Print("How many rounds do you want to play? ")
fmt.Scanf("%d", &rounds)
reader.ReadString('\n') // Clear the newline character from the input buffer
var rockCounter, scissorCounter, paperCounter int
// Initialize prevMove to an invalid value
prevMove = -1
for i := 0; i < rounds; i++ {
// Player move
fmt.Println("\nRound ", i+1, ": Choose either R, P or S")
cMove, _ = reader.ReadString('\n')
cMove = strings.TrimSpace(cMove)
if cMove == "R" {
move = rock
rockCounter++
} else if cMove == "P" {
move = paper
paperCounter++
} else if cMove == "S" {
move = scissors
scissorCounter++
} else {
fmt.Println("-> Illegal move")
i--
continue // Go back to the top of the loop
}
// Reset counter if player changes their move
if prevMove != -1 {
if move != prevMove {
// fmt.Println("-> You played a different move than the previous round")
rockCounter = 0
scissorCounter = 0
paperCounter = 0
}
}
// Set machine move based on counters
if rockCounter >= 10 {
machineMove = paper
} else if scissorCounter >= 10 {
machineMove = rock
} else if paperCounter >= 10 {
machineMove = scissors
} else {
// Random Move
source := rand.NewSource(time.Now().UnixNano())
rng := rand.New(source)
machineMove = rng.Intn(3)
}
// Determine the result using switch
switch move {
case rock:
if machineMove == rock {
fmt.Println("-> draw")
draws++
} else if machineMove == paper {
fmt.Println("-> machine wins")
machineWins++
} else {
fmt.Println("-> you win")
wins++
}
case paper:
if machineMove == rock {
fmt.Println("-> you win")
wins++
} else if machineMove == paper {
fmt.Println("-> draw")
draws++
} else {
fmt.Println("-> machine wins")
machineWins++
}
case scissors:
if machineMove == rock {
fmt.Println("-> machine wins")
machineWins++
} else if machineMove == paper {
fmt.Println("-> you win")
wins++
} else {
fmt.Println("-> draw")
draws++
}
}
// Update previous move
prevMove = move
}
fmt.Println("\nAfter", rounds, "rounds:\n",
"you win: ", wins,
" machine wins ", machineWins,
", with ", draws, "draws")
}
Key Concepts in Go
This example introduces several important features of Go:
- CLI Tool Development: Learn how to create an interactive Command Line Interface (CLI) application.
- Switch Statements: See how Go handles multiple cases with
switch
for decision-making. - Randomization: The use of
math/rand
to introduce randomness. - User Input: Using
bufio.NewReader
andos.Stdin
for user interaction. - Basic State Tracking: Demonstrates counters to track repeated behavior.
- Logic for Adaptation: Implements a simple rule-based system, hinting at machine learning concepts.
Overview of the Game
This implementation of Rock-Paper-Scissors is a CLI-based game where:
- The player chooses a move: Rock (
R
), Paper (P
), or Scissors (S
). - The machine plays either:
- Randomly, for most of the game.
- Predictively, if the player repeats the same move ten times, the machine adapts to counter it.
Real-World Applications
- Game Design:
- This example can be extended to learn game mechanics or simulate AI players.
- Machine Learning Basics:
- Introduces the concept of leveraging historical data (player's repeated moves) to predict and counter future moves.
- CLI-Based Tools:
- A foundation for creating interactive command-line programs, useful in automation or user interaction in terminals.
Code Explanation
The code can be broken down into key sections:
-
Player Input and Move Validation:
reader := bufio.NewReader(os.Stdin) cMove, _ = reader.ReadString('\n') cMove = strings.TrimSpace(cMove)
- Accepts and trims the user's input.
- Validates the move (
R
,P
, orS
).
-
Machine Move Logic:
- Random Move:
source := rand.NewSource(time.Now().UnixNano()) rng := rand.New(source) machineMove = rng.Intn(3)
- Generates a random move for the machine.
- Adaptive Move:
if rockCounter >= 10 { machineMove = paper } else if scissorCounter >= 10 { machineMove = rock } else if paperCounter >= 10 { machineMove = scissors }
- Tracks repeated moves by the player and adapts to counter them.
- Random Move:
-
Game Outcome:
- Uses a
switch
to determine the result:switch move { case rock: if machineMove == rock { /* draw logic */ } else if machineMove == paper { /* machine wins */ } else { /* player wins */ } }
- Uses a
-
Result Summary:
fmt.Println("\nAfter", rounds, "rounds:\n", "you win: ", wins, " machine wins ", machineWins, ", with ", draws, "draws")
- Provides a game summary after all rounds are played.
How to Run
To play the game:
- Save the file as
rps.go
. - Run the program with:
go run rps.go
- Input the number of rounds you'd like to play.
- Play by entering your move (
R
,P
, orS
) when prompted.
Example run:
How many rounds do you want to play? 3
Round 1: Choose either R, P or S
R
-> draw
Round 2: Choose either R, P or S
P
-> machine wins
Round 3: Choose either R, P or S
P
-> you win
After 3 rounds:
you win: 1 machine wins 1 , with 1 draws
Extensions
- Add more moves like Lizard and Spock to make it more challenging.
- Implement machine learning to predict player moves based on historical patterns.
- Build a graphical user interface (GUI) for a more interactive experience.
Employee Salary Parser in Go
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
// Open the file for reading
f, err := os.Open("employee.txt")
check(err)
defer f.Close()
// Read the file line by line
scanner := bufio.NewScanner(f)
// Declare slices
var fullNames []string
var salaries []uint32
// Add data to the corresponding slice
for scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
if len(parts) == 3 {
fullName := parts[0] + " " + parts[1]
fullNames = append(fullNames, fullName)
salary, err := strconv.ParseUint(parts[2], 10, 32)
check(err)
salaries = append(salaries, uint32(salary))
} else {
fmt.Println("-> Invalid line format")
}
}
// Error handling
if err := scanner.Err(); err != nil {
check(err)
}
//Find the employee with the smallest salary
minSalary := salaries[0]
minIndex := 0
for i, salary := range salaries {
if salary < minSalary {
minSalary = salary
minIndex = i
}
}
//Find the employee with the largest salary
maxSalary := salaries[0]
maxIndex := 0
for i, salary := range salaries {
if salary > maxSalary {
maxSalary = salary
maxIndex = i
}
}
//Find the average salary
var totalSalary uint32
for _, salary := range salaries {
totalSalary += salary
}
averageSalary := float64(totalSalary) / float64(len(salaries))
fmt.Printf("Company's smallest salary: %s, with: %v\n", fullNames[minIndex], minSalary)
fmt.Printf("Company's largest salary: %s, with: %v\n", fullNames[maxIndex], maxSalary)
fmt.Printf("Company's average salary: %.2f", averageSalary)
}
Key Concepts in Go
This example introduces:
- File Handling:
- Using
os.Open
to read files andbufio.Scanner
to process them line by line.
- Using
- Error Handling:
- The
check
function demonstrates a simple way of handling errors.
- The
- String and Slice Operations:
- Parsing strings with
strings.Fields
and managing dynamic collections with slices.
- Parsing strings with
- Numeric Conversions:
- Converting string data to unsigned integers with
strconv.ParseUint
.
- Converting string data to unsigned integers with
- Basic Algorithms:
- Finding minimum, maximum, and average values in a dataset.
Overview of the Program
The program reads a file (employee.txt
) containing employee data in the format:
FirstName LastName Salary
- Example Content:
John Doe 5000 Jane Smith 7500 Alice Johnson 4000 Bob Brown 8000
- It processes this data to:
- Find the employee with the smallest salary.
- Find the employee with the largest salary.
- Calculate the average salary across all employees.
Real-World Applications
- Payroll Management:
- This example can form the basis of more comprehensive payroll systems, such as generating reports or filtering employees by salary range.
- Data Analytics:
- Parsing and analyzing structured text data for insights.
- File Processing:
- A starting point for building tools to process large datasets.
Code Explanation
The code can be divided into key steps:
-
Reading and Parsing the File:
f, err := os.Open("employee.txt") check(err) defer f.Close() scanner := bufio.NewScanner(f)
- Opens the file and initializes a scanner to read it line by line.
-
Processing Each Line:
parts := strings.Fields(line) if len(parts) == 3 { fullName := parts[0] + " " + parts[1] salary, err := strconv.ParseUint(parts[2], 10, 32) check(err) fullNames = append(fullNames, fullName) salaries = append(salaries, uint32(salary)) } else { fmt.Println("-> Invalid line format") }
- Splits each line into parts: first and last name, and salary.
- Validates the format and appends data to slices.
-
Finding Minimum and Maximum Salaries:
minSalary := salaries[0] minIndex := 0 for i, salary := range salaries { if salary < minSalary { minSalary = salary minIndex = i } }
- Iterates through the
salaries
slice to find the smallest and largest values and their indices.
- Iterates through the
-
Calculating the Average Salary:
var totalSalary uint32 for _, salary := range salaries { totalSalary += salary } averageSalary := float64(totalSalary) / float64(len(salaries))
- Computes the total salary and divides it by the number of employees.
-
Displaying Results:
fmt.Printf("Company's smallest salary: %s, with: %v\n", fullNames[minIndex], minSalary) fmt.Printf("Company's largest salary: %s, with: %v\n", fullNames[maxIndex], maxSalary) fmt.Printf("Company's average salary: %.2f", averageSalary)
- Outputs the results with appropriate formatting.
How to Run
- Example output:
Company's smallest salary: Alice Johnson, with: 4000 Company's largest salary: Bob Brown, with: 8000 Company's average salary: 6125.00
Extensions
- Input Validation:
- Add checks for negative salaries or invalid characters.
- Additional Analytics:
- Include features like median salary or salary distribution.
- Dynamic Input:
- Allow the filename to be passed as a command-line argument.
- Database Integration:
- Replace file-based storage with database queries for scalability.
MergeSort Algorithm in Go
package main
import (
"fmt"
"math/rand"
"time"
)
func merge(sortedSlice1 []int, sortedSlice2 []int) []int {
mergedSlice := make([]int, 0, len(sortedSlice1)+len(sortedSlice2))
var index1, index2 int
for index1 < len(sortedSlice1) && index2 < len(sortedSlice2) {
if sortedSlice1[index1] < sortedSlice2[index2] {
mergedSlice = append(mergedSlice, sortedSlice1[index1])
index1++
} else {
mergedSlice = append(mergedSlice, sortedSlice2[index2])
index2++
}
}
mergedSlice = append(mergedSlice, sortedSlice1[index1:]...)
mergedSlice = append(mergedSlice, sortedSlice2[index2:]...)
return mergedSlice
}
func mergeSort(items []int) []int {
if len(items) < 2 {
return items
}
mid := len(items) / 2
first := mergeSort(items[:mid])
second := mergeSort(items[mid:])
return merge(first, second)
}
func main() {
const nElements = 10000
unsortedSlice := make([]int, nElements)
// generate numbers
source := rand.NewSource(time.Now().UnixNano())
rng := rand.New(source)
for i := 0; i < nElements; i++ {
unsortedSlice[i] = rng.Intn(10000)
}
sorted := mergeSort(unsortedSlice)
fmt.Println(sorted[:100])
}
Key Concepts in Go
This example demonstrates:
- Recursive Functions:
- The
mergeSort
function showcases how recursion can simplify divide-and-conquer algorithms like MergeSort.
- The
- Slices in Go:
- Efficiently splits and merges slices in a type-safe manner.
- Custom Slice Operations:
- Implements a
merge
function to combine two sorted slices.
- Implements a
- Random Number Generation:
- Uses
math/rand
to generate a large dataset for sorting.
- Uses
Overview of MergeSort
MergeSort is a divide-and-conquer algorithm that:
- Divides the input array into two halves recursively.
- Conquers by sorting each half.
- Merges the two sorted halves into a single sorted array.
It has a time complexity of (O(n \log n)), which makes it efficient for sorting large datasets.
Why a Go Implementation? While MergeSort is traditionally implemented in lower-level languages like C for performance, the Go implementation focuses on:
- Readability: The recursive approach and slice operations make the code easy to understand.
- Ease of Use: Go's built-in slice handling eliminates the need for manual memory management.
Real-World Applications
- Database Systems:
- Efficiently sorts large datasets stored in external memory (e.g., disk or SSD).
- Parallel Computing:
- Its divide-and-conquer nature makes it suitable for parallelization.
- Merging Sorted Data:
- Combines multiple sorted data streams in distributed systems.
- Sorting Algorithms in Libraries:
- Often used as a fallback for sorting libraries when data size is too large for in-place algorithms like QuickSort.
Code Explanation
The code can be divided into logical sections:
-
Merge Function:
func merge(sortedSlice1 []int, sortedSlice2 []int) []int { mergedSlice := make([]int, 0, len(sortedSlice1)+len(sortedSlice2)) var index1, index2 int for index1 < len(sortedSlice1) && index2 < len(sortedSlice2) { if sortedSlice1[index1] < sortedSlice2[index2] { mergedSlice = append(mergedSlice, sortedSlice1[index1]) index1++ } else { mergedSlice = append(mergedSlice, sortedSlice2[index2]) index2++ } } mergedSlice = append(mergedSlice, sortedSlice1[index1:]...) mergedSlice = append(mergedSlice, sortedSlice2[index2:]...) return mergedSlice }
- Combines two sorted slices (
sortedSlice1
andsortedSlice2
) into a single sorted slice. - Maintains stability, meaning the order of equal elements is preserved.
- Combines two sorted slices (
-
MergeSort Function:
func mergeSort(items []int) []int { if len(items) < 2 { return items } mid := len(items) / 2 first := mergeSort(items[:mid]) second := mergeSort(items[mid:]) return merge(first, second) }
- Recursively divides the input slice into halves until each slice contains one or zero elements.
- Merges the sorted halves back together using the
merge
function.
-
Main Function:
func main() { const nElements = 10000 unsortedSlice := make([]int, nElements) source := rand.NewSource(time.Now().UnixNano()) rng := rand.New(source) for i := 0; i < nElements; i++ { unsortedSlice[i] = rng.Intn(10000) } sorted := mergeSort(unsortedSlice) fmt.Println(sorted[:100]) }
- Generates a slice of 10,000 random integers.
- Sorts the slice using the
mergeSort
function. - Prints the first 100 sorted elements for verification.
Example output:
[0 5 12 16 21 ... 9876 9999]
Extensions
- Parallelization:
- Use goroutines to parallelize the sorting of the two halves.
- Custom Comparators:
- Modify the
merge
function to support custom sorting criteria (e.g., descending order or by specific object attributes).
- Modify the
- Benchmarking:
- Compare the performance of this implementation with Go's built-in
sort
package.
- Compare the performance of this implementation with Go's built-in
- Visualization:
- Create a graphical representation of how MergeSort works step-by-step.
Poker Straight Flush Probability Simulation in Go
package main
import (
"fmt"
"math/rand"
"sort"
"time"
)
type Suit int
type Pip int
const (
club Suit = iota
diamond
heart
spade
)
type Card struct {
s Suit
pip Pip
}
func shuffle(d *[]Card) {
rand.Seed(time.Now().UnixNano())
rand.Shuffle(len(*d), func(i, j int) {
(*d)[i], (*d)[j] = (*d)[j], (*d)[i]
})
}
func isStraightFlush(h []Card) bool {
var ccount, dcount, hcount, scount int
var sameSuitCards []Card
for _, v := range h {
switch v.s {
case club:
ccount++
case diamond:
dcount++
case heart:
hcount++
case spade:
scount++
}
}
// Step 1 : Check if all cards are of the same suit
if ccount >= 5 || dcount >= 5 || hcount >= 5 || scount >= 5 {
// Collect all cards of the same suit
for _, v := range h {
if (ccount >= 5 && v.s == club) ||
(dcount >= 5 && v.s == diamond) ||
(hcount >= 5 && v.s == heart) ||
(scount >= 5 && v.s == spade) {
sameSuitCards = append(sameSuitCards, v)
}
}
// Step 2 : Sort the cards by pip value
sort.Slice(sameSuitCards, func(i, j int) bool {
return sameSuitCards[i].pip < sameSuitCards[j].pip
})
// Step 3 : Check if all cards are in sequence
consecutive := 1
for i := 1; i < len(sameSuitCards); i++ {
if sameSuitCards[i].pip == sameSuitCards[i-1].pip+1 {
consecutive++
if consecutive == 5 {
return true
}
} else if sameSuitCards[i].pip == sameSuitCards[i-1].pip {
consecutive = 1
}
}
}
return false
}
func main() {
deck := make([]Card, 52)
var sfcount int // number of straight flushes
var totcnt int // Number of trials
fmt.Print("Enter the number of trials: ")
_, err := fmt.Scanln(&totcnt)
if err != nil {
fmt.Println("Invalid input. Please enter a valid number.")
return
}
// Initialize the deck
for i := 0; i < 13; i++ {
deck[i] = Card{club, Pip(i + 1)}
deck[i+13] = Card{diamond, Pip(i + 1)}
deck[i+26] = Card{heart, Pip(i + 1)}
deck[i+39] = Card{spade, Pip(i + 1)}
}
// Run the trials
for i := 0; i < totcnt; i++ {
shuffle(&deck)
hand := deck[:7]
if isStraightFlush(hand) {
sfcount++
}
}
fmt.Printf("\nStraight flushes for %d trials: %d \n", totcnt, sfcount)
fmt.Printf("Probability of straight flush: %.8f\n", float64(sfcount)/float64(totcnt))
}
Key Concepts in Go
This example introduces:
-
Enumerated Types with
iota
:- Go uses the
iota
keyword to simplify the declaration of constants. It automatically increments for each line in theconst
block.
const ( club Suit = iota diamond heart spade )
- Here,
club
is assigned0
,diamond
is1
,heart
is2
, andspade
is3
.
- Go uses the
-
The
for _,
Loop:- Go uses the
for
loop for all iterations, replacingwhile
anddo-while
loops in C. - The
_
is a placeholder when you don’t need the index from the loop.
for _, v := range h { switch v.s { case club: ccount++ case diamond: dcount++ case heart: hcount++ case spade: scount++ } }
- Here, the loop iterates through the
h
slice, andv
holds each element of the slice.
- Go uses the
-
Slices and Sorting:
- Slices in Go allow dynamic resizing. The
sort.Slice
function sorts slices based on a custom comparison.
sort.Slice(sameSuitCards, func(i, j int) bool { return sameSuitCards[i].pip < sameSuitCards[j].pip })
- Slices in Go allow dynamic resizing. The
-
Randomization:
- The
rand.Shuffle
function shuffles the deck using a time-based seed for randomness.
- The
-
Probability Estimation:
- The program calculates the probability of a straight flush by dividing the number of straight flushes by the total number of trials.
Overview of the Program
This program simulates the probability of getting a straight flush in a 7-card hand from a shuffled deck. It involves:
- User Input:
- The user provides the number of trials to simulate.
- Deck Initialization:
- A standard 52-card deck is created, with each card represented by a
Suit
and aPip
(rank).
- A standard 52-card deck is created, with each card represented by a
- Shuffling and Hand Selection:
- The deck is shuffled, and the top 7 cards form the hand.
- Straight Flush Detection:
- The hand is analyzed to check if it contains a straight flush.
- Probability Calculation:
- The program calculates the percentage of hands containing a straight flush after all trials.
Real-World Applications
- Game Theory and Probability:
- This program demonstrates the mathematical principles behind poker probabilities.
- Monte Carlo Simulations:
- Randomized simulations like this are used in fields ranging from finance to physics to model complex systems.
- Card Game Algorithms:
- Understanding the odds of different hands helps in designing and balancing card games.
Code Explanation
The code can be broken down into key sections:
-
Deck Initialization:
for i := 0; i < 13; i++ { deck[i] = Card{club, Pip(i + 1)} deck[i+13] = Card{diamond, Pip(i + 1)} deck[i+26] = Card{heart, Pip(i + 1)} deck[i+39] = Card{spade, Pip(i + 1)} }
- Creates a 52-card deck with 13 ranks (
1-13
) for each suit.
- Creates a 52-card deck with 13 ranks (
-
Shuffling:
func shuffle(d *[]Card) { rand.Seed(time.Now().UnixNano()) rand.Shuffle(len(*d), func(i, j int) { (*d)[i], (*d)[j] = (*d)[j], (*d)[i] }) }
- Shuffles the deck in place using the
rand.Shuffle
function.
- Shuffles the deck in place using the
-
Straight Flush Detection:
- The
isStraightFlush
function checks if the hand contains a straight flush:- Counts cards of each suit.
- Collects cards of the same suit.
- Sorts the cards by rank (
pip
). - Checks if the sorted cards form a sequence of 5 consecutive ranks:
consecutive := 1 for i := 1; i < len(sameSuitCards); i++ { if sameSuitCards[i].pip == sameSuitCards[i-1].pip+1 { consecutive++ if consecutive == 5 { return true } } else if sameSuitCards[i].pip == sameSuitCards[i-1].pip { consecutive = 1 } }
- The
-
Main Function:
- Runs the simulation:
for i := 0; i < totcnt; i++ { shuffle(&deck) hand := deck[:7] if isStraightFlush(hand) { sfcount++ } }
- Calculates and prints the probability:
fmt.Printf("Probability of straight flush: %.8f\n", float64(sfcount)/float64(totcnt))
- Runs the simulation:
How to Run
- Enter the number of trials when prompted:
Enter the number of trials: 10000
- Example output:
Straight flushes for 10000 trials: 12 Probability of straight flush: 0.00120000
Extensions
- Additional Hands:
- Extend the program to detect and calculate probabilities for other poker hands (e.g., four of a kind, full house).
- Parallelization:
- Use goroutines to run multiple trials in parallel for faster simulations.
- Graphical Analysis:
- Visualize the probabilities of different hands using a library like
gonum/plot
.
- Visualize the probabilities of different hands using a library like
- Card Representation:
- Add a string representation for cards to display the hands more clearly.
Doubly Linked List and Palindrome Checker in Go
// Doubly linked list to store a sequence of characters and determine if it is a palindrome
package main
import (
"bufio"
"fmt"
"os"
"strings"
)
type ListElement struct {
data rune // data of the element
next *ListElement // pointer to the next element
prev *ListElement // pointer to the previous element
}
func createListElement(data rune, ptr *ListElement) *ListElement {
var element ListElement
element.data = data
element.next = ptr
if ptr != nil {
element.prev = ptr.prev
}
return &element
}
func (h *ListElement) PrintList() {
if h == nil {
fmt.Println("List is empty")
return
}
fmt.Printf("%c -> ", h.data)
h.next.PrintList()
}
func AddToFront(dataSlice []rune, h **ListElement) {
head := *h
for _, v := range dataSlice {
head = createListElement(v, head)
}
*h = head
}
func AddToRear(dataSlice []rune, h **ListElement) {
head := *h
for _, v := range dataSlice {
newElement := createListElement(v, nil)
if head != nil {
head.next = newElement
}
head = newElement
}
}
func DeleteFront(h **ListElement) {
head := *h
if head == nil {
return
}
*h = head.next
if head.next != nil {
head.next.prev = nil
}
}
func DeleteRear(h **ListElement) {
head := *h
if head == nil {
return
}
for head.next != nil {
head = head.next
}
if head.prev != nil {
head.prev.next = nil
} else {
*h = nil
}
}
func FindValue(value rune, h *ListElement) *ListElement {
if h == nil {
return nil
}
if h.data == value {
return h
}
return FindValue(value, h.next)
}
func DeleteValue(value rune, h **ListElement) {
head := *h
if head == nil {
return
}
if head.data == value {
DeleteFront(h)
return
}
for head.next != nil {
if head.next.data == value {
head.next = head.next.next
if head.next != nil {
head.next.prev = head
}
return
}
head = head.next
}
}
func IsEmpty(h *ListElement) bool {
if h == nil {
return true
}
return false
}
func FindLength(h *ListElement) int {
if h == nil {
return 0
}
return 1 + FindLength(h.next)
}
func InsertPosition(value rune, position int, h **ListElement) {
head := *h
if position < 0 {
return
}
if position == 0 {
*h = createListElement(value, head)
return
}
for i := 0; i < position-1; i++ {
if head == nil {
return
}
head = head.next
}
if head == nil {
return
}
head.next = createListElement(value, head.next)
}
func DeletePosition(position int, h **ListElement) {
head := *h
if position < 0 {
return
}
if position == 0 {
DeleteFront(h)
return
}
for i := 0; i < position-1; i++ {
if head == nil {
return
}
head = head.next
}
if head == nil {
return
}
head.next = head.next.next
if head.next != nil {
head.next.prev = head
}
}
func IsPalindrome(h *ListElement) bool {
if h == nil {
return false
}
// Find the tail of the List
tail := h
for tail.next != nil {
tail = tail.next
}
// Iterate from both ends towards the middle
for h != nil && tail != nil && h != tail && h.prev != tail {
if h.data != tail.data {
return false
}
h = h.next
tail = tail.prev
}
return true
}
func main() {
var head *ListElement
fmt.Print("Type a word into the terminal to check if it is a palindrome: \n")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Error reading input")
return
}
input = strings.TrimSpace(input)
// Convert the input string to a slice of runes
dataslice := ([]rune)(input)
// Add the input to the front of the doubly linked list
AddToFront(dataslice, &head)
fmt.Println("Added to front")
head.PrintList()
fmt.Println()
// Test if the input is a palindrome
fmt.Println("Is the input a palindrome? ")
fmt.Println(IsPalindrome(head))
fmt.Println()
// Test the other doubly linked list functions
fmt.Println("Testing the doubly linked list functions")
AddToRear(dataslice, &head)
fmt.Println("Added to rear")
head.PrintList()
fmt.Println()
FindValue('a', head)
if FindValue('a', head) != nil {
fmt.Println("Value 'a' found")
} else {
fmt.Println("Value 'a' not found")
}
head.PrintList()
fmt.Println()
if FindValue('a', head) != nil {
fmt.Println("Deleted value 'a'")
DeleteValue('a', &head)
} else {
fmt.Println("Value 'a' not found and not deleted")
}
IsEmpty(head)
if IsEmpty(head) {
fmt.Println("List is empty")
} else {
fmt.Println("List is not empty")
}
fmt.Println()
FindLength(head)
fmt.Println("Length of the list is: ", FindLength(head))
InsertPosition('a', 0, &head)
fmt.Println("Inserted 'a' at position 0")
head.PrintList()
fmt.Println()
DeletePosition(0, &head)
fmt.Println("Deleted position 0")
head.PrintList()
fmt.Println()
DeleteFront(&head)
fmt.Println("Deleted front element")
head.PrintList()
fmt.Println()
DeleteRear(&head)
fmt.Println("Deleted rear element")
head.PrintList()
fmt.Println()
}
Key Concepts in Go
This example showcases:
- Doubly Linked List Implementation:
- A doubly linked list is a data structure where each element (node) contains pointers to both its previous and next elements.
- The list supports operations such as insertion, deletion, traversal, and searching.
- Palindrome Detection:
- Checks whether a word reads the same forwards and backwards using the doubly linked list.
- Error Handling:
- Handles potential errors gracefully, such as invalid input or operations on an empty list.
- Recursive and Iterative Approaches:
- Functions like
PrintList
use recursion, while others likeDeleteRear
use iteration.
- Functions like
Overview of the Program
The program implements a doubly linked list to:
- Store a sequence of characters (as
rune
data type). - Perform various linked list operations such as adding, deleting, searching, and inserting nodes.
- Check if an input word is a palindrome using the doubly linked list.
Real-World Applications
- String Manipulation:
- Palindrome detection is often used in text processing and cryptography.
- Data Structure Learning:
- Doubly linked lists are fundamental data structures used in memory management, undo/redo operations, and navigation systems.
- Error-Resilient Algorithms:
- Demonstrates how to handle edge cases like empty structures and invalid operations.
Code Explanation
-
Struct Definition:
type ListElement struct { data rune // data of the element next *ListElement // pointer to the next element prev *ListElement // pointer to the previous element }
- Defines the structure for each node in the doubly linked list.
-
Core Linked List Functions:
-
Adding to Front:
func AddToFront(dataSlice []rune, h **ListElement) { head := *h for _, v := range dataSlice { head = createListElement(v, head) } *h = head }
- Iterates through the input data and adds each character to the front of the list.
-
Deleting from Rear:
func DeleteRear(h **ListElement) { head := *h if head == nil { return } for head.next != nil { head = head.next } if head.prev != nil { head.prev.next = nil } else { *h = nil } }
- Traverses to the end of the list and removes the last element.
-
Palindrome Check:
func IsPalindrome(h *ListElement) bool { if h == nil { return false } tail := h for tail.next != nil { tail = tail.next } for h != nil && tail != nil && h != tail && h.prev != tail { if h.data != tail.data { return false } h = h.next tail = tail.prev } return true }
- Compares characters from both ends of the list to determine if the word is a palindrome.
-
-
Error Handling:
- Handles invalid input gracefully:
reader := bufio.NewReader(os.Stdin) input, err := reader.ReadString('\n') if err != nil { fmt.Println("Error reading input") return }
- Checks for empty lists in operations like
DeleteFront
andDeleteRear
.
- Handles invalid input gracefully:
-
Main Function:
- Accepts user input and checks if the word is a palindrome:
fmt.Print("Type a word into the terminal to check if it is a palindrome: \n") input = strings.TrimSpace(input) dataslice := ([]rune)(input) AddToFront(dataslice, &head) fmt.Println("Is the input a palindrome? ") fmt.Println(IsPalindrome(head))
- Tests various linked list operations to verify their correctness.
- Accepts user input and checks if the word is a palindrome:
How to Run
- Enter a word when prompted to check if it is a palindrome:
Type a word into the terminal to check if it is a palindrome: racecar
- Example output:
Added to front r -> a -> c -> e -> c -> a -> r -> Is the input a palindrome? true
Extensions
- Error Reporting:
- Add more informative error messages for invalid operations.
- Enhanced Palindrome Check:
- Ignore spaces, punctuation, and capitalization when checking for palindromes.
- Performance Improvements:
- Optimize functions like
FindLength
to avoid redundant calculations.
- Optimize functions like
- Interactive CLI:
- Allow users to perform linked list operations interactively.
3D Volume Calculator Using Interfaces in Go
package main
import (
"bufio"
"fmt"
"math"
"os"
"strconv"
"strings"
)
type Solid interface {
Volume() float64
}
type Sphere struct {
radius float64
}
type Cube struct {
length float64
}
type Pyramid struct {
base float64
height float64
}
func (s Sphere) Volume() float64 {
return 4 * math.Pi * math.Pow(s.radius, 3) / 3
}
func (l Cube) Volume() float64 {
return math.Pow(l.length, 3)
}
func (p Pyramid) Volume() float64 {
return math.Pow(p.base, 2) * p.height / 3
}
func main() {
fmt.Println("Reading data.txt")
file, err := os.Open("data.txt")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer file.Close()
var solids []Solid
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
parts := strings.Fields(line)
if len(parts) != 3 {
fmt.Println("Invalid line format: ", line)
continue
}
shapeType := parts[0]
dimension1, err1 := strconv.ParseFloat(parts[1], 64)
dimension2, err2 := strconv.ParseFloat(parts[2], 64)
if err1 != nil || err2 != nil {
fmt.Println("Invalid number format in line:", line)
continue
}
switch shapeType {
case "S":
solids = append(solids, Sphere{radius: dimension1})
case "C":
solids = append(solids, Cube{length: dimension1})
case "P":
solids = append(solids, Pyramid{base: dimension1, height: dimension2})
default:
fmt.Println("Unknown shape type in line:", line)
}
}
if err := scanner.Err(); err != nil {
fmt.Println("Error reading file", err)
}
for _, solid := range solids {
fmt.Printf("Volume: %.2f\n", solid.Volume())
}
}
Key Concepts in Go
-
Interfaces
- An interface defines a set of method signatures. Any type that implements those methods satisfies the interface.
- Here, the
Solid
interface requires theVolume() float64
method. - This allows different shapes (Sphere, Cube, Pyramid) to be treated uniformly when calculating volume.
-
Structs and Methods
Sphere
,Cube
, andPyramid
are struct types, each with their own fields and aVolume()
method matching theSolid
interface.
-
Input Parsing and File Handling
- Uses
bufio.Scanner
andos.Open
to read and parse input from a file. - Handles errors gracefully for file operations and input format.
- Uses
-
Switch Statements
- The code uses a
switch
to select the correct struct type based on the input.
- The code uses a
Overview of the Program
-
The program reads a file (
data.txt
) where each line describes a solid:- The first letter is the shape type:
C
for CubeP
for PyramidS
for Sphere
- The following two numbers are dimensions:
- For
Cube
: side length, second value unused (set as 0.0 in file) - For
Sphere
: radius, second value unused (set as 0.0) - For
Pyramid
: base length and height
- For
- The first letter is the shape type:
-
For each line, the corresponding shape’s volume is calculated using methods that implement the interface, and the result is printed.
Example Input (data.txt
)
C 2.5 0.0
P 3 6.0
S 4.5 0.0
...
Code Explanation
-
Solid Interface and Structs:
type Solid interface { Volume() float64 } type Sphere struct { radius float64 } type Cube struct { length float64 } type Pyramid struct { base, height float64 }
-
Volume Methods:
func (s Sphere) Volume() float64 { return 4 * math.Pi * math.Pow(s.radius, 3) / 3 } func (l Cube) Volume() float64 { return math.Pow(l.length, 3) } func (p Pyramid) Volume() float64 { return math.Pow(p.base, 2) * p.height / 3 }
-
Input Handling:
- Reads each line, splits fields, converts numeric strings to float64, and selects the shape with a
switch
. - Handles invalid or unknown input gracefully.
- Reads each line, splits fields, converts numeric strings to float64, and selects the shape with a
-
Processing:
for _, solid := range solids { fmt.Printf("Volume: %.2f\n", solid.Volume()) }
How to Run
- Prepare a
data.txt
file with one shape per line as above. - Save your Go file as
interfaces.go
. - Run with:
go run interfaces.go
- Sample output:
Volume: 15.63 Volume: 18.00 Volume: 381.70 ...
Extensions & Real-World Applications
- Adding More Shapes:
Implement more 3D solids by creating new structs and theirVolume
methods—no change to the main logic needed. - Polymorphism:
Interfaces allow for flexible, extensible, and decoupled code, a key principle in large Go programs. - Error Reporting:
Improve error messages, or log and skip bad lines in larger data files. - Practical Use:
Useful for any geometry processing, CAD software, or scientific computation where multiple shape types are handled generically.
Dining Philosophers Problem in Go
// Philosopher's problem, exploring concurrency in Go
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
const (
numPhilosophers = 5
numForks = 5
numMeals = 3
)
type Philosopher struct {
id int
leftFork *sync.Mutex
rightFork *sync.Mutex
ladle *sync.Mutex
}
func (p *Philosopher) eat(wg *sync.WaitGroup) {
defer wg.Done()
for i := 0; i < numMeals; i++ {
// think
think := rand.Intn(5) + 1
fmt.Printf("Philosopher %d is thinking for %d seconds\n", p.id, think)
time.Sleep(time.Duration(think) * time.Second)
// pick up ladle
p.ladle.Lock()
fmt.Printf("Philosopher %d used the ladle\n", p.id)
// pick up fork
p.leftFork.Lock()
fmt.Printf("Philosopher %d picked up left fork\n", p.id)
p.rightFork.Lock()
fmt.Printf("Philosopher %d picked up right fork\n", p.id)
// eat after picking up two forks
eat := rand.Intn(5) + 1
fmt.Printf("Philosopher %d is eating for %d seconds\n", p.id, eat)
time.Sleep(time.Duration(eat) * time.Second)
// put down forks
p.leftFork.Unlock()
fmt.Printf("Philosopher %d put down the left fork\n", p.id)
p.rightFork.Unlock()
fmt.Printf("Philosopher %d put down the right fork\n", p.id)
// Put down ladle
p.ladle.Unlock()
fmt.Printf("Philosopher %d put down the ladle\n", p.id)
}
}
func main() {
forks := make([]*sync.Mutex, numForks)
for i := range forks {
forks[i] = &sync.Mutex{}
}
ladle := &sync.Mutex{}
philosophers := make([]*Philosopher, numPhilosophers)
for i := range philosophers {
leftFork := forks[i]
rightFork := forks[(i+1)%numForks]
philosophers[i] = &Philosopher{id: i + 1, leftFork: leftFork, rightFork: rightFork, ladle: ladle}
}
var wg sync.WaitGroup
wg.Add(numPhilosophers)
for _, p := range philosophers {
go p.eat(&wg)
}
wg.Wait()
}
Key Concepts in Go
-
Concurrency with Goroutines
- Go’s goroutines are lightweight threads managed by the Go runtime, making concurrent programming simple and efficient.
- Each philosopher runs as a separate goroutine, simulating independent actors that can execute and be scheduled concurrently.
-
Mutual Exclusion with Mutexes
- Uses
sync.Mutex
to represent forks and a ladle (shared resource), ensuring that only one philosopher can access each at a time. - Prevents race conditions and ensures data consistency.
- Uses
-
Deadlock Avoidance
- The classic dining philosophers problem risks deadlocks if every philosopher picks up one fork and waits for another.
- This implementation introduces a ladle (a single mutex all philosophers must acquire before picking up forks), serializing entry to the critical section and eliminating deadlock risk.
-
WaitGroup Synchronization
sync.WaitGroup
is used to wait for all philosopher goroutines to finish their meals before the main program exits.
Overview of the Program
- Problem: Five philosophers sit at a table with five forks. Each needs two forks to eat. This classic concurrency problem explores synchronization and deadlock.
- Go Solution:
- Each philosopher is modeled as a goroutine.
- Forks and the ladle are mutexes.
- Philosophers repeatedly “think,” then attempt to eat by acquiring the ladle and both adjacent forks.
- After eating, they release all resources and think again.
Real-World Applications of Concurrency
- Resource Scheduling: This pattern is useful for modeling systems where many agents need exclusive access to a limited set of resources (e.g., database locks, printer queues).
- Professional Use: Concurrency is fundamental in backend services, networking, simulations, and distributed systems.
- Go in Production: Go’s model is widely used in cloud infrastructure, microservices, high-performance servers, and real-time applications.
Code Explanation
-
Philosopher Struct:
type Philosopher struct { id int leftFork *sync.Mutex rightFork *sync.Mutex ladle *sync.Mutex }
- Each philosopher keeps track of their forks and the shared ladle.
-
Eating Logic:
func (p *Philosopher) eat(wg *sync.WaitGroup) { defer wg.Done() for i := 0; i < numMeals; i++ { // Think (simulate with sleep) // Lock ladle (serialize access) // Lock left then right fork (mutexes) // Eat (simulate with sleep) // Unlock all in reverse order } }
- The philosopher must acquire the ladle before forks, then eat, then release all.
-
Launching Goroutines:
for _, p := range philosophers { go p.eat(&wg) } wg.Wait()
How to Run
- Save the file as
philosopher.go
. - Run the program:
go run philosopher.go
- Example output:
Philosopher 1 is thinking for 3 seconds Philosopher 2 is thinking for 5 seconds ... Philosopher 1 used the ladle Philosopher 1 picked up left fork Philosopher 1 picked up right fork Philosopher 1 is eating for 2 seconds ...
Extensions and Professional Tips
- Alternative Deadlock Solutions: Explore solutions such as ordering resource acquisition or using semaphores.
- Performance Tuning: In high-concurrency systems, analyze lock contention and consider lock-free or channel-based designs.
- Production Use: Always test concurrent code with race detection (
go run -race
); concurrency bugs can be subtle.
Would you like to document another concurrency example or go deeper into Go’s concurrency patterns?
Generic Stack Implementation and Testing in Go
genstack.go
// A package to implement a generic stack in Go
package genstack
import "fmt"
type Stack[T any] struct {
vals []interface{}
}
func (s *Stack[T]) Push(val interface{}) {
s.vals = append(s.vals, val)
}
func (s *Stack[T]) isEmpty() bool {
return len(s.vals) == 0
}
func (s *Stack[T]) Pop() (val interface{}, err error) {
if s.isEmpty() {
var zero T
return zero, fmt.Errorf("Stack is empty")
}
val = s.vals[len(s.vals)-1]
s.vals = s.vals[:len(s.vals)-1]
return val, nil
}
func (s *Stack[T]) Top() (val interface{}, err error) {
if s.isEmpty() {
var zero T
return zero, fmt.Errorf("stack is empty")
}
return s.vals[len(s.vals)-1], nil
}
// Fill the stack from a slice
func (s *Stack[T]) CopyFromSlice(slice []interface{}) {
for _, val := range slice {
s.Push(val)
}
}
// Pops the stack contents into a slice
func (s *Stack[T]) CopyToSlice() []interface{} {
var slice []interface{}
for !s.isEmpty() {
val, err := s.Pop()
if err != nil {
break
}
slice = append(slice, val)
}
return slice
}
func main() {
fmt.Println("Stacks")
var intStack Stack[int]
fmt.Println(intStack)
intStack.Push(15)
intStack.Push("dog")
intStack.Push(25)
fmt.Println(intStack)
fmt.Println(intStack.isEmpty())
// Copy stack contents to a slice
slice := intStack.CopyToSlice()
fmt.Println("Slice:", slice)
fmt.Println("Stack after CopyToSlice:", intStack)
intStack.CopyFromSlice(slice)
fmt.Println("Stack after CopyFromSlice:", intStack)
}
genstack_test.go
// Running unit tests for genstack package
package genstack
import (
"testing"
)
func TestPushPop(t *testing.T) {
stack := Stack[int]{}
stack.Push(10)
stack.Push(20)
val, err := stack.Pop()
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
if val != 20 {
t.Errorf("Expected 20, got %v", val)
}
val2, err2 := stack.Pop()
if err2 != nil {
t.Errorf("Unexpected error: %v", err)
}
if val2 != 10 {
t.Errorf("Expected 10, got %v", val2)
}
_, err = stack.Pop()
if err == nil {
t.Errorf("Expected error, got nil")
}
}
func TestIsEmpty(t *testing.T) {
stack := Stack[int]{}
if !stack.isEmpty() {
t.Errorf("Expected stack to be empty")
}
stack.Push(10)
if stack.isEmpty() {
t.Errorf("Expected stack to be non-empty")
}
stack.Pop()
if !stack.isEmpty() {
t.Errorf("Expected stack to be empty")
}
}
Key Concepts in Go
-
Generics
- The stack is defined as
Stack[T any]
, using Go’s generics to allow for stacks of any type. - This provides flexibility and type safety, a major feature added in Go 1.18+.
- The stack is defined as
-
Interfaces and Type Flexibility
- Internally, the stack stores values as
[]interface{}
. This enables pushing different types onto the stack, but means type assertions or checks may be necessary when popping.
- Internally, the stack stores values as
-
Idiomatic Error Handling
- Both
Pop()
andTop()
return an error if the stack is empty, following Go’s convention of explicit error returns rather than exceptions.
- Both
-
Testing with the
testing
Package- Testing in Go is simple and built-in: functions beginning with
Test
in a*_test.go
file are automatically discovered and run withgo test
.
- Testing in Go is simple and built-in: functions beginning with
Overview of the Program
- genstack.go implements a generic stack, supporting:
- Pushing and popping elements
- Checking if the stack is empty
- Copying stack contents to/from slices
- genstack_test.go provides unit tests to verify core stack behavior, ensuring reliability and correctness.
Test File Explanation (genstack_test.go
)
-
TestPushPop
- Pushes integers onto the stack, pops them, and checks order (LIFO: last-in, first-out).
- Ensures errors are correctly raised when popping from an empty stack.
-
TestIsEmpty
- Verifies that the stack correctly reports empty and non-empty states as elements are pushed and popped.
Example:
func TestPushPop(t *testing.T) {
stack := Stack[int]{}
stack.Push(10)
stack.Push(20)
val, err := stack.Pop()
if val != 20 { /* ... */ }
// ... further checks ...
}
How to Run the Tests
- Ensure both
genstack.go
andgenstack_test.go
are in the same directory/package. - Run:
go test
- Output:
ok theflyoccultist/hello-go/genstack 0.002s
Real-World Use Cases
-
Testing in Go:
- Automated tests are crucial for reliability, especially when implementing generic data structures.
- Go's testing framework is simple, fast, and integrates with tools for continuous integration and code coverage.
-
Generic Data Structures:
- Stacks are used in parsing, algorithms, interpreter runtimes, undo/redo features, and much more.
Extensions
- Type Safety:
- Consider using
[]T
instead of[]interface{}
for stricter type guarantees (now possible with Go generics).
- Consider using
- More Operations:
- Add Peek, Size, or Clear methods for a more complete stack implementation.
- More Tests:
- Add tests for copying to/from slices, pushing mixed types, or concurrent usage.
Dijkstra's Algorithm in Go
package main
import "fmt"
// Edge represents a connection between two nodes
type Edge struct {
Source int
Destination int
Weight int
}
// Graph represents a graph with a list of edges
type Graph struct {
vertices int
edges []Edge
}
const large = 999999
// NewGraph creates a new graph with a given number of vertices
func NewGraph(vertices int) *Graph {
return &Graph{
vertices: vertices,
edges: make([]Edge, 0),
}
}
// AddEdge adds an edge to the graph
func (g *Graph) AddEdge(source, destination, weight int) {
g.edges = append(g.edges, Edge{source, destination, weight})
g.edges = append(g.edges, Edge{destination, source, weight})
}
// Dijkstra calculates the shortest path from a source node to all other nodes
func (g *Graph) Dijkstra(source int) []int {
distances := make([]int, g.vertices)
visited := make([]bool, g.vertices)
for i := range distances {
distances[i] = large
}
distances[source] = 0
for i := 0; i < g.vertices-1; i++ {
u := g.minDistance(distances, visited)
visited[u] = true
for _, edge := range g.edges {
if !visited[edge.Destination] && edge.Source == u {
newDistance := distances[u] + edge.Weight
if newDistance < distances[edge.Destination] {
distances[edge.Destination] = newDistance
}
}
}
}
return distances
}
func (g *Graph) minDistance(distances []int, visited []bool) int {
minDist := large
minIndex := -1
for v := 0; v < g.vertices; v++ {
if !visited[v] && distances[v] <= minDist {
minDist = distances[v]
minIndex = v
}
}
return minIndex
}
func main() {
g := NewGraph(9)
// Add edges to the graph
g.AddEdge(0, 1, 4)
g.AddEdge(0, 7, 8)
g.AddEdge(1, 2, 8)
g.AddEdge(1, 7, 11)
g.AddEdge(2, 3, 7)
g.AddEdge(2, 8, 2)
g.AddEdge(2, 5, 4)
g.AddEdge(3, 4, 9)
g.AddEdge(3, 5, 14)
g.AddEdge(4, 5, 10)
g.AddEdge(5, 6, 2)
g.AddEdge(6, 7, 1)
g.AddEdge(6, 8, 6)
g.AddEdge(7, 8, 7)
// Calculate the shortest path from node 0 to all other nodes
distances := g.Dijkstra(0)
// Print the shortest path to all nodes
fmt.Println("Shortest path from node 0 to all other nodes:")
for i, distance := range distances {
fmt.Printf("Node %d: %d\n", i, distance)
}
}
Key Concepts in Go
-
Graph Representation
- The graph is modeled using a
Graph
struct with a slice ofEdge
structs. - Each
Edge
holds a source, destination, and weight.
- The graph is modeled using a
-
Dijkstra's Algorithm
- Finds the shortest path from a source node to all other nodes in a weighted graph with non-negative weights.
- The implementation uses basic slices for distances and visited nodes, focusing on simplicity and readability.
-
Idiomatic Go
- Makes use of Go’s slices, custom struct types, and method receivers for clean, modular code.
- Uses zero-based indexing and initialization patterns familiar to Go developers.
Overview of the Program
- Purpose:
Finds the shortest path from a starting node (node 0) to all other nodes in a sample undirected weighted graph. - How It Works:
- A graph is created and edges are added.
- The
Dijkstra
method computes shortest paths from node 0. - Results are printed to the terminal.
Code Explanation
-
Structs and Graph Construction
type Edge struct { Source, Destination, Weight int } type Graph struct { vertices int; edges []Edge }
- The graph is undirected: each
AddEdge
call inserts both directions.
- The graph is undirected: each
-
Dijkstra’s Algorithm
func (g *Graph) Dijkstra(source int) []int { distances := make([]int, g.vertices) visited := make([]bool, g.vertices) // Initialize all distances to a large value // Main loop: pick unvisited node with smallest distance, update neighbors }
- Uses a simple linear search for the next node (not a heap/priority queue), prioritizing clarity over speed.
-
Result Output
for i, distance := range distances { fmt.Printf("Node %d: %d\n", i, distance) }
How to Run
- Save the file as
dijkstra.go
. - Run the program:
go run dijkstra.go
- Expected output:
Shortest path from node 0 to all other nodes: Node 0: 0 Node 1: 4 Node 2: 12 Node 3: 19 Node 4: 21 Node 5: 11 Node 6: 9 Node 7: 8 Node 8: 14
Real-World Applications
- Navigation & Routing:
GPS, mapping software, network packet routing. - Game Development:
Pathfinding for AI, dynamic environments. - Operations Research:
Logistics, resource allocation, project planning.
Extensions
- Performance:
For larger graphs, replace the linear search inminDistance
with a min-heap (priority queue). - Directed Graphs:
ModifyAddEdge
to add only one direction for directed graphs. - Path Reconstruction:
Track predecessors to reconstruct the actual shortest path, not just distances.
Why Go?
- Simplicity:
Go’s syntax allows for clear and concise code, reducing boilerplate. - Concurrency:
While not used here, Go excels at concurrent computation—useful for parallel pathfinding in large or dynamic graphs. - Readability:
Easier for teams to understand and maintain compared to lower-level languages.