Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

🕯️ 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 your setup.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

  1. Open a Terminal

    Navigate to the directory where you want to clone the repository.

    cd /path/to/your/directory
    
  2. 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
    
  3. 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

namekokyufeature
静岡 アダメ地獄の呼吸突進
竜宮城炎の呼吸眉毛の二段
岡山 悟水の呼吸天然
大豆の子
鱗滝水の呼吸師匠

table name: eva

namekawaiirole
レイ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
namefeature
静岡 アダメ突進
竜宮城眉毛の二段
岡山 悟天然
大豆の子
鱗滝師匠

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;
namekokyufeature
静岡 アダメ地獄の呼吸突進
竜宮城炎の呼吸眉毛の二段
岡山 悟水の呼吸天然
大豆の子
鱗滝水の呼吸師匠

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;
namekawaii
レイ10
ゆい6

AND: Add more conditions to your WHERE record query

SELECT *
  FROM eva
WHERE "kawaii" > 5 AND "role" = 'パイロット';
namekawaiirole
レイ10パイロット

OR: The record appeals to either those conditions

SELECT *
  FROM eva
WHERE "kawaii" > 5 OR "role" = 'パイロット';
namekawaiirole
レイ10パイロット
アスカ3パイロット
ゆい6

BETWEEN

SELECT *
  FROM eva
WHERE "kawaii" BETWEEN 4 AND 6;
namekawaiirole
ゆい6
ミサト4作戦部長

IN, NOT IN

SELECT *
  FROM eva
WHERE "role" IN ('パイロット', '作戦部長');
namekawaiirole
レイ10パイロット
アスカ3パイロット
ミサト4作戦部長
SELECT *
  FROM eva
WHERE "role" NOT IN ('パイロット', '作戦部長');
namekawaiirole
ゆい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
namekawaiirole
アスカ3パイロット

IS NULL/ IS NOT NULL: Look For Empty Data / Not Empty Data

SELECT *
  FROM eva
 WHERE "role" IS NULL;
namekawaiirole
ゆい6

LIMIT: When You Don't Want To Query The Entire Column

  • SQL result rows start at 1 when displayed, but LIMIT and OFFSET are 0-based. So LIMIT 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;
namekawaiirole
レイ10パイロット
アスカ3パイロット

ORDER BY: Sort

SELECT *
  FROM eva
ORDER BY "kawaii";
namekawaiirole
アスカ3パイロット
ミサト4作戦部長
ゆい6
レイ10パイロット
SELECT *
  FROM eva
ORDER BY "kawaii" DESC;
namekawaiirole
レイ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"), not AVG "age". PostgreSQL will absolutely lose it otherwise.

We will work with this table

table name: members

namecreated_daychannelage
エレン2021-02-13web27
こういち2021-02-13ad27
さゆり2021-02-15ad27
上谷2021-02-15ad33
あかり2021-02-16web24

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_daycount
2021-02-132
2021-02-152
2021-02-161
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_daychannelcount
2021-02-13web1
2021-02-13ad1
2021-02-15ad2
2021-02-16web1

Aggregation functions: (集計関数)

Aggregates values

  • COUNT: number of records
  • AVG: the average value
  • MAX: the maximum
  • MIN: the minimum
  • SUM: the total
SELECT "created_day", AVG("age"), MAX("age")
  FROM members
GROUP BY "created_day";
created_dayavgmax
2021-02-132727
2021-02-153033
2021-02-162424

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

idname
1ハリー
2ハーマイオニー
3ロン
4ダンブルドア
5ヴォルデモート

table name: histories

idmartians_idplanet
13地球
21木星
34土星
45海王星

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;
idnameidmartians_idplanet
1ハリー21木星
3ロン13地球
4ダンブルドア34土星
5ヴォルデモート45海王星

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.nameh.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.nameh.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.nameh.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.nameh.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_namepopulation
京都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:

districtpopulation
関西300
関西900
九州500
九州100

