{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "da8b18a1",
   "metadata": {},
   "source": [
    "# Strings\n",
    "\n",
    "```{index} string\n",
    "```"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "ch02-strings-setup",
   "metadata": {
    "editable": true,
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:14.876886Z",
     "iopub.status.busy": "2026-10-23T03:50:14.876823Z",
     "iopub.status.idle": "2026-10-23T03:50:15.047049Z",
     "shell.execute_reply": "2026-10-23T03:50:15.046686Z"
    },
    "slideshow": {
     "slide_type": ""
    },
    "tags": [
     "hide-input",
     "thebe-init",
     "runnable"
    ]
   },
   "outputs": [],
   "source": [
    "import sys\n",
    "from pathlib import Path\n",
    "\n",
    "current = Path.cwd()\n",
    "for parent in [current, *current.parents]:\n",
    "    if (parent / '_config.yml').exists():\n",
    "        project_root = parent  # ← Add project root, not chapters\n",
    "        break\n",
    "else:\n",
    "    project_root = Path.cwd().parent.parent\n",
    "\n",
    "sys.path.insert(0, str(project_root))\n",
    "\n",
    "from shared import thinkpython, diagram, jupyturtle"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "503f6daf",
   "metadata": {},
   "source": [
    "```{contents}\n",
    ":local:\n",
    ":depth: 2\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b403f73c",
   "metadata": {},
   "source": [
    "A great deal of business data is text: customer names, email addresses, product codes, and free-text survey answers. Text that people type is often messy. The same answer can arrive as `\"Yes\"`, `\" yes \"`, and `\"YES\"`, and a product code such as `\"SKU-1024-BLUE\"` packs several facts into one string.\n",
    "\n",
    "{doc}`Python Syntax <0201-py-syntax>` introduced strings: how to create them with quotes, join them with `+`, repeat them with `*`, and measure them with `len()`. This section adds the tools you need to work with text data:\n",
    "\n",
    "- select parts of a string by position,\n",
    "- clean text with string methods,\n",
    "- split a string into pieces and join pieces back together,\n",
    "- format numbers as currency and percentages, and\n",
    "- loop over the characters and words in a string.\n",
    "\n",
    "These skills carry forward. In Chapter 4, pandas applies the same methods to a whole column of text at once through `.str`, for example `df[\"email\"].str.lower()`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "71b95823",
   "metadata": {},
   "source": [
    "## Indexing and Slicing Strings\n",
    "\n",
    "```{index} string; indexing\n",
    "```\n",
    "\n",
    "```{index} string; slicing\n",
    "```\n",
    "\n",
    "A string is a **sequence** of characters, so it uses the same indexing and slicing rules as a list ({doc}`Lists <0203-lists>`): positions start at `0`, negative indexes count from the end, and a slice `[start:stop]` includes `start` but not `stop`. Spaces and punctuation count as characters too.\n",
    "\n",
    "Many business identifiers have a fixed layout, so slicing can pull out each part."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "3ee27df3",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.048500Z",
     "iopub.status.busy": "2026-10-23T03:50:15.048392Z",
     "iopub.status.idle": "2026-10-23T03:50:15.050619Z",
     "shell.execute_reply": "2026-10-23T03:50:15.050259Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "13\n",
      "S\n",
      "E\n",
      "SKU\n",
      "1024\n",
      "BLUE\n"
     ]
    }
   ],
   "source": [
    "code = \"SKU-1024-BLUE\"\n",
    "\n",
    "print(len(code))          ### 13 characters, including the hyphens\n",
    "print(code[0])            ### 'S', the first character\n",
    "print(code[-1])           ### 'E', the last character\n",
    "print(code[:3])           ### 'SKU', positions 0, 1, 2\n",
    "print(code[4:8])          ### '1024', positions 4 to 7\n",
    "print(code[-4:])          ### 'BLUE', the last four characters"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bf71215d",
   "metadata": {},
   "source": [
    "Slicing works the same way on any fixed-layout value. In this invoice ID, positions 3 to 6 hold the year and the last five characters hold the invoice number."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "a58df4dd",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.051645Z",
     "iopub.status.busy": "2026-10-23T03:50:15.051580Z",
     "iopub.status.idle": "2026-10-23T03:50:15.053210Z",
     "shell.execute_reply": "2026-10-23T03:50:15.052960Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Year: 2026\n",
      "Number: 00042\n"
     ]
    }
   ],
   "source": [
    "invoice_id = \"INV2026-00042\"\n",
    "\n",
    "year = invoice_id[3:7]\n",
    "number = invoice_id[-5:]\n",
    "\n",
    "print(\"Year:\", year)\n",
    "print(\"Number:\", number)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "ch02-strings-slicing-exercise-q",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.054010Z",
     "iopub.status.busy": "2026-10-23T03:50:15.053953Z",
     "iopub.status.idle": "2026-10-23T03:50:15.055391Z",
     "shell.execute_reply": "2026-10-23T03:50:15.055064Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Slice an Order ID\n",
    "# Order IDs use the layout ORD-YYYY-MM-NNNN.\n",
    "# 1. Use slicing to extract the prefix, year, month, and sequence number.\n",
    "# 2. Print each part.\n",
    "# Print exactly:\n",
    "# Prefix: ORD\n",
    "# Year: 2026\n",
    "# Month: 10\n",
    "# Sequence: 0587\n",
    "order_id = \"ORD-2026-10-0587\"\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "ch02-strings-slicing-exercise-a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.056262Z",
     "iopub.status.busy": "2026-10-23T03:50:15.056212Z",
     "iopub.status.idle": "2026-10-23T03:50:15.057808Z",
     "shell.execute_reply": "2026-10-23T03:50:15.057522Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Prefix: ORD\n",
      "Year: 2026\n",
      "Month: 10\n",
      "Sequence: 0587\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "order_id = \"ORD-2026-10-0587\"\n",
    "\n",
    "print(\"Prefix:\", order_id[:3])\n",
    "print(\"Year:\", order_id[4:8])\n",
    "print(\"Month:\", order_id[9:11])\n",
    "print(\"Sequence:\", order_id[-4:])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a6f3ad67",
   "metadata": {},
   "source": [
    "## Cleaning Text with String Methods\n",
    "\n",
    "```{index} string method\n",
    "```\n",
    "\n",
    "A **string method** is a function that belongs to a string. You call it with the dot operator, just like the list methods in {doc}`Lists <0203-lists>`: `text.method()`. String methods do not change the original string. Each one **returns a new string**, which you can print or save."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "38bff7d0",
   "metadata": {},
   "source": [
    "### Changing Case\n",
    "\n",
    "```{index} lower() and upper()\n",
    "```\n",
    "\n",
    "```{index} title()\n",
    "```\n",
    "\n",
    "`lower()`, `upper()`, and `title()` return copies of a string in lowercase, uppercase, or title case (first letter of each word capitalized). Converting to one case before comparing makes `\"Yes\"` and `\"YES\"` count as the same answer."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "230060f0",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.058629Z",
     "iopub.status.busy": "2026-10-23T03:50:15.058564Z",
     "iopub.status.idle": "2026-10-23T03:50:15.060145Z",
     "shell.execute_reply": "2026-10-23T03:50:15.059872Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "alice johnson\n",
      "ALICE JOHNSON\n",
      "Alice Johnson\n",
      "False\n",
      "True\n"
     ]
    }
   ],
   "source": [
    "name = \"aLiCe johnson\"\n",
    "\n",
    "print(name.lower())                 ### 'alice johnson'\n",
    "print(name.upper())                 ### 'ALICE JOHNSON'\n",
    "print(name.title())                 ### 'Alice Johnson'\n",
    "\n",
    "print(\"YES\" == \"yes\")               ### False: case matters\n",
    "print(\"YES\".lower() == \"yes\")       ### True after converting the case"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "34106e97",
   "metadata": {},
   "source": [
    "### Removing Extra Spaces\n",
    "\n",
    "```{index} strip()\n",
    "```\n",
    "\n",
    "```{index} whitespace\n",
    "```\n",
    "\n",
    "`strip()` removes **whitespace** (spaces, tabs, and newline characters) from both ends of a string; `lstrip()` and `rstrip()` remove it from only the left or right end. Spaces in the middle are kept. Square brackets in the output below show where each string starts and ends."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "fcd6377a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.060957Z",
     "iopub.status.busy": "2026-10-23T03:50:15.060900Z",
     "iopub.status.idle": "2026-10-23T03:50:15.062434Z",
     "shell.execute_reply": "2026-10-23T03:50:15.062188Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[   Yes  \n",
      "]\n",
      "[Yes]\n",
      "[Yes  \n",
      "]\n"
     ]
    }
   ],
   "source": [
    "response = \"   Yes  \\n\"\n",
    "\n",
    "print(f\"[{response}]\")              ### spaces and a newline around Yes\n",
    "print(f\"[{response.strip()}]\")      ### [Yes]\n",
    "print(f\"[{response.lstrip()}]\")     ### only the left side is removed"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cebdca9e",
   "metadata": {},
   "source": [
    "Methods can be **chained**: each method runs on the result of the one before it. `strip().lower()` is a common first step for cleaning survey answers."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "edd62c55",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.063265Z",
     "iopub.status.busy": "2026-10-23T03:50:15.063209Z",
     "iopub.status.idle": "2026-10-23T03:50:15.064632Z",
     "shell.execute_reply": "2026-10-23T03:50:15.064337Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "yes\n"
     ]
    }
   ],
   "source": [
    "response = \"   Yes  \\n\"\n",
    "clean = response.strip().lower()\n",
    "print(clean)                        ### 'yes'"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "02356904",
   "metadata": {},
   "source": [
    "### Replacing Text\n",
    "\n",
    "```{index} replace()\n",
    "```\n",
    "\n",
    "`replace(old, new)` returns a copy with every occurrence of `old` replaced by `new`. Replacing with the empty string `\"\"` deletes characters. A price exported as text, such as `\"$1,250.00\"`, must lose its `$` and `,` before {doc}`type conversion <0201-py-syntax>` with `float()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "5e2e590f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.065571Z",
     "iopub.status.busy": "2026-10-23T03:50:15.065506Z",
     "iopub.status.idle": "2026-10-23T03:50:15.067053Z",
     "shell.execute_reply": "2026-10-23T03:50:15.066769Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1250.00\n",
      "2500.0\n"
     ]
    }
   ],
   "source": [
    "price_text = \"$1,250.00\"\n",
    "\n",
    "digits = price_text.replace(\"$\", \"\").replace(\",\", \"\")\n",
    "price = float(digits)\n",
    "\n",
    "print(digits)                       ### '1250.00', still a string\n",
    "print(price * 2)                    ### 2500.0, now a number"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "06b3c6d7",
   "metadata": {},
   "source": [
    "### Searching Text\n",
    "\n",
    "```{index} in operator; strings\n",
    "```\n",
    "\n",
    "```{index} startswith() and endswith()\n",
    "```\n",
    "\n",
    "```{index} find()\n",
    "```\n",
    "\n",
    "Several tools answer questions about what a string contains:\n",
    "\n",
    "| Tool | Returns | Example |\n",
    "|---|---|---|\n",
    "| `part in text` | `True` or `False` | `\"@\" in email` |\n",
    "| `text.startswith(part)` | `True` or `False` | `code.startswith(\"SKU\")` |\n",
    "| `text.endswith(part)` | `True` or `False` | `email.endswith(\".edu\")` |\n",
    "| `text.find(part)` | index of the first match, or `-1` | `email.find(\"@\")` |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "303f4cff",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.067922Z",
     "iopub.status.busy": "2026-10-23T03:50:15.067864Z",
     "iopub.status.idle": "2026-10-23T03:50:15.069512Z",
     "shell.execute_reply": "2026-10-23T03:50:15.069212Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "True\n",
      "True\n",
      "False\n",
      "7\n",
      "-1\n"
     ]
    }
   ],
   "source": [
    "email = \"pat.lee@example.com\"\n",
    "\n",
    "print(\"@\" in email)                 ### True\n",
    "print(email.endswith(\".com\"))       ### True\n",
    "print(email.startswith(\"sales\"))    ### False\n",
    "print(email.find(\"@\"))              ### 7, the index of \"@\"\n",
    "print(email.find(\"#\"))              ### -1, not found"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d65232a4",
   "metadata": {},
   "source": [
    "Because `find()` returns a position, you can combine it with slicing. Everything after the `@` is the email domain."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "5d54c18d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.070319Z",
     "iopub.status.busy": "2026-10-23T03:50:15.070263Z",
     "iopub.status.idle": "2026-10-23T03:50:15.071601Z",
     "shell.execute_reply": "2026-10-23T03:50:15.071364Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "example.com\n"
     ]
    }
   ],
   "source": [
    "at = email.find(\"@\")\n",
    "domain = email[at + 1:]\n",
    "print(domain)                       ### 'example.com'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "ch02-strings-cleaning-exercise-q",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.072408Z",
     "iopub.status.busy": "2026-10-23T03:50:15.072340Z",
     "iopub.status.idle": "2026-10-23T03:50:15.073778Z",
     "shell.execute_reply": "2026-10-23T03:50:15.073489Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Clean Survey Responses\n",
    "# 1. Create an empty list named cleaned.\n",
    "# 2. Loop over responses; strip spaces and convert each answer to lowercase.\n",
    "# 3. Append each cleaned answer to cleaned.\n",
    "# 4. Count how many cleaned answers equal \"yes\".\n",
    "# Print exactly:\n",
    "# Cleaned: ['yes', 'no', 'yes', 'maybe', 'yes']\n",
    "# Yes count: 3\n",
    "responses = [\"  Yes\", \"no \", \"YES\", \" Maybe \", \"yes\"]\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "ch02-strings-cleaning-exercise-a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.074563Z",
     "iopub.status.busy": "2026-10-23T03:50:15.074513Z",
     "iopub.status.idle": "2026-10-23T03:50:15.076135Z",
     "shell.execute_reply": "2026-10-23T03:50:15.075893Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Cleaned: ['yes', 'no', 'yes', 'maybe', 'yes']\n",
      "Yes count: 3\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "responses = [\"  Yes\", \"no \", \"YES\", \" Maybe \", \"yes\"]\n",
    "\n",
    "cleaned = []\n",
    "for response in responses:\n",
    "    cleaned.append(response.strip().lower())\n",
    "\n",
    "yes_count = 0\n",
    "for answer in cleaned:\n",
    "    if answer == \"yes\":\n",
    "        yes_count += 1\n",
    "\n",
    "print(\"Cleaned:\", cleaned)\n",
    "print(\"Yes count:\", yes_count)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b90efcd4",
   "metadata": {},
   "source": [
    "## Splitting and Joining\n",
    "\n",
    "```{index} split()\n",
    "```\n",
    "\n",
    "```{index} join()\n",
    "```\n",
    "\n",
    "```{index} delimiter\n",
    "```\n",
    "\n",
    "{doc}`Lists <0203-lists>` introduced `split()`, which breaks a string into a list, and `join()`, which combines a list of strings into one string. The character that marks the boundary between pieces is called the **delimiter**. With {doc}`tuple unpacking <0203-lists>`, one line can split a product code and name each part."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "628af7ce",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.076953Z",
     "iopub.status.busy": "2026-10-23T03:50:15.076903Z",
     "iopub.status.idle": "2026-10-23T03:50:15.078376Z",
     "shell.execute_reply": "2026-10-23T03:50:15.078139Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['SKU', '1024', 'BLUE']\n",
      "Number: 1024\n",
      "Color: BLUE\n"
     ]
    }
   ],
   "source": [
    "code = \"SKU-1024-BLUE\"\n",
    "\n",
    "parts = code.split(\"-\")\n",
    "print(parts)                        ### ['SKU', '1024', 'BLUE']\n",
    "\n",
    "prefix, number, color = code.split(\"-\")\n",
    "print(\"Number:\", number)\n",
    "print(\"Color:\", color)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2826da9b",
   "metadata": {},
   "source": [
    "The same pattern parses an email address or one line of comma-separated data. Every piece that `split()` returns is a string, so convert numeric pieces with `int()` or `float()` before doing arithmetic."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "118ba95f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.079252Z",
     "iopub.status.busy": "2026-10-23T03:50:15.079188Z",
     "iopub.status.idle": "2026-10-23T03:50:15.080709Z",
     "shell.execute_reply": "2026-10-23T03:50:15.080479Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "pat.lee | example.com\n",
      "Alice works in Sales; a 3% raise is 1560.0\n"
     ]
    }
   ],
   "source": [
    "user, domain = \"pat.lee@example.com\".split(\"@\")\n",
    "print(user, \"|\", domain)\n",
    "\n",
    "line = \"Alice,Sales,52000\"\n",
    "name, department, salary = line.split(\",\")\n",
    "salary = int(salary)                ### '52000' becomes 52000\n",
    "print(f\"{name} works in {department}; a 3% raise is {salary * 0.03}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8f327638",
   "metadata": {},
   "source": [
    "`join()` is called on the delimiter, and its argument is the list of strings to combine. It is the reverse of `split()`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "657148da",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.081575Z",
     "iopub.status.busy": "2026-10-23T03:50:15.081526Z",
     "iopub.status.idle": "2026-10-23T03:50:15.083067Z",
     "shell.execute_reply": "2026-10-23T03:50:15.082717Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Ana, Ben, Chloe\n",
      "Ana | Ben | Chloe\n",
      "pat.lee@example.com\n"
     ]
    }
   ],
   "source": [
    "first_names = [\"Ana\", \"Ben\", \"Chloe\"]\n",
    "\n",
    "print(\", \".join(first_names))       ### Ana, Ben, Chloe\n",
    "print(\" | \".join(first_names))      ### Ana | Ben | Chloe\n",
    "\n",
    "email = \".\".join([\"pat\", \"lee\"]) + \"@example.com\"\n",
    "print(email)                        ### pat.lee@example.com"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1be2d76",
   "metadata": {},
   "source": [
    "`join()` accepts only strings. To join numbers, convert each one with `str()` first."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "ch02-strings-split-join-exercise-q",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.084206Z",
     "iopub.status.busy": "2026-10-23T03:50:15.084152Z",
     "iopub.status.idle": "2026-10-23T03:50:15.085593Z",
     "shell.execute_reply": "2026-10-23T03:50:15.085319Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Parse Product Codes\n",
    "# 1. Create an empty list named numbers.\n",
    "# 2. Loop over codes; split each code on \"-\" and unpack it into prefix, number, and color.\n",
    "# 3. Print the number and color, and append the number to numbers.\n",
    "# 4. Join numbers with \" | \" and print the result.\n",
    "# Print exactly:\n",
    "# 1024: BLUE\n",
    "# 2048: RED\n",
    "# 4096: BLUE\n",
    "# Product numbers: 1024 | 2048 | 4096\n",
    "codes = [\"SKU-1024-BLUE\", \"SKU-2048-RED\", \"SKU-4096-BLUE\"]\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "ch02-strings-split-join-exercise-a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.086438Z",
     "iopub.status.busy": "2026-10-23T03:50:15.086372Z",
     "iopub.status.idle": "2026-10-23T03:50:15.088104Z",
     "shell.execute_reply": "2026-10-23T03:50:15.087802Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1024: BLUE\n",
      "2048: RED\n",
      "4096: BLUE\n",
      "Product numbers: 1024 | 2048 | 4096\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "codes = [\"SKU-1024-BLUE\", \"SKU-2048-RED\", \"SKU-4096-BLUE\"]\n",
    "\n",
    "numbers = []\n",
    "for code in codes:\n",
    "    prefix, number, color = code.split(\"-\")\n",
    "    print(f\"{number}: {color}\")\n",
    "    numbers.append(number)\n",
    "\n",
    "print(\"Product numbers:\", \" | \".join(numbers))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14946b8c",
   "metadata": {},
   "source": [
    "## Formatting Numbers with f-strings\n",
    "\n",
    "```{index} f-string; format specifier\n",
    "```\n",
    "\n",
    "```{index} format specifier\n",
    "```\n",
    "\n",
    "{doc}`Python Syntax <0201-py-syntax>` used f-strings such as `f\"Total: {total}\"` to place values inside text. A business report also needs numbers to *look* right: two decimal places for money, thousands separators, and percent signs. Add a **format specifier** after a colon inside the braces: `{value:spec}`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "4ac666bf",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.088894Z",
     "iopub.status.busy": "2026-10-23T03:50:15.088838Z",
     "iopub.status.idle": "2026-10-23T03:50:15.090583Z",
     "shell.execute_reply": "2026-10-23T03:50:15.090254Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1234567.891\n",
      "1234567.89\n",
      "1,234,567.89\n",
      "$1,234,567.89\n",
      "1,234,568\n"
     ]
    }
   ],
   "source": [
    "revenue = 1234567.891\n",
    "\n",
    "print(f\"{revenue}\")                 ### raw value\n",
    "print(f\"{revenue:.2f}\")             ### 2 decimal places\n",
    "print(f\"{revenue:,.2f}\")            ### thousands separator + 2 decimals\n",
    "print(f\"${revenue:,.2f}\")           ### currency: $ is plain text before the braces\n",
    "print(f\"{revenue:,.0f}\")            ### rounded to whole dollars"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "506e7fd5",
   "metadata": {},
   "source": [
    "The `%` specifier multiplies a number by 100 and adds a percent sign, so store rates as decimals (0.084, not 8.4)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "7f69981f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.091335Z",
     "iopub.status.busy": "2026-10-23T03:50:15.091287Z",
     "iopub.status.idle": "2026-10-23T03:50:15.092764Z",
     "shell.execute_reply": "2026-10-23T03:50:15.092424Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "8.4%\n",
      "8%\n"
     ]
    }
   ],
   "source": [
    "growth = 0.084\n",
    "\n",
    "print(f\"{growth:.1%}\")              ### 8.4%\n",
    "print(f\"{growth:.0%}\")              ### 8%"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "422f4841",
   "metadata": {},
   "source": [
    "The most useful specifiers are summarized below.\n",
    "\n",
    "| Specifier | Meaning | Example | Result |\n",
    "|---|---|---|---|\n",
    "| `.2f` | fixed point, 2 decimals | `f\"{3.14159:.2f}\"` | `3.14` |\n",
    "| `,.2f` | thousands separator, 2 decimals | `f\"{52000:,.2f}\"` | `52,000.00` |\n",
    "| `.1%` | percent, 1 decimal | `f\"{0.25:.1%}\"` | `25.0%` |\n",
    "| `<10` | left-align in 10 characters | `f\"{'East':<10}\"` | `East      ` |\n",
    "| `>10` | right-align in 10 characters | `f\"{42:>10}\"` | `        42` |\n",
    "| `>12,.2f` | right-align and format | `f\"{1500:>12,.2f}\"` | `    1,500.00` |"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a845751",
   "metadata": {},
   "source": [
    "```{index} alignment (f-string)\n",
    "```\n",
    "\n",
    "A **width** and an **alignment** (`<` left, `>` right) line values up in columns, which turns a few `print()` calls into a small report. Text columns are usually left-aligned and number columns right-aligned. This example uses `zip()` from {doc}`Lists <0203-lists>` to walk through regions and sales together."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "43090b54",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.093567Z",
     "iopub.status.busy": "2026-10-23T03:50:15.093509Z",
     "iopub.status.idle": "2026-10-23T03:50:15.095093Z",
     "shell.execute_reply": "2026-10-23T03:50:15.094881Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Region             Sales\n",
      "North         125,000.00\n",
      "South          98,500.50\n",
      "East          143,250.75\n",
      "West           87,000.00\n",
      "Total         453,751.25\n"
     ]
    }
   ],
   "source": [
    "regions = [\"North\", \"South\", \"East\", \"West\"]\n",
    "sales = [125000, 98500.5, 143250.75, 87000]\n",
    "\n",
    "print(f\"{'Region':<10}{'Sales':>14}\")\n",
    "for region, amount in zip(regions, sales):\n",
    "    print(f\"{region:<10}{amount:>14,.2f}\")\n",
    "print(f\"{'Total':<10}{sum(sales):>14,.2f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "ch02-strings-format-exercise-q",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.095844Z",
     "iopub.status.busy": "2026-10-23T03:50:15.095779Z",
     "iopub.status.idle": "2026-10-23T03:50:15.097177Z",
     "shell.execute_reply": "2026-10-23T03:50:15.096901Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Format a Sales Summary\n",
    "# 1. Use an f-string with a currency format for sales.\n",
    "# 2. Use an f-string with a percent format (1 decimal) for margin.\n",
    "# Print exactly:\n",
    "# Region: Midwest\n",
    "# Sales: $48,250.50\n",
    "# Margin: 18.3%\n",
    "region = \"Midwest\"\n",
    "sales = 48250.5\n",
    "margin = 0.183\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "ch02-strings-format-exercise-a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.097936Z",
     "iopub.status.busy": "2026-10-23T03:50:15.097885Z",
     "iopub.status.idle": "2026-10-23T03:50:15.099347Z",
     "shell.execute_reply": "2026-10-23T03:50:15.099121Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Region: Midwest\n",
      "Sales: $48,250.50\n",
      "Margin: 18.3%\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "region = \"Midwest\"\n",
    "sales = 48250.5\n",
    "margin = 0.183\n",
    "\n",
    "print(f\"Region: {region}\")\n",
    "print(f\"Sales: ${sales:,.2f}\")\n",
    "print(f\"Margin: {margin:.1%}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4e3c9b5b",
   "metadata": {},
   "source": [
    "## Strings Are Immutable\n",
    "\n",
    "```{index} immutability; string\n",
    "```\n",
    "\n",
    "Strings are **immutable**, like tuples: once a string exists, its characters cannot be changed. Assigning to an index raises a `TypeError`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "c50aed52",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.100236Z",
     "iopub.status.busy": "2026-10-23T03:50:15.100179Z",
     "iopub.status.idle": "2026-10-23T03:50:15.101528Z",
     "shell.execute_reply": "2026-10-23T03:50:15.101213Z"
    },
    "tags": []
   },
   "outputs": [],
   "source": [
    "product = \"apple\"\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "9081e47d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.102304Z",
     "iopub.status.busy": "2026-10-23T03:50:15.102251Z",
     "iopub.status.idle": "2026-10-23T03:50:15.106411Z",
     "shell.execute_reply": "2026-10-23T03:50:15.106159Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "ename": "TypeError",
     "evalue": "'str' object does not support item assignment",
     "output_type": "error",
     "traceback": [
      "\u001b[31mTypeError\u001b[39m\u001b[31m:\u001b[39m 'str' object does not support item assignment\n"
     ]
    }
   ],
   "source": [
    "%%expect TypeError\n",
    "\n",
    "product[0] = \"A\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5be8ac52",
   "metadata": {},
   "source": [
    "To get a different string, build a new one and assign it to a name."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "f0b9f7f7",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.107286Z",
     "iopub.status.busy": "2026-10-23T03:50:15.107226Z",
     "iopub.status.idle": "2026-10-23T03:50:15.108535Z",
     "shell.execute_reply": "2026-10-23T03:50:15.108269Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Apple\n"
     ]
    }
   ],
   "source": [
    "product = \"A\" + product[1:]          ### a new string from pieces\n",
    "print(product)                      ### 'Apple'"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ec8ecbe2",
   "metadata": {},
   "source": [
    "This is why every string method returns a new string. A common mistake is to call a method and forget to save the result: the original string stays the same."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "80cab9a6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.109311Z",
     "iopub.status.busy": "2026-10-23T03:50:15.109259Z",
     "iopub.status.idle": "2026-10-23T03:50:15.110731Z",
     "shell.execute_reply": "2026-10-23T03:50:15.110496Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[  Ana Lopez ]\n",
      "[Ana Lopez]\n"
     ]
    }
   ],
   "source": [
    "name = \"  Ana Lopez \"\n",
    "\n",
    "name.strip()                        ### result is computed, then thrown away\n",
    "print(f\"[{name}]\")                  ### still has the spaces\n",
    "\n",
    "name = name.strip()                 ### save the new string under the same name\n",
    "print(f\"[{name}]\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "ch02-strings-immutable-exercise-q",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.111615Z",
     "iopub.status.busy": "2026-10-23T03:50:15.111550Z",
     "iopub.status.idle": "2026-10-23T03:50:15.112960Z",
     "shell.execute_reply": "2026-10-23T03:50:15.112689Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Region code: mw\n"
     ]
    }
   ],
   "source": [
    "### Exercise: Save the Cleaned Value\n",
    "# The code below calls upper() but does not save the result.\n",
    "# 1. Fix it so that region_code refers to the uppercase string.\n",
    "# Print exactly:\n",
    "# Region code: MW\n",
    "region_code = \"mw\"\n",
    "### Your code starts here.\n",
    "region_code.upper()\n",
    "\n",
    "### Your code ends here.\n",
    "print(\"Region code:\", region_code)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "ch02-strings-immutable-exercise-a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.113719Z",
     "iopub.status.busy": "2026-10-23T03:50:15.113668Z",
     "iopub.status.idle": "2026-10-23T03:50:15.115072Z",
     "shell.execute_reply": "2026-10-23T03:50:15.114772Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Region code: MW\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "region_code = \"mw\"\n",
    "region_code = region_code.upper()\n",
    "print(\"Region code:\", region_code)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f6b9f83c",
   "metadata": {},
   "source": [
    "## Looping Over Characters and Words\n",
    "\n",
    "```{index} string; looping\n",
    "```\n",
    "\n",
    "A `for` loop over a string visits one character at a time. String methods such as `isdigit()` test a single character, so a loop can count or collect characters of one kind. {doc}`Functions <0208-functions>` returns to character loops."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "bf3f31be",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.115881Z",
     "iopub.status.busy": "2026-10-23T03:50:15.115824Z",
     "iopub.status.idle": "2026-10-23T03:50:15.117369Z",
     "shell.execute_reply": "2026-10-23T03:50:15.117092Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Digits: 10\n"
     ]
    }
   ],
   "source": [
    "phone = \"(555) 010-4477\"\n",
    "\n",
    "digits = 0\n",
    "for character in phone:\n",
    "    if character.isdigit():\n",
    "        digits += 1\n",
    "\n",
    "print(\"Digits:\", digits)            ### 10"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "52d0b421",
   "metadata": {},
   "source": [
    "To work with **words** instead of characters, `split()` the text first and loop over the list. Cleaning the text before splitting, with `lower()` and `replace()`, makes `\"Great\"` and `\"great,\"` count as the same word."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "fb1f0127",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.118218Z",
     "iopub.status.busy": "2026-10-23T03:50:15.118153Z",
     "iopub.status.idle": "2026-10-23T03:50:15.119744Z",
     "shell.execute_reply": "2026-10-23T03:50:15.119440Z"
    },
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "['great', 'product', 'fast', 'shipping', 'great', 'price']\n",
      "Word count: 6\n",
      "Times 'great' appears: 2\n"
     ]
    }
   ],
   "source": [
    "review = \"Great product, fast shipping, great price\"\n",
    "\n",
    "words = review.lower().replace(\",\", \"\").split()\n",
    "print(words)\n",
    "print(\"Word count:\", len(words))\n",
    "\n",
    "great_count = 0\n",
    "for word in words:\n",
    "    if word == \"great\":\n",
    "        great_count += 1\n",
    "print(\"Times 'great' appears:\", great_count)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "92849264",
   "metadata": {},
   "source": [
    "Counting one word at a time works for a single word. To count every word in a collection of reviews, the next section, {doc}`Dictionaries <0206-dictionaries>`, stores one counter per word."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "ch02-strings-looping-exercise-q",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.120553Z",
     "iopub.status.busy": "2026-10-23T03:50:15.120502Z",
     "iopub.status.idle": "2026-10-23T03:50:15.121870Z",
     "shell.execute_reply": "2026-10-23T03:50:15.121619Z"
    },
    "tags": [
     "thebe-interactive"
    ]
   },
   "outputs": [],
   "source": [
    "### Exercise: Count Words in Reviews\n",
    "# 1. Loop over reviews with enumerate(reviews, start=1) and print each word count.\n",
    "# 2. Count the reviews that mention \"shipping\" (ignore upper and lower case).\n",
    "# Print exactly:\n",
    "# Review 1: 5 words\n",
    "# Review 2: 2 words\n",
    "# Review 3: 5 words\n",
    "# Reviews mentioning shipping: 2\n",
    "reviews = [\n",
    "    \"Fast shipping and great price\",\n",
    "    \"Great quality\",\n",
    "    \"Slow SHIPPING but great support\",\n",
    "]\n",
    "### Your code starts here.\n",
    "\n",
    "\n",
    "\n",
    "### Your code ends here."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "ch02-strings-looping-exercise-a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-23T03:50:15.122606Z",
     "iopub.status.busy": "2026-10-23T03:50:15.122554Z",
     "iopub.status.idle": "2026-10-23T03:50:15.124226Z",
     "shell.execute_reply": "2026-10-23T03:50:15.123920Z"
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Review 1: 5 words\n",
      "Review 2: 2 words\n",
      "Review 3: 5 words\n",
      "Reviews mentioning shipping: 2\n"
     ]
    }
   ],
   "source": [
    "### Solution\n",
    "reviews = [\n",
    "    \"Fast shipping and great price\",\n",
    "    \"Great quality\",\n",
    "    \"Slow SHIPPING but great support\",\n",
    "]\n",
    "\n",
    "shipping_count = 0\n",
    "for number, review in enumerate(reviews, start=1):\n",
    "    print(f\"Review {number}: {len(review.split())} words\")\n",
    "    if \"shipping\" in review.lower():\n",
    "        shipping_count += 1\n",
    "\n",
    "print(\"Reviews mentioning shipping:\", shipping_count)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fd070e09",
   "metadata": {},
   "source": [
    "## Summary\n",
    "\n",
    "- Strings are sequences, so indexing and slicing work as they do for lists.\n",
    "- String methods such as `strip()`, `lower()`, `title()`, and `replace()` return new, cleaned strings; `in`, `startswith()`, `endswith()`, and `find()` search text.\n",
    "- `split()` breaks a string into a list at a delimiter, and `join()` combines a list of strings into one string.\n",
    "- Format specifiers such as `:,.2f`, `:.1%`, `<10`, and `>12` make numbers readable in reports.\n",
    "- Strings are immutable: save the result of a method if you want to keep it.\n",
    "- The pandas `.str` methods in Chapter 4 apply these same operations to whole columns of text."
   ]
  }
 ],
 "metadata": {
  "jupytext": {
   "default_lexer": "ipython3"
  },
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.16"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
