Repository: JanDeDobbeleer/oh-my-posh
Pull request #3974 (2023-06-17 19:39:44)

Feature: `lucky7` execution time formatter
### Prerequisites

- [x] I have read and understood the [contributing guide][CONTRIBUTING.md].
- [x] The commit message follows the [conventional commits][cc] guidelines.
- [x] Tests for the changes have been added (for bug fixes / features).
- [x] Docs have been added/updated (for bug fixes / features).

### Description

<!--
copilot:summary
-->
### <samp>🤖 Generated by Copilot at 8780529</samp>

This pull request adds a new `Lucky7` option for the execution time segment, which displays the duration with 7 characters in a consistent way. It also adds unit tests and fuzz tests for the new formatting function, and updates the website documentation to reflect the new option.

### How

<!--
copilot:walkthrough
-->
### <samp>🤖 Generated by Copilot at 8780529</samp>

*  Add `Lucky7` option for formatting execution time segment with 7 characters
  * Implement `formatDurationLucky7` function in `src/segments/executiontime.go` ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/3974/files?diff=unified&w=0#diff-453261e599114c96f3e5e7fce11fc2511c420954a0e7a5a707140d5006aa56a8R230-R277))
  * Document `Lucky7` option and examples in `website/docs/segments/executiontime.mdx` ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/3974/files?diff=unified&w=0#diff-4c7d8a37997c4026401fe4d1980814196c43d12f64ee7e403e6ccb5b32f6d7d8R51))
