Today: strings, loops, string functions, Doctests

## Week-3 Thoughts

• Pace has been pretty high
We'll slow down a bit now concepts/day
• HW2 lesson: y, pixel_left, .. diagram
Wrong 49 times, right 1 time .. fine
Understand that last one!
• Something fun on Friday

## String Reminder

-See guide: Python-String
• Sequence of characters in quotes
• `'Python'`
• len() function
• Square brackets
• `+` to put strings together
• Indexed: 0, 1, 2, ... len-1
• Strings are immutable

## str int Conversion

• What is '123' vs. 123?
• '123' is string
• 123 is in
• str(8) returns '8'
• int('8') returns 8

## Loop Over Every Char in a String? • Python is very consistent
• Zero-based indexing
• Linear collection of elements
• Index each element at: 0, 1, 2, ... len-1
• String works this way
• Images worked this way too
• Collections to learn later .. work this way too

## Loop Over Every Char

• Loop over index numbers
• `for i in range(len(s)):`
• Use variable name `i` for the index numbers
• This is so common, it's idiomatic
• Use `s[i]` to access char in loop
```# have string s
for i in range(len(s)):
# use s[i] in here
```

## Example 1. double_char

(these work for everyone now, sorry for the snafu)

• Idiomatic loop look at every char
• Initialize `result` variable before loop
• Update result in loop with = and +
• Return result at end
• Could use `+=` shortcut
• Q: does it work on empty string input?

Solution code

```def double_char(s):
result = ''
for i in range(len(s)):
result = result + s[i] + s[i]
return result
```

## String Testing == 'a' 'A'

We've used `==` already. `'a'` and `'A'` are different characters.

```>>> s = 'red'
>>> s == 'red'   # two equal signs
True
>>> s == 'Red'   # must match exactly
False
```

## String Testing - in

• The word in - test if a substring appears in a string
• Mnemonic: same word inside for-loop, like Python wants to introduce very few new words
• Chars must match exactly - upper/lower are considered different
```>>> 'c' in 'abcd'
True
>>> 'bc' in 'abcd'
True
>>> 'bx' in 'abcd'
False
>>> 'A' in 'abcd'
False
```

## String Character Class Tests

• String is made of characters
• Categorize characters into major classes:
• 1. Alphabetic - `'a' 'Z'` .. used to make words
• 2. Digits - `'0' '2'` .. used to make numbers
• 3. Space - space, tab, newline - aka "whitespace"
• There are noun.verb tests for the above 3, returning boolean True or False
• For empty string return False (a weird edge case)
• All the other chars form a miscellaneous class - `'\$' '%' ';'` ... any char not in the first 3 categories

`s.isdigit()` - True if all chars in s are digits '0' '1' .. '9'

`s.isalpha()` - True for alphabetic word char, i.e. a-z A-Z. Each unicode alphabet has its own definition of what's alphabetic, e.g. 'Ω' below is alphabetic.

`s.isalnum()` - alphanumeric, just combines isalpha() and isdigit()

`s.isspace()` - True for whitespace char, e.g. space, tab, newline

```>>> 'a'.isalpha()
True
>>> 'abc'.isalpha()  # works for multiple chars too
True
>>> 'Z'.isalpha()
True
>>> '@'.isalpha()
False
>>> '9'.isdigit()
True
>>> ' '.isspace()
True
```

## Example 2. digits_only

> 2.digits_only
• Return string of only digits
• So 'ab4xx2' returns '42'
• Use the standard range loop
• If logic inside the loop

Solution code

```def digits_only(s):
result = ''
for i in range(len(s)):
if s[i].isdigit():
result += s[i]
return result
```

## if Variation: if / else

```if test:
Lines-A
else:
Lines-B
```
• See guide: Python-if
• Regular if - do the body lines, or do nothing
• if/else variant:
Choose between 2 actions
Always do one of them
• Run lines-A if test is True
• Run lines-B if test is False
• Style: use if/else to choose 1 of 2 actions
• Avoid using "else" if it is not needed
The regular "if" we've seen is most common

## Example 3. str_dx

> 3.str_dx
• Return string where every digit changes to 'd'
And every other char changed to 'x'
• So 'ab42z' returns 'xxddx'
• Use if/else

Solution code

```def str_dx(s):
result = ''
for i in range(len(s)):
if s[i].isdigit():
result += 'd'
else:
result += 'x'
return result
```

## Big Picture - Program, Functions, Testing

• Big Picture
• Program Made of many functions
• Decomposition
• Best Practice:
Test a function in isolation
Then later functions can call it
• We did this with Bit sometimes
• Fits with Divide and Conquer strategy
• Fits with Black Box model of a function
Inputs and Outputs - test with these
• Today: tech for doing this in Python! ## str1 project

• Expand to get "str1" folder
• Open the folder in PyCharm

## Python Function - Pydoc

• See str_dx() below
• See triple-quote description at top
• This is known as "Pydoc" of the function
What inputs fn takes
What it promises to return
Black box model
"Contract" idea
```def str_dx(s):
"""
Given string s.
Returns a string where every digit is changed to 'd',
and all other chars are changed to 'x'.
(this code + tests are complete)
>>> str_dx('Hi4!x3')
'xxdxxd'
>>> str_dx('123')
'ddd'
>>> str_dx('')
''
"""
```

## Python Function - Doctest

See lines like:

```    >>> str_dx('Hi4!x3')
'xxdxxd'
```
• That syntax spells out one test of the str_dx() function
• Looks like a fn call
• Input between the parens
• Output on the next line
• In PyCharm:
Right click on the test
Select "Run Doctest ..."
• Output: `Process finished with exit code 0`
That means it worked perfectly
This message could be more fun about it!
• Otherwise get error output
• Experiment: try putting in a bug, run Doctest, fix the bug
• Protip: ctrl-r runs most recent test
Avoid having to re-click every time

## String Test: Upper / Lower Case

• Lowercase 'a'
• Uppercase 'A'
• Test functions return boolean True/False
• `s.isupper(), s.islower()`
• True for uppercase / lowercase alphabetic.
• False for characters like '9' and '\$' which do not have upper/lower versions.
```>>> 'a'.islower()
True
>>> 'A'.islower()
False
>>> 'A'.isupper()
True
>>> '9'.isupper()
False
```

## String Change Upper/Lower Case

`s.lower()` - returns a new version of s where each char is converted to its lowercase form, so 'A' becomes 'a'. Chars like '\$' are unchanged. The original s is unchanged - a good example of strings being immutable. Each unicode alphabet includes its own rules about upper/lower case.

`s.upper()` - returns an uppercase version of s

```>>> s = 'Python123'
>>> s.lower()
'python123'
>>> s.upper()
'PYTHON123'
>>> s
'Python123'
```

## Programming With Doctest

• Have a few Doctests for a function
• Work on the code
• Type ctrl-r to test your fn as you go
• This is a fantastic workflow
• 1. Function has tests right in the code
• 2. You work on fn, run tests until it works

## Doctest Example 2, crazy_str()

Write code for this problem. Use the Doctests as you go.

```def crazy_str(s):
"""
Given string s. Return a string where every lowercase char
in s is converted to uppercase, and every other char
is converted to lowercase.
>>> crazy_str('Hello')
'hELLO'
>>> crazy_str('abc2@Z')
'ABC2@z'
>>> crazy_str('')
''
"""
pass
```