Голанский округ до ближайшего 0,05

Я ищу функцию округления до ближайшего 0,05 в Голанге. Конечный результат использования функции всегда должен быть в 0,05 раза.


Вот несколько примеров выходов для функции, которую я ищу: (Функция Round еще не существует, я надеюсь, что она может быть включена в ответ)

Round(0.363636) // 0.35
Round(3.232)    // 3.25
Round(0.4888)   // 0.5

Я искал вокруг целую вечность и не нашел ответов.

Ответ 1

Go 1.10 был выпущен, и он добавляет функцию math.Round(). Эта функция округляется до ближайшего целого числа (которое в основном представляет собой операцию "округление до ближайшей версии 1.0" ), и с помощью этого мы можем легко построить функцию, которая округляется до единицы по нашему выбору:

func Round(x, unit float64) float64 {
    return math.Round(x/unit) * unit
}

Тестирование:

fmt.Println(Round(0.363636, 0.05)) // 0.35
fmt.Println(Round(3.232, 0.05))    // 3.25
fmt.Println(Round(0.4888, 0.05))   // 0.5

fmt.Println(Round(-0.363636, 0.05)) // -0.35
fmt.Println(Round(-3.232, 0.05))    // -3.25
fmt.Println(Round(-0.4888, 0.05))   // -0.5

Попробуйте на Go Playground.

Исходный ответ следует за тем, который был создан в эпоху, когда не существует math.Round(), а также детализирует логику нашей пользовательской функции Round(). Это здесь для образовательных целей.


В эпоху pre-Go1.10 не было math.Round(). Но...

Задачи округления могут быть легко реализованы с помощью преобразования float64 = > int64, но следует позаботиться о том, чтобы преобразование с плавающей точкой в ​​int не округлялось, а сохраняло целочисленную часть (подробности см. в Go: преобразование float64 в int с множителем).

Например:

var f float64
f = 12.3
fmt.Println(int64(f)) // 12
f = 12.6
fmt.Println(int64(f)) // 12

Результат 12 в обоих случаях - целая часть. Чтобы получить функциональность округления, просто добавьте 0.5:

f = 12.3
fmt.Println(int64(f + 0.5)) // 12
f = 12.6
fmt.Println(int64(f + 0.5)) // 13

Пока все хорошо. Но мы не хотим округлять до целых чисел. Если бы мы хотели округлить до 1 дробной цифры, мы бы умножали на 10 до добавления 0.5 и конвертировали:

f = 12.31
fmt.Println(float64(int64(f*10+0.5)) / 10) // 12.3
f = 12.66
fmt.Println(float64(int64(f*10+0.5)) / 10) // 12.7

Таким образом, в основном вы умножаетесь на обратную часть единицы, к которой вы хотите округлить. Чтобы округлить до 0.05 единиц, умножьте на 1/0.05 = 20:

f = 12.31
fmt.Println(float64(int64(f*20+0.5)) / 20) // 12.3
f = 12.66
fmt.Println(float64(int64(f*20+0.5)) / 20) // 12.65

Включение этого в функцию:

func Round(x, unit float64) float64 {
    return float64(int64(x/unit+0.5)) * unit
}

Используя его:

fmt.Println(Round(0.363636, 0.05)) // 0.35
fmt.Println(Round(3.232, 0.05))    // 3.25
fmt.Println(Round(0.4888, 0.05))   // 0.5

Попробуйте примеры на Go Playground.

Обратите внимание, что округление 3.232 с помощью unit=0.05 не будет точно печатать 3.25, а 0.35000000000000003. Это связано с тем, что цифры float64 сохраняются с использованием конечной точности, называемой IEEE-754. Подробнее см. Golang, конвертирующий float64 в int error.

Также обратите внимание, что unit может быть "любым" числом. Если он 1, то Round() в основном округляется до ближайшего целого числа. Если он 10, он округляется до десятков, если он 0.01, округляет до двух цифр фракции.

Также обратите внимание, что когда вы вызываете Round() с отрицательным числом, вы можете получить неожиданный результат:

fmt.Println(Round(-0.363636, 0.05)) // -0.3
fmt.Println(Round(-3.232, 0.05))    // -3.2
fmt.Println(Round(-0.4888, 0.05))   // -0.45

Это связано с тем, что, как сказано ранее, преобразование сохраняет целочисленную часть, и, например, целая часть -1.6 равна -1 (которая больше, чем -1.6, а целая часть 1.6 равна 1, который меньше 1.6).

Если вы хотите, чтобы -0.363636 стал -0.35 вместо -0.30, тогда в случае отрицательных чисел добавьте -0.5 вместо 0.5 внутри функции Round(). См. Нашу улучшенную функцию Round2():

func Round2(x, unit float64) float64 {
    if x > 0 {
        return float64(int64(x/unit+0.5)) * unit
    }
    return float64(int64(x/unit-0.5)) * unit
}

И используя его:

fmt.Println(Round2(-0.363636, 0.05)) // -0.35
fmt.Println(Round2(-3.232, 0.05))    // -3.25
fmt.Println(Round2(-0.4888, 0.05))   // -0.5

EDIT:

Чтобы ответить на ваш комментарий: потому что вы не "любите" неточное 0.35000000000000003, вы предложили отформатировать его и повторно разобрать его так:

formatted, err := strconv.ParseFloat(fmt.Sprintf("%.2f", rounded), 64)

И это "похоже" приводит к точному результату, так как печать дает именно 0.35.

Но это всего лишь "иллюзия". Поскольку 0.35 не может быть представлен конечными битами с использованием стандарта IEEE-754, не имеет значения, что вы делаете с номером, если вы храните его в значении типа float64, это будет не точно 0.35 ( но номер IEEE-754 очень близок к нему). То, что вы видите, fmt.Println() печатает его как 0.35, потому что fmt.Println() уже делает некоторое округление.

Но если вы попытаетесь напечатать его с большей точностью:

fmt.Printf("%.30f\n", Round(0.363636, 0.05))
fmt.Printf("%.30f\n", Round(3.232, 0.05))
fmt.Printf("%.30f\n", Round(0.4888, 0.05))

Вывод: он не более приятный (может быть, даже уродливый): попробуйте на Go Playground:

0.349999999999999977795539507497
3.250000000000000000000000000000
0.500000000000000000000000000000

Обратите внимание, что с другой стороны 3.25 и 0.5 точны, потому что они могут быть представлены конечными битами точно, потому что представление в двоичном формате:

3.25 = 3 + 0.25 = 11.01binary
0.5 = 0.1binary

Какой урок? Не стоит форматировать и повторно анализировать результат, так как он не будет точным (просто другое значение float64, которое по правилам форматирования по умолчанию fmt.Println() может быть приятнее при печати). Если вам нужен хороший печатный формат, просто форматируйте с точностью, например:

func main() {
    fmt.Printf("%.3f\n", Round(0.363636, 0.05))
    fmt.Printf("%.3f\n", Round(3.232, 0.05))
    fmt.Printf("%.3f\n", Round(0.4888, 0.05))
}

func Round(x, unit float64) float64 {
    return float64(int64(x/unit+0.5)) * unit
}

И это будет точно (попробуйте на Go Playground):

0.350
3.250
0.500

Или просто умножьте их на 100 и работайте с целыми числами, чтобы не возникало ошибки представления или округления.