* Add test function for `formatDurationLucky7` function in `src/segments/executiontime_test.go` ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/3974/files?diff=unified&w=0#diff-8522a3ed8ddad68615da856743d0f0f5adada7c658ff9d8c83b5537bed9d179cR4), [link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/3974/files?diff=unified&w=0#diff-8522a3ed8ddad68615da856743d0f0f5adada7c658ff9d8c83b5537bed9d179cR293-R334))
  * Test various input durations and expected outputs ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/3974/files?diff=unified&w=0#diff-8522a3ed8ddad68615da856743d0f0f5adada7c658ff9d8c83b5537bed9d179cR293-R334))
  * Fuzz test random durations and check output length ([link](https://github.com/JanDeDobbeleer/oh-my-posh/pull/3974/files?diff=unified&w=0#diff-8522a3ed8ddad68615da856743d0f0f5adada7c658ff9d8c83b5537bed9d179cR293-R334))

<!---

Tips:

If you're not comfortable with working with Git,
we're working a guide (https://ohmyposh.dev/docs/contributing_git) to help you out.
Oh My Posh advises GitKraken (https://www.gitkraken.com/invite/nQmDPR9D) as your preferred cross platform Git GUI power tool.

-->

[CONTRIBUTING.md]: https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/CONTRIBUTING.md
[cc]: https://www.conventionalcommits.org/en/v1.0.0/#summary



Patch:
diff --git a/src/segments/executiontime.go b/src/segments/executiontime.go
index 8f523a78..ac968ada 100644
--- a/src/segments/executiontime.go
+++ b/src/segments/executiontime.go
@@ -41,6 +41,8 @@ const (
 	Amarillo DurationStyle = "amarillo"
 	// Round will round the output of the format
 	Round DurationStyle = "round"
+	// Always 7 character width
+	Lucky7 = "lucky7"
 
 	second           = 1000
 	minute           = 60000
@@ -91,6 +93,8 @@ func (t *Executiontime) formatDuration(style DurationStyle) string {
 		return t.formatDurationAmarillo()
 	case Round:
 		return t.formatDurationRound()
+	case Lucky7:
+		return t.formatDurationLucky7()
 	default:
 		return fmt.Sprintf("Style: %s is not available", style)
 	}
@@ -223,3 +227,57 @@ func (t *Executiontime) formatDurationRound() string {
 	}
 	return fmt.Sprintf("%dms", t.Ms%second)
 }
+
+func (t *Executiontime) formatDurationLucky7() string {
+	// https://github.com/JanDeDobbeleer/oh-my-posh/issues/3970
+	// execution time will always be 7 characters long
+	// decimal point will be at the same location (3rd space or str[2])
+	// seconds and milliseconds will be aligned
+	// [m, s], [h, m], [d, h] will be aligned
+	if t.Ms < second {
+		//   999ms
+		// 1234567
+		return fmt.Sprintf("%5dms", t.Ms%second)
+	}
+
+	if t.Ms < minute {
+		// 12.34s
+		// 1234567
+
+		//  1.23s
+		// 1230 (= 1230ms)
+		// ^ use Sprintf pad left space
+		//  1230
+		// from here, just take 1, 23 of 230, and append s and ' '
+
+		result := fmt.Sprintf("%5d", t.Ms)
+
+		return result[:2] + "." + result[2:4] + "s "
+	}
+
+	if t.Ms < hour {
+		m := t.Ms / minute
+		s := t.Ms % minute / second
+
+		return fmt.Sprintf("%2dm %2ds", m, s)
+	}
+
+	if t.Ms < day {
+		h := t.Ms / hour
+		m := t.Ms % hour / minute
+
+		return fmt.Sprintf("%2dh %2dm", h, m)
+	}
+
+	if t.Ms < 100*day {
+		d := t.Ms / day
+		h := t.Ms % day / hour
+
+		return fmt.Sprintf("%2dd %2dh", d, h)
+	}
+
+	// I have no Idea how you got here
+	// return "   ∞   "
+	d := t.Ms / day
+	return fmt.Sprintf("%6dd", d)
+}
diff --git a/website/docs/segments/executiontime.mdx b/website/docs/segments/executiontime.mdx
index acd19923..31aeaa8c 100644
--- a/website/docs/segments/executiontime.mdx
+++ b/website/docs/segments/executiontime.mdx
@@ -48,6 +48,7 @@ Style specifies the format in which the time will be displayed. The table below
 | `houston`     | `00:00:00.001` | `00:00:02.1`   | `00:03:02.1`   | `04:03:02.1`     |
 | `amarillo`    | `0.001s`       | `2.1s`         | `182.1s`       | `14,582.1s`      |
 | `round`       | `1ms`          | `2s`           | `3m 2s`        | `4h 3m`          |
+| `lucky7`      | `    1ms`      | `    2s `      | ` 3m  2s`      | ` 4h  3m`        |
 
 ## Template ([info][templates])
 


Test patch:
diff --git a/src/segments/executiontime_test.go b/src/segments/executiontime_test.go
index a757a86c..f6710da0 100644
--- a/src/segments/executiontime_test.go
+++ b/src/segments/executiontime_test.go
@@ -1,6 +1,7 @@
 package segments
 
 import (
+	"math"
 	"testing"
 	"time"
 
@@ -289,3 +290,75 @@ func TestExecutionTimeFormatDurationRound(t *testing.T) {
 		assert.Equal(t, tc.Expected, output)
 	}
 }
+
+func TestExecutionTimeFormatDurationLucky7(t *testing.T) {
+	cases := []struct {
+		Input    string
+		Expected string
+	}{
+		{
+			Input:    "0.001s",
+			Expected: "    1ms",
+		},
+		{
+			Input:    "0.1s",
+			Expected: "  100ms",
+		},
+		{
+			Input:    "1s",
+			Expected: " 1.00s ",
+		},
+		{
+			Input:    "2.1s",
+			Expected: " 2.10s ",
+		},
+		{
+			Input:    "1m",
+			Expected: " 1m  0s",
+		},
+		{
+			Input:    "3m2.1s",
+			Expected: " 3m  2s",
+		},
+		{
+			Input:    "1h",
+			Expected: " 1h  0m",
+		},
+		{
+			Input:    "4h3m2.1s",
+			Expected: " 4h  3m",
+		},
+		{
+			Input:    "124h3m2.1s",
+			Expected: " 5d  4h",
+		},
+		{
+			Input:    "124h3m2.0s",
+			Expected: " 5d  4h",
+		},
+	}
+
+	for _, tc := range cases {
+		duration, _ := time.ParseDuration(tc.Input)
+		executionTime := &Executiontime{}
+		executionTime.Ms = duration.Milliseconds()
+		output := executionTime.formatDurationLucky7()
+		assert.Equal(t, tc.Expected, output)
+	}
+
+	// Extra fuzz test
+	var timestamp int64 = 1
+	var ms1000days int64 = 1000 * 24 * 60 * 60 * 1000
+
+	// log(ms1000days, 1.5) is approx 62.1
+	for timestamp < ms1000days {
+		timestamp = int64(math.Ceil(float64(timestamp) * 1.5))
+
+		executionTime := (&Executiontime{
+			Ms: timestamp,
+		}).formatDurationLucky7()
+
+		// Lucky 7!!
+		assert.Equal(t, len(executionTime), 7)
+	}
+}