Step 2:

districtpopulation
関西1200
九州600
  • With this, you can use SQL like a real programming language
  • Using GROUP BY and SUM 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_namepopulationrank
大阪9001
福岡5002
京都3003
佐賀1004

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 CASEmess 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 a SELECT? Well, you actually can.
  • You start becoming a query magician.

We will work with this table

table name: items

namecategoryprice
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
);
namecategoryprice
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"
  );
namecategoryprice
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 even FROM 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:

  1. Header Inclusion:

    #include <stdio.h>
    

    The stdio.h library is included for input and output functions, specifically for using printf.

  2. 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).

  3. Function Definition:

    enum month get_month(enum month m) {
        return(m);
    }
    

    The function get_month takes an argument of type enum 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.

  4. Main Function:

    int main()
    {
        printf("%u\n", get_month(apr));
        return 0;
    }
    

    The main function:

    • Calls get_month with the apr 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.

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

  1. Enum Definition:

    • enum day {sun, mon, tue, wed, thu, fri, sat};
      Defines an enumerated type day to represent days of the week.
  2. print_day Function:

    • Takes an enum day value as input and prints the corresponding day name using a switch statement. If the input is invalid, it prints an error message.
  3. next_day Function:

    • Takes an enum day value as input and computes the next day based on modulo arithmetic.
  4. previous_day Function:

    • Takes an enum day value as input and computes the previous day based on modulo arithmetic.
  5. 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.

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 and case 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 like printf.
  • stdlib.h: Provides functions like rand 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 and 999999999.

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 as MAX - 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 the data 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.

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 the FILL macro.
  • Prints the generated weights using the print_data function.

