Add initial runtime prototype
This commit is contained in:
parent
8cad290616
commit
61fede23ab
11 changed files with 581 additions and 0 deletions
14
go.mod
Normal file
14
go.mod
Normal file
|
@ -0,0 +1,14 @@
|
|||
module github.com/codemicro/adventOfCode
|
||||
|
||||
go 1.17
|
||||
|
||||
require (
|
||||
github.com/AlecAivazis/survey/v2 v2.3.2 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/mattn/go-colorable v0.1.2 // indirect
|
||||
github.com/mattn/go-isatty v0.0.8 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b // indirect
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 // indirect
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 // indirect
|
||||
golang.org/x/text v0.3.3 // indirect
|
||||
)
|
30
go.sum
Normal file
30
go.sum
Normal file
|
@ -0,0 +1,30 @@
|
|||
github.com/AlecAivazis/survey/v2 v2.3.2 h1:TqTB+aDDCLYhf9/bD2TwSO8u8jDSmMUd2SUVO4gCnU8=
|
||||
github.com/AlecAivazis/survey/v2 v2.3.2/go.mod h1:TH2kPCDU3Kqq7pLbnCWwZXDBjnhZtmsCle5EiYDJ2fg=
|
||||
github.com/Netflix/go-expect v0.0.0-20180615182759-c93bf25de8e8/go.mod h1:oX5x61PbNXchhh0oikYAH+4Pcfw5LKv21+Jnpr6r6Pc=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/hinshun/vt10x v0.0.0-20180616224451-1954e6464174/go.mod h1:DqJ97dSdRW1W22yXSB90986pcOyQ7r45iio1KN2ez1A=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||
github.com/kr/pty v1.1.4/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56 h1:b8jxX3zqjpqb2LklXPzKSGJhzyxCOZSz8ncv8Nv+y7w=
|
||||
golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
62
runtime/challenge/challenge.go
Normal file
62
runtime/challenge/challenge.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
package challenge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/codemicro/adventOfCode/runtime/util"
|
||||
)
|
||||
|
||||
type Challenge struct {
|
||||
Number int
|
||||
Name string
|
||||
Dir string
|
||||
}
|
||||
|
||||
func (c *Challenge) String() string {
|
||||
return fmt.Sprintf("%d - %s", c.Number, c.Name)
|
||||
}
|
||||
|
||||
var challengeDirRegexp = regexp.MustCompile(`(?m)^(\d{2})-([a-zA-Z]+)$`)
|
||||
|
||||
func getChallengeDirs(dir string) ([]string, error) {
|
||||
dirEntries, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var res []string
|
||||
for _, entry := range dirEntries {
|
||||
if entry.IsDir() && challengeDirRegexp.MatchString(entry.Name()) {
|
||||
res = append(res, entry.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func ListingFromDir(sourceDir string) ([]*Challenge, error) {
|
||||
|
||||
dirs, err := getChallengeDirs(sourceDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var o []*Challenge
|
||||
for _, dir := range dirs {
|
||||
x := strings.Split(dir, "-")
|
||||
dayInt, _ := strconv.Atoi(x[0]) // error ignored because regex should have ensured this is ok
|
||||
dayTitle := util.CamelToTitle(x[1])
|
||||
o = append(o, &Challenge{
|
||||
Number: dayInt,
|
||||
Name: dayTitle,
|
||||
Dir: filepath.Join(sourceDir, dir),
|
||||
})
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
36
runtime/challenge/challengeInfo.go
Normal file
36
runtime/challenge/challengeInfo.go
Normal file
|
@ -0,0 +1,36 @@
|
|||
package challenge
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
type ChallengeInfo struct {
|
||||
InputFile string `json:"inputFile"`
|
||||
TestCases struct {
|
||||
One []struct {
|
||||
Input string `json:"input"`
|
||||
Expected int64 `json:"expected"`
|
||||
} `json:"one"`
|
||||
Two []struct {
|
||||
Input string `json:"input"`
|
||||
Expected int `json:"expected"`
|
||||
} `json:"two"`
|
||||
} `json:"testCases"`
|
||||
}
|
||||
|
||||
func LoadChallengeInfo(fname string) (*ChallengeInfo, error) {
|
||||
|
||||
fcont, err := ioutil.ReadFile(fname)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := new(ChallengeInfo)
|
||||
err = json.Unmarshal(fcont, c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
26
runtime/challenge/implementations.go
Normal file
26
runtime/challenge/implementations.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package challenge
|
||||
|
||||
import (
|
||||
"github.com/codemicro/adventOfCode/runtime/runners"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (c *Challenge) GetImplementations() ([]string, error) {
|
||||
dirEntries, err := os.ReadDir(c.Dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var o []string
|
||||
for _, de := range dirEntries {
|
||||
if !de.IsDir() {
|
||||
continue
|
||||
}
|
||||
if _, ok := runners.Available[strings.ToLower(de.Name())]; ok {
|
||||
o = append(o, de.Name())
|
||||
}
|
||||
}
|
||||
|
||||
return o, nil
|
||||
}
|
118
runtime/main.go
Normal file
118
runtime/main.go
Normal file
|
@ -0,0 +1,118 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/codemicro/adventOfCode/runtime/challenge"
|
||||
"github.com/codemicro/adventOfCode/runtime/runners"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
challengeDir = "challenges"
|
||||
challengeInfoFile = "info.json"
|
||||
)
|
||||
|
||||
func userSelect(question string, choices []string) (int, error) {
|
||||
var o string
|
||||
prompt := &survey.Select{
|
||||
Message: question,
|
||||
Options: choices,
|
||||
}
|
||||
err := survey.AskOne(prompt, &o)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
for i, x := range choices {
|
||||
if x == o {
|
||||
return i, nil
|
||||
}
|
||||
}
|
||||
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func run() error {
|
||||
|
||||
// List and select challenges
|
||||
|
||||
challenges, err := challenge.ListingFromDir(challengeDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var selectedChallengeIndex int
|
||||
{
|
||||
var opts []string
|
||||
for _, c := range challenges {
|
||||
opts = append(opts, c.String())
|
||||
}
|
||||
|
||||
chosen, err := userSelect("Which challenge do you want to run?", opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selectedChallengeIndex = chosen
|
||||
}
|
||||
selectedChallenge := challenges[selectedChallengeIndex]
|
||||
|
||||
// List and select implementations
|
||||
|
||||
implementations, err := selectedChallenge.GetImplementations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var selectedImplementationIndex int
|
||||
{
|
||||
var opts []string
|
||||
for _, i := range implementations {
|
||||
opts = append(opts, runners.RunnerNames[i])
|
||||
}
|
||||
|
||||
chosen, err := userSelect("Which implementation do you want to use?", opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
selectedImplementationIndex = chosen
|
||||
}
|
||||
selectedImplementation := implementations[selectedImplementationIndex]
|
||||
|
||||
// Load info.json file
|
||||
challengeInfo, err := challenge.LoadChallengeInfo(filepath.Join(selectedChallenge.Dir, challengeInfoFile))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Load challenge input
|
||||
challengeInput, err := ioutil.ReadFile(filepath.Join(selectedChallenge.Dir, challengeInfo.InputFile))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
challengeInputString := string(challengeInput)
|
||||
|
||||
runner := runners.Available[selectedImplementation](selectedChallenge.Dir)
|
||||
runner.Queue(&runners.Task{
|
||||
Part: 1,
|
||||
Input: challengeInputString,
|
||||
})
|
||||
|
||||
for roe := range runner.Run() {
|
||||
if roe.Error != nil {
|
||||
return roe.Error
|
||||
}
|
||||
fmt.Println(*roe.Result)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
if err := run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
131
runtime/runners/comm.go
Normal file
131
runtime/runners/comm.go
Normal file
|
@ -0,0 +1,131 @@
|
|||
package runners
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Task struct {
|
||||
Part Part `json:"part"`
|
||||
Input string `json:"input"`
|
||||
OutputDir string `json:"output_dir,omitempty"`
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
TaskNumber int `json:"task_n"`
|
||||
Ok bool `json:"ok"`
|
||||
Output string `json:"output"`
|
||||
Duration float32 `json:"duration"`
|
||||
}
|
||||
|
||||
func makeErrorChan(err error) chan ResultOrError {
|
||||
c := make(chan ResultOrError, 1)
|
||||
c <- ResultOrError{Error: err}
|
||||
close(c)
|
||||
return c
|
||||
}
|
||||
|
||||
// custom writer type
|
||||
|
||||
type customWriter struct {
|
||||
pending []byte
|
||||
entries [][]byte
|
||||
mux sync.Mutex
|
||||
}
|
||||
|
||||
func (c *customWriter) Write(b []byte) (int, error) {
|
||||
var n int
|
||||
|
||||
c.mux.Lock()
|
||||
for _, x := range b {
|
||||
if x == '\n' {
|
||||
c.entries = append(c.entries, c.pending)
|
||||
c.pending = nil
|
||||
} else {
|
||||
c.pending = append(c.pending, x)
|
||||
}
|
||||
n += 1
|
||||
}
|
||||
c.mux.Unlock()
|
||||
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (c *customWriter) GetEntry() ([]byte, error) {
|
||||
c.mux.Lock()
|
||||
defer c.mux.Unlock()
|
||||
if len(c.entries) == 0 {
|
||||
return nil, errors.New("no entries")
|
||||
}
|
||||
var x []byte
|
||||
x, c.entries = c.entries[0], c.entries[1:]
|
||||
return x, nil
|
||||
}
|
||||
|
||||
func readResultsFromCommand(cmd *exec.Cmd, cleanupFn func()) chan ResultOrError {
|
||||
|
||||
stdoutWriter := &customWriter{}
|
||||
stderrBuffer := new(bytes.Buffer)
|
||||
|
||||
cmd.Stdout = stdoutWriter
|
||||
cmd.Stderr = stderrBuffer
|
||||
|
||||
err := cmd.Start()
|
||||
if err != nil {
|
||||
return makeErrorChan(err)
|
||||
}
|
||||
|
||||
// Command status listener
|
||||
status := make(chan bool) // true if success, false if failure
|
||||
|
||||
go func() {
|
||||
_ = cmd.Wait()
|
||||
status <- cmd.ProcessState.Success()
|
||||
close(status)
|
||||
}()
|
||||
|
||||
// Now let's read some results
|
||||
|
||||
c := make(chan ResultOrError)
|
||||
|
||||
go func() {
|
||||
readerLoop:
|
||||
for {
|
||||
inp, err := stdoutWriter.GetEntry()
|
||||
// will return an error if there is nothing to retrieve
|
||||
if err == nil {
|
||||
|
||||
res := new(Result)
|
||||
err = json.Unmarshal(inp, res)
|
||||
if err != nil {
|
||||
// echo anything that won't parse to stdout (this lets us add debug print statements)
|
||||
fmt.Printf("AA %#v\n", strings.TrimSpace(string(inp)))
|
||||
} else {
|
||||
c <- ResultOrError{Result: res}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
select {
|
||||
case successfulFinish := <-status:
|
||||
if !successfulFinish {
|
||||
c <- ResultOrError{Error: errors.New("run failed: " + stderrBuffer.String())}
|
||||
}
|
||||
break readerLoop
|
||||
default:
|
||||
}
|
||||
}
|
||||
close(c)
|
||||
|
||||
if cleanupFn != nil {
|
||||
cleanupFn()
|
||||
}
|
||||
}()
|
||||
|
||||
return c
|
||||
}
|
46
runtime/runners/interface/python.py
Normal file
46
runtime/runners/interface/python.py
Normal file
|
@ -0,0 +1,46 @@
|
|||
from py import Challenge
|
||||
|
||||
import time
|
||||
import json
|
||||
|
||||
TASKS_STR = """{{ .TasksJSON }}"""
|
||||
TASKS = json.loads(TASKS_STR)
|
||||
|
||||
def send_result(task_number, ok, output, duration):
|
||||
print(json.dumps({
|
||||
"task_n": task_number,
|
||||
"ok": ok,
|
||||
"output": str(output) if output is not None else "",
|
||||
"duration": float(duration),
|
||||
}), flush=True)
|
||||
|
||||
for task_number, task in enumerate(TASKS):
|
||||
taskPart = task["part"]
|
||||
|
||||
run = None
|
||||
|
||||
if taskPart == 1:
|
||||
run = lambda: Challenge().one(task["input"])
|
||||
elif taskPart == 2:
|
||||
run = lambda: Challenge().two(task["input"])
|
||||
elif taskPart == 3:
|
||||
run = lambda: Challenge().vis(task["input"], task["output_dir"])
|
||||
else:
|
||||
send_result(task_number, False, "unknown task part", 0)
|
||||
continue
|
||||
|
||||
start_time = time.time()
|
||||
res = None
|
||||
err = None
|
||||
try:
|
||||
res = run()
|
||||
except Exception as e:
|
||||
err = f"{type(e)}: {e}"
|
||||
end_time = time.time()
|
||||
|
||||
running_time = end_time-start_time
|
||||
|
||||
if err is not None:
|
||||
send_result(task_number, False, err, running_time)
|
||||
else:
|
||||
send_result(task_number, True, res, running_time)
|
70
runtime/runners/pythonRunner.go
Normal file
70
runtime/runners/pythonRunner.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package runners
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
const python3Installation = "python3"
|
||||
|
||||
type pythonRunner struct {
|
||||
dir string
|
||||
tasks []*Task
|
||||
}
|
||||
|
||||
func newPythonRunner(dir string) Runner {
|
||||
return &pythonRunner{
|
||||
dir: dir,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *pythonRunner) Queue(task *Task) {
|
||||
p.tasks = append(p.tasks, task)
|
||||
}
|
||||
|
||||
//go:embed interface/python.py
|
||||
var pythonInterface string
|
||||
|
||||
func (p *pythonRunner) Run() chan ResultOrError {
|
||||
|
||||
wrapperFilename := "runtime-wrapper.py"
|
||||
wrapperFilepath := filepath.Join(p.dir, wrapperFilename)
|
||||
|
||||
// Generate interaction code
|
||||
taskJSON, err := json.Marshal(p.tasks)
|
||||
if err != nil {
|
||||
return makeErrorChan(err)
|
||||
}
|
||||
|
||||
interactionCodeBuffer := new(bytes.Buffer)
|
||||
{
|
||||
templ := template.Must(template.New("").Parse(pythonInterface))
|
||||
err := templ.Execute(interactionCodeBuffer, struct{
|
||||
TasksJSON string
|
||||
}{string(taskJSON)})
|
||||
if err != nil {
|
||||
return makeErrorChan(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Save interaction code
|
||||
err = ioutil.WriteFile(wrapperFilepath, interactionCodeBuffer.Bytes(), 0644)
|
||||
if err != nil {
|
||||
return makeErrorChan(err)
|
||||
}
|
||||
|
||||
// Run Python and gather output
|
||||
cmd := exec.Command(python3Installation, "-B", wrapperFilename) // -B prevents .pyc files from being written
|
||||
cmd.Dir = p.dir
|
||||
|
||||
return readResultsFromCommand(cmd, func() {
|
||||
// Remove leftover files
|
||||
_ = os.Remove(wrapperFilepath)
|
||||
})
|
||||
}
|
29
runtime/runners/runners.go
Normal file
29
runtime/runners/runners.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package runners
|
||||
|
||||
type Part uint8
|
||||
|
||||
const (
|
||||
PartOne Part = iota + 1
|
||||
PartTwo
|
||||
Visualise
|
||||
)
|
||||
|
||||
type Runner interface {
|
||||
Queue(task *Task)
|
||||
Run() chan ResultOrError
|
||||
}
|
||||
|
||||
type ResultOrError struct {
|
||||
Result *Result
|
||||
Error error
|
||||
}
|
||||
|
||||
type RunnerCreator func(dir string) Runner
|
||||
|
||||
var Available = map[string]RunnerCreator{
|
||||
"py": newPythonRunner,
|
||||
}
|
||||
|
||||
var RunnerNames = map[string]string{
|
||||
"py": "Python",
|
||||
}
|
19
runtime/util/util.go
Normal file
19
runtime/util/util.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"unicode"
|
||||
)
|
||||
|
||||
func CamelToTitle(x string) string {
|
||||
var o string
|
||||
for i, char := range x {
|
||||
if i == 0 {
|
||||
o += string(unicode.ToUpper(char))
|
||||
} else if unicode.IsUpper(char) {
|
||||
o += " " + string(char)
|
||||
} else {
|
||||
o += string(char)
|
||||
}
|
||||
}
|
||||
return o
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue