Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Random Concept]: Captains Log Exercise #3571

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,18 @@
"loops"
],
"status": "wip"
},
{
"slug": "captains-log",
"name": "Captains Log",
"uuid": "c781da14-40c0-4aeb-8452-769043592775",
"concepts": ["random"],
"prerequisites": [
"numbers",
"lists",
"string-formatting"
],
"status": "wip"
}
],
"practice": [
Expand Down
3 changes: 3 additions & 0 deletions exercises/concept/captains-log/.docs/hints.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Hints

TODO
42 changes: 42 additions & 0 deletions exercises/concept/captains-log/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Instructions

Mary is a big fan of the TV series _Star Trek: The Next Generation_. She often plays pen-and-paper role playing games, where she and her friends pretend to be the crew of the _Starship Enterprise_. Mary's character is Captain Picard, which means she has to keep the captain's log. She loves the creative part of the game, but doesn't like to generate random data on the spot.

Help Mary by creating random generators for data commonly appearing in the captain's log.

## 1. Generate a random planet

The _Starship Enterprise_ encounters many planets in its travels. Planets in the Star Trek universe are split into categories based on their properties. For example, Earth is a class M planet. All possible planetary classes are: D, H, J, K, L, M, N, R, T, and Y.

Implement the `random_planet` function. It should return one of the planetary classes at random.

```Python
random_planet_class()
# => "K"
```

## 2. Generate a random starship registry number

Enterprise (registry number NCC-1701) is not the only starship flying around! When it rendezvous with another starship, Mary needs to log the registry number of that starship.

Registry numbers start with the prefix "NCC-" and then use a number from 1000 to 9999 (inclusive).

Implement the `random_ship_registry_number` function that returns a random starship registry number.

```Python
random_ship_registry_number()
# => "NCC-1947"
```

## 3. Generate a random stardate

What's the use of a log if it doesn't include dates?

A stardate is a floating point number. The adventures of the _Starship Enterprise_ from the first season of _The Next Generation_ take place between the stardates 41000.0 and 42000.0. The "4" stands for the 24th century, the "1" for the first season.

Implement the function `random_stardate` that returns a floating point number between 41000.0 and 42000.0 (both inclusive).

```Python
random_stardate()
# => 41458.15721310934
```
127 changes: 127 additions & 0 deletions exercises/concept/captains-log/.docs/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Introduction

Many programs need (apparently) random values to simulate real-world events.

Common, familiar examples include:
- A coin toss: a random value from `('H', 'T')`.
- The roll of a die: a random integer from 1 to 6.
- Shuffling a deck of cards: a random ordering of a card list.
- The creation of trees and bushes in a 3-D graphics simulation.

Generating _truly_ random values with a computer is a [surprisingly difficult technical challenge][truly-random], so you may see these results referred to as "pseudorandom".

In practice, a well-designed library like the [`random`][random] module in the Python standard library is fast, flexible, and gives results that are amply good enough for most applications in modelling, simulation and games.

For this brief introduction, we show the four most commonly used functions from the module.
We encourage you to explore the full [`random`][random] documentation, as there are many tools and options.


~~~~exercism/caution

The `random` module should __NOT__ be used for security and cryptographic applications!!

Instead, Python provides the [`secrets`][secrets] module.
This is specially optimized for cryptographic security.
Some of the prior issues and reasons for creating the secrets module can be found in [PEP 506][PEP 506].

[secrets]: https://docs.python.org/3.11/library/secrets.html#module-secrets
[PEP 506]: https://peps.python.org/pep-0506/
~~~~


Before you can utilize the tools in the `random` module, you must first import it:

```python
>>> import random

# Choose random integer from a range
>>> random.randrange(1000)
360

>>> random.randrange(-1, 500)
228

>>> random.randrange(-10, 11, 2)
-8

# Choose random integer between two values (inclusive)
>>> random.randint(5, 25)
22

```

To avoid typing the name of the module, you can import specific functions by name:

```python
>>> from random import choice, choices

# Using choice() to pick Heads or Tails 10 times
>>> tosses = []
>>> for side in range(10):
>>> tosses.append(choice(['H', 'T']))

>>> print(tosses)
['H', 'H', 'H', 'H', 'H', 'H', 'H', 'T', 'T', 'H']


# Using choices() to pick Heads or Tails 8 times
>>> picks = []
>>> picks.extend(choices(['H', 'T'], k=8))
>>> print(picks)
['T', 'H', 'H', 'T', 'H', 'H', 'T', 'T']
```

## `randrange()` and `randint()`

Shown in the first example above, the `randrange()` function has three forms:

1. `randrange(stop)` gives an integer `n` such that `0 <= n < stop`
2. `randrange(start, stop)` gives an integer `n` such that `start <= n < stop`
3. `randrange(start, stop, step)` gives an integer `n` such that `start <= n < stop`
and `n` is in the sequence `start, start + step, start + 2*step...`

For the most common case where `step == 1`, `randint(a, b)` may be more convenient and readable.
Possible results from `randint()` _include_ the upper bound, so `randint(a, b)` is the same as using `randrange(a, b+1)`.



## `choice()` and `choices()`

These two functions assume that you are starting from some [sequence][sequence-types], or other container.
This will typically be a `list`, or with some limitations a `tuple` or a `set` (_a `tuple` is immutable, and `set` is unordered_).

The `choice()` function will return one entry chosen at random from a given sequence, and `choices()` will return `k` number of entries chosen at random from a given sequence.

In the examples shown above, we assumed a fair coin with equal probability of heads or tails, but weights can also be specified.

For example, if a bag contains 10 red balls and 15 green balls, and we would like to pull one out at random:


```python
>>> random.choices(['red', 'green'], [10, 15])
['red']
```

## `random()` and `uniform()`

For integers, `randrange()` and `randint()` are used when all probabilities are equal. This is called a `uniform` distributuion.

There are floating-point equivalents to `randrange()` and `randint()`.

__`random()`__ gives a `float` value `x` such that `0.0 <= x < 1.0`.

__`uniform(a, b)`__ gives `x` such that `a <= x <= b`.

```python
>>> [round(random.random(), 3) for n in range(5)]
[0.876, 0.084, 0.483, 0.22, 0.863]

>>> [round(random.uniform(2, 5), 3) for n in range(5)]
[2.798, 2.539, 3.779, 3.363, 4.33]
```



[random]: https://docs.python.org/3/library/random.html
[sequence-types]: https://docs.python.org/3/library/stdtypes.html#sequence-types-list-tuple-range
[truly-random]: https://www.malwarebytes.com/blog/news/2013/09/in-computers-are-random-numbers-really-random
20 changes: 20 additions & 0 deletions exercises/concept/captains-log/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"authors": [
"colinleach",
"BethanyG"
],
"contributors": [],
"files": {
"solution": [
"captains_log.py"
],
"test": [
"captains_log_test.py"
],
"exemplar": [
".meta/exemplar.py"
]
},
"forked_from": ["elixir/captains-log"],
"blurb": "Help Mary with her role playing game by generating suitable random data."
}
48 changes: 48 additions & 0 deletions exercises/concept/captains-log/.meta/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Design