Usage

  1. Compile the program using a C compiler, e.g., gcc weight_generator.c -o weight_generator.
  2. Run the program: ./weight_generator.
  3. 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:

  1. Deck Setup:

    • Defines suits (CLUB, DIAMOND, HEART, SPADE) and ranks (Ace to King) of cards.
    • Creates an array of 52 cards (deck) and initializes it with all possible combinations of suits and ranks.
  2. Operations on the Deck:

    • Initialization: Fills the deck with cards in order.
    • Shuffling: Randomly shuffles the deck using the Fisher-Yates algorithm.
  3. 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.
  4. 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.
  5. Other Details:

    • Uses global counters to tally hand outcomes.
    • Employs rand() for randomness and initializes it with the current time using srand(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 using printf and scanf.

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 and j: 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 size n 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

  1. Modular Design:
    • The sorting logic is encapsulated in the bubbleSort function.
    • The swap function enhances code readability and reusability.
  2. 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

  1. 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.
  2. 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 like exit().

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, and putc(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.
  • 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:

    1. Print the contents of the input file to the terminal using print_file().
    2. Double-space the input file's contents into the output file using double_space().
    3. Print the contents of the output file to the terminal.
    4. Close both files to release resources.
  • Example Usage:

    ./double_space_file input.txt output.txt
    
    • Reads input.txt, double-spaces its content, and writes the result to output.txt.

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, and 5 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

  1. 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.
  2. Sorting Mechanism:
    • Bubble Sort algorithm adapted for linked lists.
  3. 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 like printf.
  • 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 and max.

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

  1. Linked List Manipulation:
    • Demonstrates creation, traversal, and memory management.
  2. Sorting Algorithm:
    • Adapts Bubble Sort for linked lists, showcasing its flexibility.
  3. Random Number Handling:
    • Generates randomized inputs to simulate real-world scenarios.

Limitations

  1. Inefficient Sorting:
    • Bubble Sort has (O(n^2)) complexity, making it unsuitable for large datasets.
  2. Memory Overhead:
    • The program uses dynamic memory allocation, which requires careful management to prevent leaks.

Possible Improvements

  1. Sorting Efficiency:
    • Replace Bubble Sort with a more efficient algorithm like Merge Sort.
  2. Dynamic Size:
    • Allow the user to specify the size of the list at runtime.
  3. 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:

  1. Add 10 to the list.
  2. Add 20 to the list.
  3. Add 30 to the list.
  4. Traverse the list forward.
  5. Traverse the list backward.
  6. Delete 20 from the list.
  7. Traverse the list forward again.

Example Output

Step-by-Step Output:

  1. Adding 10 to the list:
    List: 10

  2. Adding 20 to the list:
    List: 10 <-> 20

  3. Adding 30 to the list:
    List: 10 <-> 20 <-> 30

  4. Traversing forward:
    Forward: 10 -> 20 -> 30

  5. Traversing backward:
    Backward: 30 -> 20 -> 10

  6. Deleting 20 from the list:
    List: 10 <-> 30

  7. 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:

  1. split_list
    This function divides the doubly linked list into two halves. The midpoint is typically found using a slow and fast pointer approach.

  2. 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

  1. Bidirectional Traversal:
    The biggest advantage is the ability to traverse both forwards and backwards. This makes certain algorithms and operations easier to implement.

  2. 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.

  3. 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

  1. Increased Memory Usage:
    Each node requires an extra pointer (prev), which doubles the memory used for pointers.

  2. Reduced Performance:
    Due to the extra pointer, operations like insertion and deletion involve slightly more overhead for managing the prev pointer.

  3. 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:

  1. The first number (5) specifies the number of integers to insert into the binary tree.
  2. 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, and endwin
  • Cursor positioning with move()
  • Replacing printf with printw because this is now a windowed world.
// 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(), and noecho() (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 via getmaxyx()
  • Classic C-style string handling (char[] and strlen())
// 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 with wgetnstr() for actual safety.


🎮 Chapter 7: Building a Menu with Arrow Keys (Real Game Dev Vibes)

Concepts introduced:

  • Menu system with const char* and keypad()
  • 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, but getch() times out after tenths of a second.
  • nodelay(stdscr, TRUE) – makes getch() non-blocking entirely.
  • timeout(ms)getch() blocks for up to ms 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;
}

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 like A_BOLD, A_REVERSE, etc.
  • color_pair: The index of the color pair (from init_pair).
  • opts: Always NULL. Reserved for future cursed extensions.

The Family Tree:

FunctionContextCursor Affects?Uses a WINDOW?Coordinates?
chgat()stdscrYesNoCurrent pos
mvchgat()stdscrYesNoMoves cursor
wchgat()custom windowYesYesCurrent pos
mvwchgat()custom windowYesYesMoves 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() (or wrefresh()) 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() or wrefresh().
  • 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 every waddch(), 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 like free() 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:

  1. Open Neovim.
  2. Run :Mason and search for clangd.
  3. 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;
}
  1. 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.
  1. 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;
}
  1. 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.
  1. 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.
  1. 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.
  1. 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.
  1. 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

  1. Encapsulation
  • brand and speed are private, meaning they cannot be accessed outside of the class.
  • Access is provided through getter functions (getBrand(), getSpeed()).
  1. 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.
  1. 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.
  1. 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

  1. 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.
  1. 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

  1. Automatic Object Lifetime Management
  • myCar and mySportsCar are stack-allocated.
  • No need for delete because destructors run automatically when they go out of scope.
  1. Calling Methods on Objects
  • myCar.accelerate(); increases the speed by 10.
  • mySportsCar.turboBoost(); is only available in SportsCar.
  1. 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) {}
  1. 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).
  1. Parameterized Constructor (Point(double xVal, double yVal))
Point(double xVal, double yVal) : x(xVal), y(yVal) {}

Allows creating a Point object with custom values.

  1. 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.
  1. 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.

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;
}
  1. 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 base student constructor.
  • Base class constructed first.
  • student_id and gpa are protected, so accessible to derived class.

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 reuses student::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 both student and grad_student objects.
  • Without virtual, the base class's print() is called.
  • pgs calls the derived version because it's typed as grad_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 a Point3D 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 CaseInheritance?
