glasio/las.go

1048 строки
35 KiB
Go
Исходник Обычный вид История

2020-04-28 22:04:01 +03:00
// (c) softland 2020
// softlandia@gmail.com
2020-02-27 19:21:49 +03:00
package glasio
import (
"bufio"
2020-04-10 00:55:08 +03:00
"bytes"
2020-02-27 19:21:49 +03:00
"errors"
"fmt"
"io"
2020-03-14 00:32:03 +03:00
"io/ioutil"
2020-02-27 19:21:49 +03:00
"math"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/softlandia/cpd"
"github.com/softlandia/xlib"
)
///format strings represent structure of LAS file
const (
2020-05-19 01:18:23 +03:00
_LasFirstLine = "~Version information\n"
_LasVersion = "VERS. %3.1f : glas (c) softlandia@gmail.com\n"
2020-02-27 19:21:49 +03:00
_LasWrap = "WRAP. NO : ONE LINE PER DEPTH STEP\n"
2020-05-19 01:18:23 +03:00
_LasWellInfoSec = "~Well information\n"
2020-02-27 19:21:49 +03:00
_LasMnemonicFormat = "#MNEM.UNIT DATA :DESCRIPTION\n"
_LasStrt = " STRT.M %8.3f :START DEPTH\n"
_LasStop = " STOP.M %8.3f :STOP DEPTH\n"
_LasStep = " STEP.M %8.3f :STEP\n"
_LasNull = " NULL. %9.3f :NULL VALUE\n"
_LasRkb = " RKB.M %8.3f :KB or GL\n"
_LasXcoord = " XWELL.M %8.3f :Well head X coordinate\n"
_LasYcoord = " YWELL.M %8.3f :Well head Y coordinate\n"
_LasOilComp = " COMP. %-43.43s:OIL COMPANY\n"
_LasWell = " WELL. %-43.43s:WELL\n"
_LasField = " FLD . %-43.43s:FIELD\n"
_LasLoc = " LOC . %-43.43s:LOCATION\n"
_LasCountry = " CTRY. %-43.43s:COUNTRY\n"
_LasServiceComp = " SRVC. %-43.43s:SERVICE COMPANY\n"
_LasDate = " DATE. %-43.43s:DATE\n"
_LasAPI = " API . %-43.43s:API NUMBER\n"
_LasUwi = " UWI . %-43.43s:UNIVERSAL WELL INDEX\n"
_LasCurvSec = "~Curve Information Section\n"
_LasCurvFormat = "#MNEM.UNIT :DESCRIPTION\n"
_LasCurvDept = " DEPT.M :\n"
_LasCurvLine = " %s.%s :\n"
_LasDataSec = "~ASCII Log Data\n"
//secName: 0 - empty, 1 - Version, 2 - Well info, 3 - Curve info, 4 - dAta
lasSecIgnore = 0
2020-05-10 04:20:51 +03:00
lasSecVersion = 1
2020-02-27 19:21:49 +03:00
lasSecWellInfo = 2
lasSecCurInfo = 3
lasSecData = 4
)
2020-06-10 19:39:48 +03:00
// HeaderParam - any parameter of LAS
type HeaderParam struct {
lineNo int // number of line in source file
source, // text line from source file contain this parameter
name, // name of parameter: STOP, WELL, SP - curve name also
Val, // parameter value
Unit, // unit of parameter
Description string // descrioption of parameter
2020-04-10 00:55:08 +03:00
}
2020-06-10 19:39:48 +03:00
// HeaderSection - contain parameters of Well section
type HeaderSection map[string]HeaderParam
2020-02-27 19:21:49 +03:00
// Las - class to store las file
// input code page autodetect
// at read file always code page converted to UTF
// at save file code page converted to specifyed in Las.toCodePage
//TODO add pointer to cfg
2020-04-28 22:04:01 +03:00
//TODO при создании объекта las есть возможность указать кодировку записи, нужна возможность указать явно кодировку чтения
2020-02-27 19:21:49 +03:00
type Las struct {
2020-06-11 00:00:45 +03:00
rows []string // buffer for read source file, only converted to UTF-8 no any othe change
2020-06-11 23:49:11 +03:00
nRows int // actually count of lines in source file
2020-06-11 00:00:45 +03:00
FileName string // file name from load
File *os.File // the file from which we are reading
Reader io.Reader // reader created from File, provides decode from codepage to UTF-8
scanner *bufio.Scanner // scanner
Ver float64 // version 1.0, 1.2 or 2.0
Wrap string // YES || NO
Strt float64 // start depth
Stop float64 // stop depth
Step float64 // depth step
Null float64 // value interpreted as empty
Well string // well name
Rkb float64 // altitude KB
Logs LasCurves // store all logs
LogDic *map[string]string // external dictionary of standart log name - mnemonics
VocDic *map[string]string // external vocabulary dictionary of log mnemonic
Warnings TLasWarnings // slice of warnings occure on read or write
ePoints int // expected count (.)
nPoints int // actually count (.)
oCodepage cpd.IDCodePage // codepage to save, default xlib.CpWindows1251. to special value, specify at make: NewLas(cp...)
iDuplicate int // index of duplicated mnemonic, increase by 1 if found duplicated
currentLine int // index of current line in readed file
maxWarningCount int // default maximum warning count
stdNull float64 // default null value
2020-06-10 19:39:48 +03:00
VerSec,
WellSec,
CurSec,
ParSec,
OthSec HeaderSection
2020-02-27 19:21:49 +03:00
}
2020-04-10 00:55:08 +03:00
var (
2020-06-10 19:39:48 +03:00
// ExpPoints - первоначальный размер слайсов для хранения данных Logs.D и Logs.V
// ожидаемое количество точек данных, до чтения мы не можем знать сколько точек будет фактически прочитано
// данные увеличиваются при необходимости и обрезаются после окончания чтения
2020-04-10 00:55:08 +03:00
ExpPoints int = 1000
// StdNull - пустое значение
StdNull float64 = -999.25
// MaxWarningCount - слишком много сообщений писать смысла нет
MaxWarningCount int = 20
)
2020-02-27 19:21:49 +03:00
//NewLas - make new object Las class
//autodetect code page at load file
2020-03-08 00:34:07 +03:00
//code page to save by default is cpd.CP1251
2020-02-27 19:21:49 +03:00
func NewLas(outputCP ...cpd.IDCodePage) *Las {
las := new(Las)
las.Ver = 2.0
las.Wrap = "NO"
2020-05-10 04:20:51 +03:00
// до того как прочитаем данные мы не можем знать сколько их фактически, предполагаем что 1000, достаточно большой буфер
// избавит от необходимости довыделять память при чтении
las.ePoints = ExpPoints
2020-06-11 23:49:11 +03:00
las.nRows = ExpPoints
las.rows = make([]string, 0, las.nRows)
2020-02-27 19:21:49 +03:00
las.Logs = make(map[string]LasCurve)
2020-06-10 19:39:48 +03:00
las.VerSec = make(HeaderSection)
las.WellSec = make(HeaderSection)
las.CurSec = make(HeaderSection)
las.ParSec = make(HeaderSection)
las.OthSec = make(HeaderSection)
2020-04-10 00:55:08 +03:00
las.maxWarningCount = MaxWarningCount
2020-06-02 20:49:41 +03:00
las.stdNull = StdNull
las.Strt = StdNull
las.Stop = StdNull
las.Step = StdNull
2020-02-27 19:21:49 +03:00
if len(outputCP) > 0 {
las.oCodepage = outputCP[0]
} else {
las.oCodepage = cpd.CP1251
}
//mnemonic dictionary
las.LogDic = nil
//external log dictionary
las.VocDic = nil
//счётчик повторяющихся мнемоник, увеличивается каждый раз на 1, используется при переименовании мнемоники
las.iDuplicate = 0
return las
}
2020-04-28 22:04:01 +03:00
// selectSection - analize first char after ~
// ~V - section vertion
// ~W - well info section
// ~C - curve info section
// ~A - data section
func (las *Las) selectSection(r rune) int {
2020-02-27 19:21:49 +03:00
switch r {
case 86: //V
2020-05-10 04:20:51 +03:00
return lasSecVersion //version section
2020-02-27 19:21:49 +03:00
case 118: //v
2020-05-10 04:20:51 +03:00
return lasSecVersion //version section
2020-02-27 19:21:49 +03:00
case 87: //W
return lasSecWellInfo //well info section
case 119: //w
return lasSecWellInfo //well info section
case 67: //C
return lasSecCurInfo //curve section
case 99: //c
return lasSecCurInfo //curve section
case 65: //A
return lasSecData //data section
case 97: //a
return lasSecData //data section
default:
return lasSecIgnore
}
}
// IsWraped - return true if WRAP == YES
2020-04-28 22:04:01 +03:00
func (las *Las) IsWraped() bool {
return strings.Contains(strings.ToUpper(las.Wrap), "Y") //(strings.Index(strings.ToUpper(o.Wrap), "Y") >= 0)
2020-02-27 19:21:49 +03:00
}
// SaveWarning - save to file all warning
2020-04-28 22:04:01 +03:00
func (las *Las) SaveWarning(fileName string) error {
if las.Warnings.Count() == 0 {
2020-02-27 19:21:49 +03:00
return nil
}
oFile, err := os.Create(fileName)
if err != nil {
return err
}
2020-04-28 22:04:01 +03:00
las.SaveWarningToFile(oFile)
2020-02-27 19:21:49 +03:00
oFile.Close()
return nil
}
// SaveWarningToWriter - store all warning to writer, return count lines writed to
2020-04-28 22:04:01 +03:00
func (las *Las) SaveWarningToWriter(writer *bufio.Writer) int {
n := las.Warnings.Count()
2020-02-27 19:21:49 +03:00
if n == 0 {
return 0
}
2020-04-28 22:04:01 +03:00
for _, w := range las.Warnings {
2020-02-27 19:21:49 +03:00
writer.WriteString(w.String())
writer.WriteString("\n")
}
return n
}
// SaveWarningToFile - store all warning to file, file not close. return count warning writed
2020-04-28 22:04:01 +03:00
func (las *Las) SaveWarningToFile(oFile *os.File) int {
2020-02-27 19:21:49 +03:00
if oFile == nil {
return 0
}
2020-04-28 22:04:01 +03:00
if las.Warnings.Count() == 0 {
2020-02-27 19:21:49 +03:00
return 0
}
2020-04-28 22:04:01 +03:00
oFile.WriteString("**file: " + las.FileName + "**\n")
n := las.Warnings.SaveWarningToFile(oFile)
2020-02-27 19:21:49 +03:00
oFile.WriteString("\n")
return n
}
2020-04-28 22:04:01 +03:00
func (las *Las) addWarning(w TWarning) {
if las.Warnings.Count() < las.maxWarningCount {
las.Warnings = append(las.Warnings, w)
if las.Warnings.Count() == las.maxWarningCount {
las.Warnings = append(las.Warnings, TWarning{0, 0, 0, "*maximum count* of warning reached, change parameter 'maxWarningCount' in 'glas.ini'"})
2020-02-27 19:21:49 +03:00
}
}
}
2020-03-04 00:02:17 +03:00
// GetMnemonic - return Mnemonic from dictionary by Log Name,
// if Mnemonic not found return ""
// if Dictionary is nil, then return ""
2020-04-28 22:04:01 +03:00
func (las *Las) GetMnemonic(logName string) string {
if (las.LogDic == nil) || (las.VocDic == nil) {
2020-03-04 00:02:17 +03:00
return "" //"-"
2020-02-27 19:21:49 +03:00
}
2020-04-28 22:04:01 +03:00
_, ok := (*las.LogDic)[logName]
2020-02-27 19:21:49 +03:00
if ok { //GOOD - название каротажа равно мнемонике
return logName
}
2020-04-28 22:04:01 +03:00
v, ok := (*las.VocDic)[logName]
2020-02-27 19:21:49 +03:00
if ok { //POOR - название загружаемого каротажа найдено в словаре подстановок, мнемоника найдена
return v
}
return ""
}
// Open - load las file
2020-04-13 21:31:31 +03:00
// return error on:
2020-06-02 20:49:41 +03:00
// - file not exist
2020-04-13 21:31:31 +03:00
// - file cannot be decoded to UTF-8
// - las is wrapped
// - las file not contain Curve section
2020-04-28 22:04:01 +03:00
func (las *Las) Open(fileName string) (int, error) {
var err error
las.File, err = os.Open(fileName)
if err != nil {
2020-06-02 20:49:41 +03:00
return 0, err //FATAL error - file not exist
2020-04-28 22:04:01 +03:00
}
defer las.File.Close()
las.FileName = fileName
//create and store Reader, this reader decode to UTF-8
las.Reader, err = cpd.NewReader(las.File)
if err != nil {
2020-06-02 20:49:41 +03:00
return 0, err //FATAL error - file cannot be decoded to UTF-8
2020-04-28 22:04:01 +03:00
}
2020-06-02 20:49:41 +03:00
// prepare file to read
2020-04-28 22:04:01 +03:00
las.scanner = bufio.NewScanner(las.Reader)
las.currentLine = 0
las.LoadHeader()
stdChecker := NewStdChecker()
2020-06-02 20:49:41 +03:00
// check for FATAL errors
2020-05-10 04:20:51 +03:00
r := stdChecker.check(las)
2020-06-02 20:49:41 +03:00
las.storeHeaderWarning(r)
if err = r.fatal(); err != nil {
2020-05-10 04:20:51 +03:00
return 0, err
}
2020-04-28 22:04:01 +03:00
if r.nullWrong() {
las.SetNull(las.stdNull)
}
if r.strtWrong() {
h := las.GetStrtFromData() // return las.Null if cannot find strt in the data section.
if h == las.Null {
las.addWarning(TWarning{directOnRead, lasSecWellInfo, -1, fmt.Sprint("__WRN__ STRT parameter on data is wrong setting to 0")})
las.setStrt(0)
}
las.setStrt(h)
}
2020-04-28 22:04:01 +03:00
if r.stepWrong() {
2020-06-02 20:49:41 +03:00
h := las.GetStepFromData() // return las.Null if cannot calculate step from data
2020-04-28 22:04:01 +03:00
if h == las.Null {
2020-06-10 19:39:48 +03:00
las.addWarning(TWarning{directOnRead, lasSecWellInfo, las.currentLine, fmt.Sprint("__WRN__ STEP parameter on data is wrong")})
2020-04-28 22:04:01 +03:00
}
2020-06-02 20:49:41 +03:00
las.setStep(h)
2020-04-28 22:04:01 +03:00
}
return las.ReadDataSec(fileName)
}
2020-06-11 23:49:11 +03:00
// GetRows - get internal field 'rows'
func (las *Las) GetRows() []string {
return las.rows
}
// ReadRows - reads to buffer 'rows' and return total count of read lines
func (las *Las) ReadRows() int {
for i := 0; las.scanner.Scan(); i++ {
las.rows = append(las.rows, las.scanner.Text())
}
return len(las.rows)
2020-06-11 00:00:45 +03:00
}
// Open2 -
func (las *Las) Open2(fileName string) (int, error) {
var err error
las.File, err = os.Open(fileName)
if err != nil {
return 0, err //FATAL error - file not exist
}
defer las.File.Close()
las.FileName = fileName
2020-06-11 23:49:11 +03:00
//create Reader, this reader decode to UTF-8
2020-06-11 00:00:45 +03:00
las.Reader, err = cpd.NewReader(las.File)
if err != nil {
return 0, err //FATAL error - file cannot be decoded to UTF-8
}
// prepare file to read
las.scanner = bufio.NewScanner(las.Reader)
2020-06-11 23:49:11 +03:00
las.ePoints = las.ReadRows()
//las.currentLine = 0
m, _ := las.LoadHeader2()
2020-06-11 00:00:45 +03:00
stdChecker := NewStdChecker()
// check for FATAL errors
r := stdChecker.check(las)
las.storeHeaderWarning(r)
if err = r.fatal(); err != nil {
return 0, err
}
if r.nullWrong() {
las.SetNull(las.stdNull)
}
if r.strtWrong() {
h := las.GetStrtFromData() // return las.Null if cannot find strt in the data section.
if h == las.Null {
las.addWarning(TWarning{directOnRead, lasSecWellInfo, -1, fmt.Sprint("__WRN__ STRT parameter on data is wrong setting to 0")})
las.setStrt(0)
}
las.setStrt(h)
}
if r.stepWrong() {
h := las.GetStepFromData() // return las.Null if cannot calculate step from data
if h == las.Null {
las.addWarning(TWarning{directOnRead, lasSecWellInfo, las.currentLine, fmt.Sprint("__WRN__ STEP parameter on data is wrong")})
}
las.setStep(h)
}
2020-06-11 23:49:11 +03:00
return m, nil //las.ReadDataSec2(m)
2020-02-27 19:21:49 +03:00
}
2020-06-11 23:49:11 +03:00
// LoadHeader2 - new version, after test will be replace verion LoadHeader
// returns the row number with which the data section begins, contain ~A
func (las *Las) LoadHeader2() (int, error) {
2020-02-27 19:21:49 +03:00
s := ""
var err error
secNum := 0
2020-06-11 23:49:11 +03:00
for i := 0; i < len(las.rows); i++ {
s = strings.TrimSpace(las.rows[i])
2020-04-28 22:04:01 +03:00
las.currentLine++
2020-02-27 19:21:49 +03:00
if isIgnoredLine(s) {
continue
}
if s[0] == '~' { //start new section
2020-04-28 22:04:01 +03:00
secNum = las.selectSection(rune(s[1]))
2020-02-27 19:21:49 +03:00
if secNum == lasSecData {
2020-06-10 20:05:16 +03:00
break // reached the data section, stop load header
2020-06-10 19:39:48 +03:00
}
} else {
//if not comment, not empty and not new section => parameter, read it
err = las.ReadParameter(s, secNum)
if err != nil {
2020-06-11 23:49:11 +03:00
las.addWarning(TWarning{directOnRead, secNum, i, fmt.Sprintf("param: '%s' error: %v", s, err)})
2020-06-10 19:39:48 +03:00
}
}
}
2020-06-11 23:49:11 +03:00
return las.currentLine, nil
2020-06-10 19:39:48 +03:00
}
2020-06-11 23:49:11 +03:00
// saveHeaderWarning - забирает и сохраняет варнинги от всех проверок
func (las *Las) storeHeaderWarning(chkResults CheckResults) {
for _, v := range chkResults {
las.addWarning(v.warning)
}
}
/*LoadHeader - read las file and load all section before ~A
secName: 0 - empty, 1 - Version, 2 - Well info, 3 - Curve info, 4 - A data
1. читаем строку
2. если коммент или пустая в игнор
3. если начало секции, определяем какой
4. если началась секция данных заканчиваем
5. читаем одну строку (это один параметер из известной нам секции)
Пока всегда возвращает nil, причин возвращать другое значение пока нет.
*/
func (las *Las) LoadHeader() error {
2020-06-10 19:39:48 +03:00
s := ""
var err error
secNum := 0
for i := 0; las.scanner.Scan(); i++ {
s = strings.TrimSpace(las.scanner.Text())
las.currentLine++
if isIgnoredLine(s) {
continue
}
if s[0] == '~' { //start new section
secNum = las.selectSection(rune(s[1]))
if secNum == lasSecData {
2020-06-10 20:05:16 +03:00
break // reached the data section, stop load header
2020-02-27 19:21:49 +03:00
}
} else {
2020-05-10 04:20:51 +03:00
//if not comment, not empty and not new section => parameter, read it
err = las.ReadParameter(s, secNum)
2020-02-27 19:21:49 +03:00
if err != nil {
2020-06-10 19:39:48 +03:00
las.addWarning(TWarning{directOnRead, secNum, las.currentLine, fmt.Sprintf("param: '%s' error: %v", s, err)})
2020-02-27 19:21:49 +03:00
}
}
}
return nil
}
2020-03-14 00:32:03 +03:00
// ReadParameter - read one parameter
2020-04-28 22:04:01 +03:00
func (las *Las) ReadParameter(s string, secNum int) error {
2020-02-27 19:21:49 +03:00
switch secNum {
2020-05-10 04:20:51 +03:00
case lasSecVersion:
2020-04-28 22:04:01 +03:00
return las.readVersionParam(s)
2020-02-27 19:21:49 +03:00
case lasSecWellInfo:
2020-04-28 22:04:01 +03:00
return las.ReadWellParam(s)
2020-02-27 19:21:49 +03:00
case lasSecCurInfo:
2020-04-28 22:04:01 +03:00
return las.readCurveParam(s)
2020-02-27 19:21:49 +03:00
}
return nil
}
2020-04-28 22:04:01 +03:00
func (las *Las) readVersionParam(s string) error {
2020-02-27 19:21:49 +03:00
var err error
2020-04-10 00:55:08 +03:00
p := NewLasParam(s)
2020-02-27 19:21:49 +03:00
switch p.Name {
case "VERS":
2020-04-28 22:04:01 +03:00
las.Ver, err = strconv.ParseFloat(p.Val, 64)
2020-02-27 19:21:49 +03:00
case "WRAP":
2020-04-28 22:04:01 +03:00
las.Wrap = p.Val
2020-02-27 19:21:49 +03:00
}
return err
}
//ReadWellParam - read parameter from WELL section
2020-04-28 22:04:01 +03:00
func (las *Las) ReadWellParam(s string) error {
2020-02-27 19:21:49 +03:00
var err error
2020-04-10 00:55:08 +03:00
p := NewLasParam(s)
2020-02-27 19:21:49 +03:00
switch p.Name {
case "STRT":
2020-04-28 22:04:01 +03:00
las.Strt, err = strconv.ParseFloat(p.Val, 64)
2020-02-27 19:21:49 +03:00
case "STOP":
2020-04-28 22:04:01 +03:00
las.Stop, err = strconv.ParseFloat(p.Val, 64)
2020-02-27 19:21:49 +03:00
case "STEP":
2020-04-28 22:04:01 +03:00
las.Step, err = strconv.ParseFloat(p.Val, 64)
2020-02-27 19:21:49 +03:00
case "NULL":
2020-04-28 22:04:01 +03:00
las.Null, err = strconv.ParseFloat(p.Val, 64)
2020-02-27 19:21:49 +03:00
case "WELL":
2020-04-28 22:04:01 +03:00
if las.Ver < 2.0 {
las.Well = p.Desc
2020-02-27 19:21:49 +03:00
} else {
2020-04-28 22:04:01 +03:00
las.Well = wellNameFromParam(p)
2020-02-27 19:21:49 +03:00
}
}
if err != nil {
2020-04-28 22:04:01 +03:00
las.addWarning(TWarning{directOnRead, lasSecWellInfo, -1, fmt.Sprintf("detected param: %v, unit:%v, value: %v\n", p.Name, p.Unit, p.Val)})
2020-02-27 19:21:49 +03:00
}
return err
}
//ChangeDuplicateLogName - return non duplicated name of log
//if input name unique, return input name
//if input name not unique, return input name + index duplicate
//index duplicate - Las field, increase
2020-04-28 22:04:01 +03:00
func (las *Las) ChangeDuplicateLogName(name string) string {
2020-02-27 19:21:49 +03:00
s := ""
2020-04-28 22:04:01 +03:00
if _, ok := las.Logs[name]; ok {
las.iDuplicate++
s = fmt.Sprintf("%v", las.iDuplicate)
2020-02-27 19:21:49 +03:00
name += s
}
return name
}
//Разбор одной строки с мнемоникой каротажа
//Разбираем в переменную l а потом сохраняем в map
//Каждый каротаж характеризуется тремя именами
//IName - имя каротажа в исходном файле, может повторятся
//Name - ключ в map хранилище, повторятся не может. если в исходном есть повторение, то Name строится добавлением к IName индекса
2020-03-04 00:02:17 +03:00
//Mnemonic - мнемоника, берётся из словаря, если в словаре не найдено, то ""
2020-04-28 22:04:01 +03:00
func (las *Las) readCurveParam(s string) error {
2020-04-10 00:55:08 +03:00
l := NewLasCurve(s)
2020-05-10 04:20:51 +03:00
// поскольку этот метод вызывается для каждой кривой в секции ~Curve, то заново определять для каждой кривой количество ожидаемых
// точек через las.GetExpectedPointsCount() СТРАННО
l.Init(len(las.Logs), las.GetMnemonic(l.Name), las.ChangeDuplicateLogName(l.Name), las.GetExpectedPointsCount(las.Strt, las.Stop, las.Step))
2020-04-28 22:04:01 +03:00
las.Logs[l.Name] = l //добавление в хранилище кривой каротажа с колонкой глубин
2020-02-27 19:21:49 +03:00
return nil
}
2020-05-10 04:20:51 +03:00
// GetExpectedPointsCount - оценка количества точек по параметрам STEP, STRT, STOP
// перед чтением данных пытаемся оценить их количество
// в правильной ситуации количество должно быть равныи (stop - strt)/step
// случаи ошибок:
// 1. step == 0
// 2. strt == stop
// при ошибке вернём предполагаемое (заданное по умолчанию) количество
// стандартные случаи:
// 1. strt < stop & step > 0
// 2. stop < strt & step < 0
func (las *Las) GetExpectedPointsCount(strt, stop, step float64) (m int) {
if step == 0.0 {
2020-04-28 22:04:01 +03:00
return las.ePoints
2020-03-08 00:34:07 +03:00
}
2020-05-10 04:20:51 +03:00
d := stop - strt
if d == 0 {
return las.ePoints
2020-02-27 19:21:49 +03:00
}
2020-05-10 04:20:51 +03:00
m = int(d/step) + 2
2020-02-27 19:21:49 +03:00
if m < 0 {
m = -m
}
return m
}
//expandDept - if actually data points exceeds
2020-04-28 22:04:01 +03:00
func (las *Las) expandDept(d *LasCurve) {
2020-02-27 19:21:49 +03:00
//actual number of points more then expected
2020-04-28 22:04:01 +03:00
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, "actual number of data lines more than expected, check: STRT, STOP, STEP"})
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, "expand number of points"})
2020-02-27 19:21:49 +03:00
//ожидаем удвоения данных
2020-04-28 22:04:01 +03:00
las.ePoints *= 2
2020-03-14 00:32:03 +03:00
//expand first log - dept
2020-04-28 22:04:01 +03:00
newDept := make([]float64, las.ePoints)
2020-05-20 02:31:08 +03:00
copy(newDept, d.D)
d.D = newDept
2020-02-27 19:21:49 +03:00
2020-04-28 22:04:01 +03:00
newLog := make([]float64, las.ePoints)
2020-05-20 02:31:08 +03:00
copy(newLog, d.D)
d.V = newLog
2020-04-28 22:04:01 +03:00
las.Logs[d.Name] = *d
2020-02-27 19:21:49 +03:00
//loop over other logs
2020-04-28 22:04:01 +03:00
n := len(las.Logs)
2020-02-27 19:21:49 +03:00
var l *LasCurve
for j := 1; j < n; j++ {
2020-04-28 22:04:01 +03:00
l, _ = las.logByIndex(j)
newDept := make([]float64, las.ePoints)
2020-05-20 02:31:08 +03:00
copy(newDept, l.D)
l.D = newDept
2020-02-27 19:21:49 +03:00
2020-04-28 22:04:01 +03:00
newLog := make([]float64, las.ePoints)
2020-05-20 02:31:08 +03:00
copy(newLog, l.V)
l.V = newLog
2020-04-28 22:04:01 +03:00
las.Logs[l.Name] = *l
2020-02-27 19:21:49 +03:00
}
}
2020-05-10 04:20:51 +03:00
func isSeparator(r rune) bool {
switch r {
case 9, 32:
return true
default:
return false
}
}
2020-02-27 19:21:49 +03:00
// ReadDataSec - read section of data
2020-04-28 22:04:01 +03:00
func (las *Las) ReadDataSec(fileName string) (int, error) {
2020-02-27 19:21:49 +03:00
var (
v float64
err error
d *LasCurve
l *LasCurve
dept float64
i int
)
2020-05-10 04:20:51 +03:00
// исходя из параметров STRT, STOP и STEP определяем ожидаемое количество строк данных
// данную операцию уже выполняли много раз при считывании кривых
las.ePoints = las.GetExpectedPointsCount(las.Strt, las.Stop, las.Step)
2020-04-28 22:04:01 +03:00
n := len(las.Logs) //количество каротажей, столько колонок данных ожидаем
d, _ = las.logByIndex(0) //dept log
2020-06-11 00:00:45 +03:00
s := ""
for i = 0; las.scanner.Scan(); i++ {
las.currentLine++
if i == las.ePoints {
las.expandDept(d)
}
s = strings.TrimSpace(las.scanner.Text())
// i счётчик не считанных строк, а фактически считанных данных - счётчик добавлений в слайсы данных
//TODO возможно следует завести отдельный счётчик и оставить в покое счётчик цикла
if isIgnoredLine(s) {
i--
continue
}
//first column is DEPT
k := strings.IndexFunc(s, isSeparator)
if k < 0 { //line must have n+1 column and n separated spaces block (+1 becouse first column DEPT)
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, fmt.Sprintf("line: %d is empty, ignore", las.currentLine)})
i--
continue
}
dept, err = strconv.ParseFloat(s[:k], 64)
if err != nil {
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, fmt.Sprintf("first column '%s' not numeric, ignore", s[:k])})
i--
continue
}
d.D[i] = dept
// проверка шага у первых двух точек данных и сравнение с параметром step
//TODO данную проверку следует делать через Checker
if i > 1 {
if math.Pow(((dept-d.D[i-1])-las.Step), 2) > 0.1 {
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, fmt.Sprintf("actual step %5.2f ≠ global STEP %5.2f", (dept - d.D[i-1]), las.Step)})
}
}
// проверка шага между точками [i-1, i] и точками [i-2, i-1] обнаружение немонотонности колонки глубин
if i > 2 {
if math.Pow(((dept-d.D[i-1])-(d.D[i-1]-d.D[i-2])), 2) > 0.1 {
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, fmt.Sprintf("step %5.2f ≠ previously step %5.2f", (dept - d.D[i-1]), (d.D[i-1] - d.D[i-2]))})
dept = d.D[i-1] + las.Step
}
}
s = strings.TrimSpace(s[k+1:]) //cut first column
//цикл по каротажам
for j := 1; j < (n - 1); j++ {
iSpace := strings.IndexFunc(s, isSeparator)
switch iSpace {
case -1: //не все колонки прочитаны, а разделителей уже нет... пробуем игнорировать сроку заполняя оставшиеся каротажи NULLами
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, "not all column are read, set log value to NULL"})
case 0:
v = las.Null
case 1:
v, err = strconv.ParseFloat(s[:1], 64)
default:
v, err = strconv.ParseFloat(s[:iSpace], 64)
}
if err != nil {
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, fmt.Sprintf("can't convert string: '%s' to number, set to NULL", s[:iSpace-1])})
v = las.Null
}
l, err = las.logByIndex(j)
if err != nil {
las.nPoints = i
return i, errors.New("internal ERROR, func (las *Las) readDataSec()::las.logByIndex(j) return error")
}
l.D[i] = dept
l.V[i] = v
s = strings.TrimSpace(s[iSpace+1:])
}
//остаток - последняя колонка
v, err = strconv.ParseFloat(s, 64)
if err != nil {
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, "not all column are read, set log value to NULL"})
v = las.Null
}
l, err = las.logByIndex(n - 1)
if err != nil {
las.nPoints = i
return i, errors.New("internal ERROR, func (las *Las) readDataSec()::las.logByIndex(j) return error on last column")
}
l.D[i] = dept
l.V[i] = v
}
//i - actually readed lines and add (.) to data array
//crop logs to actually len
err = las.SetActuallyNumberPoints(i)
if err != nil {
return 0, err
}
return i, nil
}
// ReadDataSec2 - read from rows
2020-06-11 23:49:11 +03:00
func (las *Las) ReadDataSec2(m int) (int, error) {
2020-06-11 00:00:45 +03:00
var (
v float64
err error
d *LasCurve
l *LasCurve
dept float64
i int
)
2020-06-11 23:49:11 +03:00
las.ePoints = len(las.rows)
2020-06-11 00:00:45 +03:00
n := len(las.Logs) //количество каротажей, столько колонок данных ожидаем
d, _ = las.logByIndex(0) //dept log
2020-02-27 19:21:49 +03:00
s := ""
2020-06-11 23:49:11 +03:00
for i = m; i < len(las.rows); i++ {
s = strings.TrimSpace(las.rows[i])
2020-02-27 19:21:49 +03:00
if isIgnoredLine(s) {
continue
}
//first column is DEPT
2020-05-10 04:20:51 +03:00
k := strings.IndexFunc(s, isSeparator)
if k < 0 { //line must have n+1 column and n separated spaces block (+1 becouse first column DEPT)
2020-04-28 22:04:01 +03:00
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, fmt.Sprintf("line: %d is empty, ignore", las.currentLine)})
2020-02-27 19:21:49 +03:00
continue
}
dept, err = strconv.ParseFloat(s[:k], 64)
if err != nil {
2020-04-28 22:04:01 +03:00
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, fmt.Sprintf("first column '%s' not numeric, ignore", s[:k])})
2020-02-27 19:21:49 +03:00
continue
}
2020-05-20 02:31:08 +03:00
d.D[i] = dept
2020-04-28 22:04:01 +03:00
// проверка шага у первых двух точек данных и сравнение с параметром step
//TODO данную проверку следует делать через Checker
2020-02-27 19:21:49 +03:00
if i > 1 {
2020-05-20 02:31:08 +03:00
if math.Pow(((dept-d.D[i-1])-las.Step), 2) > 0.1 {
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, fmt.Sprintf("actual step %5.2f ≠ global STEP %5.2f", (dept - d.D[i-1]), las.Step)})
2020-02-27 19:21:49 +03:00
}
}
2020-04-28 22:04:01 +03:00
// проверка шага между точками [i-1, i] и точками [i-2, i-1] обнаружение немонотонности колонки глубин
2020-02-27 19:21:49 +03:00
if i > 2 {
2020-05-20 02:31:08 +03:00
if math.Pow(((dept-d.D[i-1])-(d.D[i-1]-d.D[i-2])), 2) > 0.1 {
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, fmt.Sprintf("step %5.2f ≠ previously step %5.2f", (dept - d.D[i-1]), (d.D[i-1] - d.D[i-2]))})
dept = d.D[i-1] + las.Step
2020-02-27 19:21:49 +03:00
}
}
s = strings.TrimSpace(s[k+1:]) //cut first column
//цикл по каротажам
for j := 1; j < (n - 1); j++ {
2020-05-10 04:20:51 +03:00
iSpace := strings.IndexFunc(s, isSeparator)
2020-02-27 19:21:49 +03:00
switch iSpace {
2020-05-19 01:18:23 +03:00
case -1: //не все колонки прочитаны, а разделителей уже нет... пробуем игнорировать сроку заполняя оставшиеся каротажи NULLами
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, "not all column are read, set log value to NULL"})
2020-02-27 19:21:49 +03:00
case 0:
2020-04-28 22:04:01 +03:00
v = las.Null
2020-02-27 19:21:49 +03:00
case 1:
v, err = strconv.ParseFloat(s[:1], 64)
default:
2020-05-19 01:18:23 +03:00
v, err = strconv.ParseFloat(s[:iSpace], 64)
2020-02-27 19:21:49 +03:00
}
if err != nil {
2020-04-28 22:04:01 +03:00
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, fmt.Sprintf("can't convert string: '%s' to number, set to NULL", s[:iSpace-1])})
v = las.Null
2020-02-27 19:21:49 +03:00
}
2020-04-28 22:04:01 +03:00
l, err = las.logByIndex(j)
2020-02-27 19:21:49 +03:00
if err != nil {
2020-04-28 22:04:01 +03:00
las.nPoints = i
return i, errors.New("internal ERROR, func (las *Las) readDataSec()::las.logByIndex(j) return error")
2020-02-27 19:21:49 +03:00
}
2020-05-20 02:31:08 +03:00
l.D[i] = dept
l.V[i] = v
2020-02-27 19:21:49 +03:00
s = strings.TrimSpace(s[iSpace+1:])
}
//остаток - последняя колонка
v, err = strconv.ParseFloat(s, 64)
if err != nil {
las.addWarning(TWarning{directOnRead, lasSecData, las.currentLine, "not all column are read, set log value to NULL"})
2020-04-28 22:04:01 +03:00
v = las.Null
2020-02-27 19:21:49 +03:00
}
2020-04-28 22:04:01 +03:00
l, err = las.logByIndex(n - 1)
2020-02-27 19:21:49 +03:00
if err != nil {
2020-04-28 22:04:01 +03:00
las.nPoints = i
return i, errors.New("internal ERROR, func (las *Las) readDataSec()::las.logByIndex(j) return error on last column")
2020-02-27 19:21:49 +03:00
}
2020-05-20 02:31:08 +03:00
l.D[i] = dept
l.V[i] = v
2020-02-27 19:21:49 +03:00
}
//i - actually readed lines and add (.) to data array
//crop logs to actually len
2020-05-20 02:31:08 +03:00
err = las.SetActuallyNumberPoints(i)
2020-04-10 00:55:08 +03:00
if err != nil {
return 0, err
}
2020-02-27 19:21:49 +03:00
return i, nil
}
// NumPoints - return actually number of points in data
2020-04-28 22:04:01 +03:00
func (las *Las) NumPoints() int {
return las.nPoints
2020-02-27 19:21:49 +03:00
}
2020-05-20 02:31:08 +03:00
// Dept - return slice of DEPT curve (first column)
2020-04-28 22:04:01 +03:00
func (las *Las) Dept() []float64 {
d, err := las.logByIndex(0)
2020-02-27 19:21:49 +03:00
if err != nil {
return nil
}
2020-05-20 02:31:08 +03:00
return d.D
2020-02-27 19:21:49 +03:00
}
2020-05-20 02:31:08 +03:00
// SetActuallyNumberPoints - crop all curve to numPoints count of data
func (las *Las) SetActuallyNumberPoints(numPoints int) error {
2020-02-27 19:21:49 +03:00
if numPoints <= 0 {
2020-04-28 22:04:01 +03:00
las.nPoints = 0
return errors.New("internal ERROR, func (las *Las) setActuallyNumberPoints(), actually number of points <= 0")
2020-02-27 19:21:49 +03:00
}
2020-04-28 22:04:01 +03:00
if numPoints > len(las.Dept()) {
las.nPoints = 0
return errors.New("internal ERROR, func (las *Las) setActuallyNumberPoints(), actually number of points > then exist data")
2020-02-27 19:21:49 +03:00
}
2020-04-28 22:04:01 +03:00
for _, l := range las.Logs {
2020-02-27 19:21:49 +03:00
l.SetLen(numPoints)
}
2020-04-28 22:04:01 +03:00
las.nPoints = numPoints
2020-02-27 19:21:49 +03:00
return nil
}
2020-04-10 00:55:08 +03:00
//Save - save to file
//rewrite if file exist
//if useMnemonic == true then on save using std mnemonic on ~Curve section
//TODO las have field filename of readed las file, after save filename must update or not? warning occure on write for what file?
2020-04-28 22:04:01 +03:00
func (las *Las) Save(fileName string, useMnemonic ...bool) error {
2020-04-10 00:55:08 +03:00
var (
err error
bufToSave []byte
)
if len(useMnemonic) > 0 {
2020-05-19 01:18:23 +03:00
bufToSave, err = las.SaveToBuf(useMnemonic[0]) //<> bufToSave, err = las.SaveToBuf(true) //TODO las.SaveToBuf(true) not test
2020-04-10 00:55:08 +03:00
} else {
2020-04-28 22:04:01 +03:00
bufToSave, err = las.SaveToBuf(false)
2020-04-10 00:55:08 +03:00
}
if err != nil {
return err
}
if !xlib.FileExists(fileName) {
err = os.MkdirAll(filepath.Dir(fileName), os.ModePerm)
if err != nil {
return errors.New("path: '" + filepath.Dir(fileName) + "' can't create >>" + err.Error())
}
}
err = ioutil.WriteFile(fileName, bufToSave, 0644)
if err != nil {
return errors.New("file: '" + fileName + "' can't open to write >>" + err.Error())
}
return nil
}
// SaveToBuf - save to file
// rewrite if file exist
// if useMnemonic == true then on save using std mnemonic on ~Curve section
// ir return err != nil then fatal error, returned slice is not full corrected
2020-04-28 22:04:01 +03:00
func (las *Las) SaveToBuf(useMnemonic bool) ([]byte, error) {
n := len(las.Logs) //log count
2020-04-10 00:55:08 +03:00
if n <= 0 {
return nil, errors.New("logs not exist")
}
var err error
var b bytes.Buffer
fmt.Fprint(&b, _LasFirstLine)
2020-05-19 01:18:23 +03:00
fmt.Fprintf(&b, _LasVersion, 2.0) //file is always saved in 2.0 format
2020-04-10 00:55:08 +03:00
fmt.Fprint(&b, _LasWrap)
fmt.Fprint(&b, _LasWellInfoSec)
2020-04-28 22:04:01 +03:00
fmt.Fprintf(&b, _LasStrt, las.Strt)
fmt.Fprintf(&b, _LasStop, las.Stop)
fmt.Fprintf(&b, _LasStep, las.Step)
fmt.Fprintf(&b, _LasNull, las.Null)
fmt.Fprintf(&b, _LasWell, las.Well)
2020-04-10 00:55:08 +03:00
fmt.Fprint(&b, _LasCurvSec)
fmt.Fprint(&b, _LasCurvDept)
var sb strings.Builder
sb.WriteString("# DEPT |") //готовим строчку с названиями каротажей глубина всегда присутствует
var l *LasCurve
for i := 1; i < n; i++ { //Пишем названия каротажей
2020-04-28 22:04:01 +03:00
l, _ := las.logByIndex(i)
2020-04-10 00:55:08 +03:00
if useMnemonic {
if len(l.Mnemonic) > 0 {
l.Name = l.Mnemonic
}
}
fmt.Fprintf(&b, _LasCurvLine, l.Name, l.Unit) //запись мнемоник в секции ~Curve
sb.WriteString(" ")
fmt.Fprintf(&sb, "%-8s|", l.Name) //Собираем строчку с названиями каротажей
}
fmt.Fprint(&b, _LasDataSec)
//write data
fmt.Fprintf(&b, "%s\n", sb.String())
2020-04-28 22:04:01 +03:00
dept, _ := las.logByIndex(0)
for i := 0; i < las.nPoints; i++ { //loop by dept (.)
2020-05-20 02:31:08 +03:00
fmt.Fprintf(&b, "%-9.3f ", dept.D[i])
2020-04-10 00:55:08 +03:00
for j := 1; j < n; j++ { //loop by logs
2020-04-28 22:04:01 +03:00
l, err = las.logByIndex(j)
2020-04-10 00:55:08 +03:00
if err != nil {
2020-04-28 22:04:01 +03:00
las.addWarning(TWarning{directOnWrite, lasSecData, i, "logByIndex() return error, log not found, panic"})
2020-04-10 00:55:08 +03:00
return nil, errors.New("logByIndex() return error, log not found, panic")
}
2020-05-20 02:31:08 +03:00
fmt.Fprintf(&b, "%-9.3f ", l.V[i])
2020-04-10 00:55:08 +03:00
}
fmt.Fprintln(&b)
}
2020-04-28 22:04:01 +03:00
r, _ := cpd.NewReaderTo(io.Reader(&b), las.oCodepage.String()) //ошибку не обрабатываем, допустимость oCodepage проверяем раньше, других причин нет
2020-04-10 00:55:08 +03:00
bufToSave, _ := ioutil.ReadAll(r)
return bufToSave, nil
}
2020-04-28 22:04:01 +03:00
// IsEmpty - test to not initialize object
func (las *Las) IsEmpty() bool {
return (las.Logs == nil)
}
2020-05-10 04:20:51 +03:00
// GetStrtFromData - return strt from data section
// read 1 line from section ~A and determine strt
// close file
// return Null if error occurs
func (las *Las) GetStrtFromData() float64 {
iFile, err := os.Open(las.FileName)
if err != nil {
return las.Null
}
defer iFile.Close()
_, iScanner, err := xlib.SeekFileStop(las.FileName, "~A")
if (err != nil) || (iScanner == nil) {
return las.Null
}
s := ""
dept1 := 0.0
for i := 0; iScanner.Scan(); i++ {
s = strings.TrimSpace(iScanner.Text())
if (len(s) == 0) || (s[0] == '#') {
continue
}
k := strings.IndexRune(s, ' ')
if k < 0 {
k = len(s)
}
dept1, err = strconv.ParseFloat(s[:k], 64)
if err != nil {
return las.Null
}
return dept1
}
//если мы попали сюда, то всё грусно, в файле после ~A не нашлось двух строчек с данными... или пустые строчки или комменты
// TODO последняя строка "return las.Null" не обрабатывается в тесте
return las.Null
}
2020-06-02 20:49:41 +03:00
// GetStepFromData - return step from data section
// read 2 line from section ~A and determine step
// close file
// return Null if error occure
func (las *Las) GetStepFromData() float64 {
iFile, err := os.Open(las.FileName)
2020-05-10 04:20:51 +03:00
if err != nil {
2020-06-02 20:49:41 +03:00
return las.Null
2020-05-10 04:20:51 +03:00
}
2020-06-02 20:49:41 +03:00
defer iFile.Close()
2020-05-10 04:20:51 +03:00
2020-06-02 20:49:41 +03:00
_, iScanner, err := xlib.SeekFileStop(las.FileName, "~A")
if (err != nil) || (iScanner == nil) {
return las.Null
2020-05-10 04:20:51 +03:00
}
2020-06-02 20:49:41 +03:00
s := ""
j := 0
dept1 := 0.0
dept2 := 0.0
for i := 0; iScanner.Scan(); i++ {
s = strings.TrimSpace(iScanner.Text())
if (len(s) == 0) || (s[0] == '#') {
continue
}
k := strings.IndexRune(s, ' ')
if k < 0 {
k = len(s)
2020-05-10 04:20:51 +03:00
}
2020-06-02 20:49:41 +03:00
dept1, err = strconv.ParseFloat(s[:k], 64)
if err != nil {
2020-06-10 20:05:16 +03:00
// case if the data row in the first position (dept place) contains not a number
2020-06-02 20:49:41 +03:00
return las.Null
}
j++
if j == 2 {
2020-06-10 20:05:16 +03:00
// good case, found two points and determined the step
2020-06-02 20:49:41 +03:00
return math.Round((dept1-dept2)*10) / 10
}
dept2 = dept1
2020-05-10 04:20:51 +03:00
}
2020-06-10 20:05:16 +03:00
//bad case, data section not contain two rows with depth
//TODO последняя строка "return las.Null" не обрабатывается в тесте
2020-06-02 20:49:41 +03:00
return las.Null
2020-05-10 04:20:51 +03:00
}
2020-06-02 20:49:41 +03:00
func (las *Las) setStep(h float64) {
las.Step = h
2020-05-10 04:20:51 +03:00
}
func (las *Las) setStrt(h float64) {
las.Strt = h
}
2020-06-02 20:49:41 +03:00
// IsStrtEmpty - return true if parameter Strt not exist in file
func (las *Las) IsStrtEmpty() bool {
return las.Strt == StdNull
2020-05-10 04:20:51 +03:00
}
2020-06-02 20:49:41 +03:00
// IsStopEmpty - return true if parameter Stop not exist in file
func (las *Las) IsStopEmpty() bool {
return las.Stop == StdNull
}
2020-05-10 04:20:51 +03:00
2020-06-02 20:49:41 +03:00
// IsStepEmpty - return true if parameter Step not exist in file
func (las *Las) IsStepEmpty() bool {
return las.Step == StdNull
}
2020-05-10 04:20:51 +03:00
2020-06-02 20:49:41 +03:00
// SetNull - change parameter NULL in WELL INFO section and in all logs
func (las *Las) SetNull(aNull float64) error {
for _, l := range las.Logs { //loop by logs
for i := range l.V { //loop by dept step
if l.V[i] == las.Null {
l.V[i] = aNull
2020-05-10 04:20:51 +03:00
}
}
}
2020-06-02 20:49:41 +03:00
las.Null = aNull
return nil
}
2020-05-10 04:20:51 +03:00
2020-06-02 20:49:41 +03:00
//logByIndex - return log from map by Index
func (las *Las) logByIndex(i int) (*LasCurve, error) {
for _, v := range las.Logs {
if v.Index == i {
return &v, nil
2020-05-10 04:20:51 +03:00
}
}
2020-06-02 20:49:41 +03:00
return nil, fmt.Errorf("log with index: %v not present", i)
2020-05-10 04:20:51 +03:00
}