## Goal

The goal of this exercise is to teach the basics of `random` functions in Python.

## Learning objectives

- Understand the use of `choice()` to select randomly from a sequence
- Understand the use of `randint()` to generate integers in a desired range
- Understand the use of `uniform()` to generate uniformly-distributed `float`s in a desired range

## Out of scope

- Non-uniform statistical distributions such as `gaussian()`
- Security-focussed functionality in `secrets`

## Concepts covered

- `datetime.date`
- `strptime()`
- `strftime()`

## Prerequisites

- `string-formatting`
- `lists`

## Resources to refer to

TODO

### Hints

TODO

## Concept Description

TODO

## Implementing

The general Python track concept exercise implantation guide can be found [here](https://github.com/exercism/v3/blob/master/languages/python/reference/implementing-a-concept-exercise.md).

Tests should be written using `unittest.TestCase` and the test file named `$SLUG_test.py`.

Code in the `.meta/example.py` file should **only use syntax & concepts introduced in this exercise or one of its prerequisites.**

15 changes: 15 additions & 0 deletions exercises/concept/captains-log/.meta/exemplar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import random


def random_planet_class():
planetary_classes = ("D", "H", "J", "K", "L", "M", "N", "R", "T", "Y")
return random.choice(planetary_classes)


def random_ship_registry_number():
registry_number = random.randint(1000, 9999)
return f"NCC-{registry_number}"


def random_stardate():
return random.uniform(41000.0, 42000.0)
11 changes: 11 additions & 0 deletions exercises/concept/captains-log/captains_log.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@

def random_planet_class():
pass


def random_ship_registry_number():
pass


def random_stardate():
pass
89 changes: 89 additions & 0 deletions exercises/concept/captains-log/captains_log_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import unittest
import pytest

from captains_log import (
random_planet_class,
random_ship_registry_number,
random_stardate)


class CaptainsLogTest(unittest.TestCase):

@pytest.mark.task(taskno=1)
def test_random_planet_class(self):
repeats = range(1000) # may need adjusting?
planetary_classes = {"D", "H", "J", "K", "L", "M", "N", "R", "T", "Y"}
actual_results = [random_planet_class() for _ in repeats]

# are all results valid planetary classes?
invalid = [planet for planet in actual_results if planet not in planetary_classes]
error_message1 = (f'Called random_planet_class() {repeats} times.'
f'The function returned these invalid results: '
f'{invalid}.')
self.assertEqual(len(invalid), 0, msg=error_message1)

# are all valid planetary classes generated, with enough repeats?
missing = [planet for planet in planetary_classes if planet not in set(actual_results)]
error_message2 = (f'Called random_planet_class() {repeats} times.'
f'The function never returned these valid results: '
f'{missing}.')
self.assertEqual(len(missing), 0, msg=error_message2)

@pytest.mark.task(taskno=2)
def test_ship_registry_number(self):
repeats = range(100) # may need adjusting?
actual_results = [random_ship_registry_number() for _ in repeats]

# Do all results have length 8?
wrong_length = [regno for regno in actual_results if len(regno) != 8]
error_message1 = (f'Called random_planet_class() {repeats} times.'
f'The function returned these invalid results (wrong length): '
f'{wrong_length}.')
self.assertEqual(len(wrong_length), 0, msg=error_message1)

# Do all results start with "NCC-"?
wrong_prefix = [regno for regno in actual_results if regno[:4] != 'NCC-']
error_message2 = (f'Called random_planet_class() {repeats} times.'
f'The function returned these invalid results (must start with "NCC-"): '
f'{wrong_prefix}.')
self.assertEqual(len(wrong_prefix), 0, msg=error_message2)

# Do all results end with a valid 4-digit integer?
not_int = [regno for regno in actual_results if not (regno[4:]).isdigit()]
error_message3 = (f'Called random_planet_class() {repeats} times.'
f'The function returned these invalid results (must end with a 4-digit integer): '
f'{not_int}.')
self.assertEqual(len(not_int), 0, msg=error_message3)

# Are all numbers from 1000 to 9999?
wrong_int = [regno for regno in actual_results if not (1000 <= int(regno[4:]) <= 9999)]
error_message4 = (f'Called random_planet_class() {repeats} times.'
f'The function returned these invalid results (integer must be 1000 to 9999): '
f'{wrong_int}.')
self.assertEqual(len(wrong_int), 0, msg=error_message4)

@pytest.mark.task(taskno=3)
def test_stardate(self):
repeats = range(100) # may need adjusting?
actual_results = [random_stardate() for _ in repeats]

def is_number(s):
try:
float(s)
return True
except ValueError:
return False

# Are all results valid float values?
not_float = [stardate for stardate in actual_results if not isinstance(stardate, float)]
error_message1 = (f'Called random_planet_class() {repeats} times.'
f'The function returned these invalid results (must be a floating-point number): '
f'{not_float}.')
self.assertEqual(len(not_float), 0, msg=error_message1)

# Are all results numbers from from 41000 to 42000?
wrong_number = [stardate for stardate in actual_results if not 41000.0 <= stardate <= 42000.0]
error_message2 = (f'Called random_planet_class() {repeats} times.'
f'The function returned these invalid results (must be from 41000.0 to 42000.0): '
f'{wrong_number}.')
self.assertEqual(len(wrong_number), 0, msg=error_message2)
Loading