You need multiple related types that behave differentlyYes
You want to generalize with base class pointers or referencesYes
You’re sharing behavior across unrelated classesNo, use composition/templates
You don’t want to deal with destructor landminesUse 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() calls B::print_i() because pb points to a B object.
  • Second pb->print_i() calls D::print_i() because pb now points to a D 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() and area().

🧪 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 an rvalue 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::moveThe 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::exchangeThe 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 to b.a, and reset b.a to nullptr, 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

ConceptIncantationWhat 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

  1. 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.
  1. Function works for multiple data types
  • substract(a, 3) works for int.
  • substract(b, 3) works for double.
  1. 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.
  1. 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

  1. template makes Summable a generic class
  • This class works with any type that supports the += operator.
  • We create Summable and Summable instances separately.
  1. 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.
  1. 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 returns .
#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 function f(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 callables
  • std::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 average O(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 like std::transform or std::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() and std::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 an Edge struct holding source, destination, and weight.
  • Dijkstra’s Algorithm: Classic implementation using a distances[] array and a greedy loop with minDistance() 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 any delete[]: 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 nodes
  • std::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 and Spotlight::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 or unsigned (as long as it's integral).
  • Custom card data type: modeled using enum class for suits and a pips 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 using new and delete, something you would use if you're asking for problems. Vectors also provides iterator support for algorithms like std::shuffle and std::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 with 2 → Full House
    • Two keys with value 2 → Two Pair
    • One key with value 2 → One Pair

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

FeatureDFSBFS
Search StyleGoes deep along a pathExplores all neighbors level by level
Data StructureStack (or recursion)Queue
Memory UseLower in wide graphsHigher 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 pathFinding 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 * +

  1. Read 3 → push to the stack 🥞 Stack: [3]

  2. Read 5 → push to the stack 🥞 Stack: [3, 5]

  3. Read 2 → push to the stack 🥞 Stack: [3, 5, 2]

  4. Read * → pop 2 and 5, multiply them → push result

    • 5 * 2 = 10 🥞 Stack: [3, 10]
  5. Read + → pop 10 and 3, add them → push result

    • 3 + 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 6
  • 3 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 CaseGo WithWhy
You’re building real objects (people, dragons, buttons)ClassThey’re blueprints for things
You just want to slap on some extra behaviorModuleThey’re like trait makeup kits
You’re trying to share methods across multiple classesModuleDRY, reusable, non-instantiable
You need to instantiate somethingClassModules 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

  1. Install Sinatra
gem install sinatra
  1. Basic app.rb Template
require 'sinatra'

get '/' do
  erb :index
end
  1. Create Views
<!-- views/index.erb -->
<h1>Hello, world</h1>
  1. Run It
ruby app.rb
  1. 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:

  1. CLI Tool Development: Learn how to create an interactive Command Line Interface (CLI) application.
  2. Switch Statements: See how Go handles multiple cases with switch for decision-making.
  3. Randomization: The use of math/rand to introduce randomness.
  4. User Input: Using bufio.NewReader and os.Stdin for user interaction.
  5. Basic State Tracking: Demonstrates counters to track repeated behavior.
  6. 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

  1. Game Design:
    • This example can be extended to learn game mechanics or simulate AI players.
  2. Machine Learning Basics:
    • Introduces the concept of leveraging historical data (player's repeated moves) to predict and counter future moves.
  3. 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:

  1. 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, or S).
  2. 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.
  3. 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 */ }
      }
      
  4. 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:

  1. Save the file as rps.go.
  2. Run the program with:
    go run rps.go
    
  3. Input the number of rounds you'd like to play.
  4. Play by entering your move (R, P, or S) 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

  1. Add more moves like Lizard and Spock to make it more challenging.
  2. Implement machine learning to predict player moves based on historical patterns.
  3. 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:

  1. File Handling:
    • Using os.Open to read files and bufio.Scanner to process them line by line.
  2. Error Handling:
    • The check function demonstrates a simple way of handling errors.
  3. String and Slice Operations:
    • Parsing strings with strings.Fields and managing dynamic collections with slices.
  4. Numeric Conversions:
    • Converting string data to unsigned integers with strconv.ParseUint.
  5. 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:
    1. Find the employee with the smallest salary.
    2. Find the employee with the largest salary.
    3. Calculate the average salary across all employees.

Real-World Applications

  1. Payroll Management:
    • This example can form the basis of more comprehensive payroll systems, such as generating reports or filtering employees by salary range.
  2. Data Analytics:
    • Parsing and analyzing structured text data for insights.
  3. File Processing:
    • A starting point for building tools to process large datasets.

Code Explanation

The code can be divided into key steps:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. Input Validation:
    • Add checks for negative salaries or invalid characters.
  2. Additional Analytics:
    • Include features like median salary or salary distribution.
  3. Dynamic Input:
    • Allow the filename to be passed as a command-line argument.
  4. 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:

  1. Recursive Functions:
    • The mergeSort function showcases how recursion can simplify divide-and-conquer algorithms like MergeSort.
  2. Slices in Go:
    • Efficiently splits and merges slices in a type-safe manner.
  3. Custom Slice Operations:
    • Implements a merge function to combine two sorted slices.
  4. Random Number Generation:
    • Uses math/rand to generate a large dataset for sorting.

Overview of MergeSort

MergeSort is a divide-and-conquer algorithm that:

  1. Divides the input array into two halves recursively.
  2. Conquers by sorting each half.
  3. 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

  1. Database Systems:
    • Efficiently sorts large datasets stored in external memory (e.g., disk or SSD).
  2. Parallel Computing:
    • Its divide-and-conquer nature makes it suitable for parallelization.
  3. Merging Sorted Data:
    • Combines multiple sorted data streams in distributed systems.
  4. 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:

  1. 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 and sortedSlice2) into a single sorted slice.
    • Maintains stability, meaning the order of equal elements is preserved.
  2. 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.
  3. 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

  1. Parallelization:
    • Use goroutines to parallelize the sorting of the two halves.
  2. Custom Comparators:
    • Modify the merge function to support custom sorting criteria (e.g., descending order or by specific object attributes).
  3. Benchmarking:
    • Compare the performance of this implementation with Go's built-in sort package.
  4. 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:

  1. Enumerated Types with iota:

    • Go uses the iota keyword to simplify the declaration of constants. It automatically increments for each line in the const block.
    const (
        club Suit = iota
        diamond
        heart
        spade
    )
    
    • Here, club is assigned 0, diamond is 1, heart is 2, and spade is 3.
  2. The for _, Loop:

    • Go uses the for loop for all iterations, replacing while and do-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, and v holds each element of the slice.
  3. 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
    })
    
  4. Randomization:

    • The rand.Shuffle function shuffles the deck using a time-based seed for randomness.
  5. 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:

  1. User Input:
    • The user provides the number of trials to simulate.
  2. Deck Initialization:
    • A standard 52-card deck is created, with each card represented by a Suit and a Pip (rank).
  3. Shuffling and Hand Selection:
    • The deck is shuffled, and the top 7 cards form the hand.
  4. Straight Flush Detection:
    • The hand is analyzed to check if it contains a straight flush.
  5. Probability Calculation:
    • The program calculates the percentage of hands containing a straight flush after all trials.

Real-World Applications

  1. Game Theory and Probability:
    • This program demonstrates the mathematical principles behind poker probabilities.
  2. Monte Carlo Simulations:
    • Randomized simulations like this are used in fields ranging from finance to physics to model complex systems.
  3. 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:

  1. 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.
  2. 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.
  3. 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
            }
        }
        
  4. 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))
      

How to Run

  1. Enter the number of trials when prompted:
    Enter the number of trials: 10000
    
  2. Example output:
    Straight flushes for 10000 trials: 12 
    Probability of straight flush: 0.00120000
    

Extensions

  1. Additional Hands:
    • Extend the program to detect and calculate probabilities for other poker hands (e.g., four of a kind, full house).
  2. Parallelization:
    • Use goroutines to run multiple trials in parallel for faster simulations.
  3. Graphical Analysis:
    • Visualize the probabilities of different hands using a library like gonum/plot.
  4. 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:

  1. 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.
  2. Palindrome Detection:
    • Checks whether a word reads the same forwards and backwards using the doubly linked list.
  3. Error Handling:
    • Handles potential errors gracefully, such as invalid input or operations on an empty list.
  4. Recursive and Iterative Approaches:
    • Functions like PrintList use recursion, while others like DeleteRear use iteration.

Overview of the Program

The program implements a doubly linked list to:

  1. Store a sequence of characters (as rune data type).
  2. Perform various linked list operations such as adding, deleting, searching, and inserting nodes.
  3. Check if an input word is a palindrome using the doubly linked list.

Real-World Applications

  1. String Manipulation:
    • Palindrome detection is often used in text processing and cryptography.
  2. Data Structure Learning:
    • Doubly linked lists are fundamental data structures used in memory management, undo/redo operations, and navigation systems.
  3. Error-Resilient Algorithms:
    • Demonstrates how to handle edge cases like empty structures and invalid operations.

Code Explanation

  1. 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.
  2. 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.
  3. 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 and DeleteRear.
  4. 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.

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

  1. Error Reporting:
    • Add more informative error messages for invalid operations.
  2. Enhanced Palindrome Check:
    • Ignore spaces, punctuation, and capitalization when checking for palindromes.
  3. Performance Improvements:
    • Optimize functions like FindLength to avoid redundant calculations.
  4. 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

  1. Interfaces

    • An interface defines a set of method signatures. Any type that implements those methods satisfies the interface.
    • Here, the Solid interface requires the Volume() float64 method.
    • This allows different shapes (Sphere, Cube, Pyramid) to be treated uniformly when calculating volume.
  2. Structs and Methods

    • Sphere, Cube, and Pyramid are struct types, each with their own fields and a Volume() method matching the Solid interface.
  3. Input Parsing and File Handling

    • Uses bufio.Scanner and os.Open to read and parse input from a file.
    • Handles errors gracefully for file operations and input format.
  4. Switch Statements

    • The code uses a switch to select the correct struct type based on the input.

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 Cube
      • P for Pyramid
      • S 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 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.
  • Processing:

    for _, solid := range solids {
        fmt.Printf("Volume: %.2f\n", solid.Volume())
    }
    

How to Run

  1. Prepare a data.txt file with one shape per line as above.
  2. Save your Go file as interfaces.go.
  3. Run with:
    go run interfaces.go
    
  4. 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 their Volume 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

  1. 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.
  2. 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.
  3. 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.
  4. 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

  1. Save the file as philosopher.go.
  2. Run the program:
    go run philosopher.go
    
  3. 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

  1. 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+.
  2. 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.
  3. Idiomatic Error Handling

    • Both Pop() and Top() return an error if the stack is empty, following Go’s convention of explicit error returns rather than exceptions.
  4. 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 with go test.

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

  1. Ensure both genstack.go and genstack_test.go are in the same directory/package.
  2. Run:
    go test
    
  3. 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).
  • 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

  1. Graph Representation

    • The graph is modeled using a Graph struct with a slice of Edge structs.
    • Each Edge holds a source, destination, and weight.
  2. 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.
  3. 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:
    1. A graph is created and edges are added.
    2. The Dijkstra method computes shortest paths from node 0.
    3. 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.
  • 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

  1. Save the file as dijkstra.go.
  2. Run the program:
    go run dijkstra.go
    
  3. 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 in minDistance with a min-heap (priority queue).
  • Directed Graphs:
    Modify AddEdge 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.