반응형
블로그 이미지
개발자로서 현장에서 일하면서 새로 접하는 기술들이나 알게된 정보 등을 정리하기 위한 블로그입니다. 운 좋게 미국에서 큰 회사들의 프로젝트에서 컬설턴트로 일하고 있어서 새로운 기술들을 접할 기회가 많이 있습니다. 미국의 IT 프로젝트에서 사용되는 툴들에 대해 많은 분들과 정보를 공유하고 싶습니다.
솔웅

최근에 올라온 글

최근에 달린 댓글

최근에 받은 트랙백

글 보관함

카테고리

코로나에서 time, date 다루기

2013. 1. 18. 06:49 | Posted by 솔웅


반응형
Posted on . Written by

“Time keeps on slippin’, slippin’, slippin’, into the future.” —Steve Miller Band / Fly Like An Eagle


어떤 프로그래밍 언어로 time 관련해서 작업을 하는 것은 프로그래머에게 혼란을 주게 됩니다. 이번주의 튜토리얼은 time 과 date 에 대해 다룹니다. 그리고 date 계산, time zone 문제 그리고 date formatting 관련해서 예제를 보여드리겠습니다.


코드를 보기전에 먼저 이해해 둬야할 몇가지 중요한 개념들이 있습니다. 컴퓨터에게는 time 의 기본 유닛은 1초 입니다. 이 값으로 우리는 date 도 계산할 수 있고 fraction 들도 다루고 심지어 time 의 더 작은 순간들도 계산해 냅니다. 사람에게 1초는 짧은 시간이지만 컴퓨터에게는 아주 긴 시간입니다. 사람들도 긴 시간의 경우 세분화해서 사용하듯이 컴퓨터도 마찬가지 입니다. 어떤 함수는 1/1000 초(milliseconds ) 단위로 작업을 하고 이것을 uSec 나 uSeconds으로 쓰기도 합니다. 더 좋은 컴퓨터는 이것보다 더 짧은 시간을 다루기도 합니다. 코로나에서는 3가지 종류의 time measurement 를 사용합니다.




Microseconds and Milliseconds


Microseconds 는 아래와 같이 call 하시면 됩니다 :


system.getTimer()


이 call은 여러분의 app 이 시작한 이후의 시간을 return 해 줍니다. 그 값은 milliseconds (1/1000ths) 단위 입니다. 대부분의 device들에서는 아래와 같은 값을 return 할 겁니다.


1839.3949


이 time은 microsecond resolution 으로 측정되는데요 여러분에게 보일 때는 milliseconds 로 변환해서 보여드리는 겁니다. 그리고 소숫점 아래는 microseconds 를 나타내죠. 이 마이크로세컨드 단위가 필요한 경우가 있을지 궁금해 하실수도 있습니다. 아마 실제로는 필요가 없겠죠. 아마 GPS 데이터를 다루거나 Einstein’s Pedometer (앱 이름인데요. 여러분이 빠르게 움직이면 얼마나 젊어지는지를 보여주는 앱입니다.) 작업을 하게 되면 이 microsecond resolution 이 필요할 수도 있습니다. 하지만 우리의 앱에서는 1초당 30~60 프레임을 사용할 거니까 1/30 에서 1/60 초 단위로 작업을 하게 될 겁니다. 그러니까 1/1000 초 보다 더 정교한 시간계산은 이 앱에서는 그다지 중요하지는 않습니다.


milliseconds와 관련해서 사용하는 함수들은 아래와 같습니다.


이 함수들은 millisecond 단위를 accept 합니다. 그러니까 이 함수에 값을 5000을 주면 그 시간은 5초가 되는 겁니다. 1/2 초는 500이 되겠죠. 2와 1/2 초는 2500이 됩니다.


timer.performWithDelay(500, someFunction)  -- a half second delay


초를 원하는 값으로 바꾸시려면 1000을 곱하면 되겠죠.


Dealing With Longer Periods of Time


위에서 언급했듯이 컴퓨터에서 time 의 기본적인 유닛은 1초입니다. iOS와 안드로이드 (Mac OS X 도 마찬가지로) 모두 Unix 에서 파생된 운영체제에 근거해 있습니다. 그리고 그런 운영체제에서 표준 "time" 함수는 1970년 1월 1일 자정 이후부터 경과된 시간을 초단위로 return 해 줍니다.  마이크로소프트는 좀 다른데요, 하지만 이미 많은 앱들이 C언어를 사용해서 만들어졌고 이 언어는 Unix 에 기반해 있거든요. 그리고 그 언어의 라이브러리도 이 time reference pointEpoch를 사용하구요.


마이너스일 경우에는 1970년 이전이 되겠죠. positive times 일 경우에는 1970년 이후가 되겠구요. 코로나에서는 os.time() API call을 사용해서 1970년 1월 1일 이후 지금까지의 number of seconds 를 얻을 수 있습니다.


print( os.time() )  -- outputs something like: 1358015442


사실 저 숫자만 나온다면 알아볼수가 없으니까 별로 의미가 없겠죠. 이것을 date math로 계산해서 실제 생활에서 쓰이는 날짜와 시간으로 바꿔야 알아볼 수가 있을 겁니다. 그런데 게임을 한번 생각해 보세요. turn-based game 이 있는데 유저가 너무 오랫동안 게임을 하고 있어서 어떤 "nudge" 가 필요한 경우 아래와 같이 마지막 move의 시간을 저장할 필요가 있을 겁니다.


player[1].lastMove = os.time()

그리고 나서 지난 한시간 동안 어떤 move 가 있는지 체크하고 싶으면 아래와 같이 하시면 됩니다.


local now = os.time()
if ( now > player[1].lastMove + 3600 ) then
  -- nudge the player
end


3600은 어디서 왔을까요? 60초 곱하기 60분을 하면 그 값입니다. 즉 1시간이죠. 그러니까 아래와 같이 하셔도 됩니다.


local now = os.time()
if ( now > player[1].lastMove + (60 * 60) ) then
  -- nudge the player
end

Lua 의 pre-processor 는 (60*60) 를 3600으로 convert 할겁니다. 그러면 여러분이 따로 미리 계산할 필요는 없겠죠. 아래와 같이 미리 날짜, 시간, 분을 상수로 정의해서 사용하셔도 됩니다.


DAY = 24 * 60 * 60
HOUR = 60 * 60
MINUTE = 60

local now = os.time()
if ( now > player[1].lastMove + HOUR ) then
  -- nudge the player
end


이렇게 초단위로 다룬 값으로 date math 를 사용하는 것은 아주 쉽습니다.


  • Is your time older than a week? Use ( 7 * 24 * 60 * 60 ).
  • Set an alarm in 30 days? Use ( 30 * 24 * 60 * 60 ).


또한 한달에 몇일이 있는지, 윤년인지 아닌지 등등도 계산할 것을 걱정하지 않으셔도 욉니다.

Working With Dates


Dates(날짜)와 관련해서는 조금 신경을 쓰셔야 됩니다. 왜냐하면 날짜를 표시할 때는 strings 를 사용하니까요.


  • April 1, 2010 4:53pm
  • April 1, 2010 4:53 P.M.
  • 4/1/10 16:53 MT
  • Sun Jan 13 15:17:32 EST 2013
  • 13-JAN-13 15:17


개발자로서 날짜를 component values로 parse 할 필요가 있습니다. date/time string의 각 파트별로 get 하셔야 합니다. (month, day, year, hour, minute, seconds and time zone). 만약에 month를 string 으로 가지고 있다면 이 string 을 number 로 그리고 timestamp로 convert 할 lookup table 을 사용하실 수 있습니다.


local monthNumbers = {}
monthNumbers["January"] = 1
monthNumbers["February"] = 2
monthNumbers["March"] = 3
--etc...
monthAsInteger = monthNumbers[monthString]


가장 일반적인 date format 중의 하나는 ISO-8601 time format입니다. 그 string 값은 대충 아래와 같습니다.


2012-01-12T12:04:35.03-0400


이 date 를 parse 하시려면 이 여러 bit들을  패턴을 사용한 각각의 값으로 break down 하기 위해 string:match string function 을 사용할 겁니다.


local pattern = "(%d+)%-(%d+)%-(%d+)%a(%d+)%:(%d+)%:([%d%.]+)([Z%p])(%d%d)%:?(%d%d)"
local year, month, day, hour, minute, seconds, tzoffset, offsethour, offsetmin =
  dateString:match( pattern )

Lua 를 처음 접하시는 분들 에게는 위 코드가 많이 복잡하게 보일 겁니다. 신택스가 그렇게 간단하지는 않죠? 이해하기 쉽도록 break down 해 보죠.


2012-01-12   T   12:04:35.03   -0400


T 이전 부분은 date 입니다. 알아보기 쉽고 또 나누기 쉽죠.  연,월,일이 숫자로 있고 각각 hyphen 으로 나눠져 있습니다. T는 시간 부분이 시작한다는 의미입니다. 그 다음에는 시,분,초를 가리키는 숫자들이 나오고 각각 colon들로 나눠져 있죠. 이 중 초단위는 floating point number 라는 것을 기억해 두세요. 그 다음부분에는 옵션으로 time zone이 나옵니다. 이 timezone 과 관련해서는 조금 더 얘기를 나눌 겁니다.


string:match() function은 어떤 패턴을 보여주는데요. string 의 span 에 대해 “wildcard” type searching 을 사용하는 그런 패턴이죠. 위의 경우에는 %d+ pattern을 상요해서 숫자를 찾고 있습니다. 이렇게 함으로서 Lua 에게 0에서 9까지의 숫자열을 찾도록 만들죠. + 표시는 한개 이상의 숫자열을 찾는 다는 것을 알려 주는 겁니다. 예를 들어 2012 같은 숫자열을요.


이 값들을 알아볼 수 있는 변수에 저장하기 위해 괄호 () 안에 어떤 pattern set들을 넣게 됩니다. 그러면 Lua에게 우리가 retrieve 하기 원하는 date/time 의 각각의 단위들을 알려줄 수가 있습니다. date 부분은 hyphen 들이 있는데요. 이것은 각 컴포넌트를 분리하는 기호입니다.


(%d+)%-(%d+)%-(%d+) -- gets the year, month and day
%a -- handles the single letter T
(%d+)%:(%d+)%:([%d%.]+) -- gets the hour, minutes and seconds



마지막 부분을 보세요. 숫자하고 점(.) 이 있죠? 초가 floating point value 이기 때문에 이렇게 찾는 겁니다. string matching에서 점(.) 은 “magic character” 입니다. 이 점 앞에 % 를 넣으셔야 Lua 는 실제 점(.) 을 찾을 겁니다.


마지막으로 + 나 - 구분자 다음에 timezone 정보가 옵니다. 혹은 Z 나 UTC 를 사용하기도 하죠. string:match function 은 각각의 match 되는 패턴들에 대한 값들을 return 할 겁니다. 그리고 그 값들을 변수에 담게 되죠. ISO-8601 standard는 timezone time 을 +/-HHMM+/-HH:MM를 사용합니다. 만약 이 timezone 정보가 없으면 여러분이 속해 있는 local timezone 을 사용할 겁니다. 아래의 패턴은 좀 복잡해 보이긴 하는데요. timezone 정보를 처리하는 코드 입니다.


([Z%p])(%d%d)%:?(%d%d)


Z 나 punctuation character 인 + 나 - 다음에 %p가 옵니다. 그 다음에는 두자리 숫자가 와야 됩니다. 그 다음에 colon 은 옵션인데요. 두개의 숫자가 더 올 수도 있습니다. 이 패턴은 tzoffset, offsethour, offsetmin등을 return 합니다. 이것을 date math 에서 사용할 숫자로 convert 하기 위해 os.time() function (documentation) 을 사용합니다.


local timestamp = os.time(
  {year=year, month=month, day=day, hour=hour, min=minute, sec=seconds} )



이제 1970년 1월 1일 이후 몇초가 경과했는지에 대한 값을 갖게 됐습니다. 그리고 이것을 date math 로 이용할 수 있게 됐구요. 한가지 문제가 있는데요. 아직까지 timezone 부분에 대해 일을 마치지는 않았습니다. 다행히 그 작업은 어렵지 않은데요. 지금 이미 기본 시간을 seconds 로 갖고 있으니까 계산만 하면 되곘죠. Z는 time 이 Coordinated Universal Time (UTC)라는 것을 가리키는 겁니다. 만약 이것이 UTC 나 local time 이라면 따로 timezone 과 관련해서 작업할 것은 없습니다.


local offset = 0
if ( tzoffset ) then
  if ( tzoffset == "+" or tzoffset == "-" ) then  -- we have a timezone!
    offset = offsethour * 60 + offsetmin
    if ( tzoffset == "-" ) then
      offset = offset * -1
    end
    timestamp = timestamp + offset
  end
end


os.time()은 여러분이 있는 곳의 timezone 을 사용해서 값을 return 할 겁니다.


아래 코드는 여러분이 라이브러리로 사용할 수 있는 샘플 예제 입니다.


function makeTimeStamp(dateString)
  local pattern = "(%d+)%-(%d+)%-(%d+)%a(%d+)%:(%d+)%:([%d%.]+)([Z%p])(%d%d)%:?(%d%d)"
  local year, month, day, hour, minute, seconds, tzoffset, offsethour, offsetmin =
    dateString:match(pattern)
  local timestamp = os.time(
    {year=year, month=month, day=day, hour=hour, min=minute, sec=seconds} )
  local offset = 0
  if ( tzoffset ) then
    if ( tzoffset == "+" or tzoffset == "-" ) then  -- we have a timezone!
      offset = offsethour * 60 + offsetmin
      if ( tzoffset == "-" ) then
        offset = offset * -1
      end
      timestamp = timestamp + offset
    end
  end
  return timestamp
end


Final note:  여기서는 아직 Daylight Savings Time(써머타임)을 적용하지는 않은 상황입니다. Daylight Savings Time 은 이 튜토리얼에서 다루지 않습니다.


Converting a Timestamp to Something Readable


어떤 경우에는 이 time 을 유저가 알아볼 수 있는 date string으로 표현할 필요가 있을 겁니다. 이게 바로 os.date() function의 목적이죠. (documentation).


os.date() function 은 두개의 파라미터를 받습니다. 둘 다 옵션인데요. 이 옵션 없이 call 하면 현재 있는 지역의 timezone 에 맞는 date/time 을 return 할 겁니다. 아마 아래와 같은 형식일 겁니다.


Sat Jan 12 14:07:30 2013


아주 다양한 형식으로 표현할수 있는데요. 다양한 formatting parameter 들을 이용하시면 됩니다. 이 format parameter들은 Unix/C++ library function strftime에 근거합니다.


print( os.date("%A") )  -- prints out a string representing the weekday.
print( os.date("%A %l:%M%p") )  -- prints out "Saturday 2:30PM"

디폴트로 os.date()가 return 하는 값은 여러분의 지역이나 time zone 에 근거할 겁니다.  RSS feeds, Corona Cloud date 등에서 일반적으로 사용하는 위에서 사용한 날짜 형식을 생성하기 원하신다면 UTC 에 근거한 output 을 받아야 합니다. 이것은 format parameter 앞에 ! 를 붙이시면 됩니다.


print( os.date("%FT%X%z") )  -- outputs: 2013-01-12T15:30:09-0500

or if you wanted it in UTC:

print( os.date("!%FT%XZ") )  -- outputs: 2013-01-12T19:30:09Z



strftime에 있는 다양한 formatting parameter 들은 다양한 format 으로 month, day, year, weekday, am/pm를 얻을 수 있도록 합니다. 



Counting Down Time


흔히 하는 질문으로 "어떻게 countdown timer 를 만들수 있죠?" 가 있습니다.

가장 좋은 방법은 직접 한번 구현해 보는 걸겁니다. 만약에 60안에 한 레벨을 끝내기를 원한다면 60초 후에 뭔가 이벤트를 발생시키기 위해 timer.performWithDelay()를 사용하시면 됩니다. 보기 좋은 UI output 과 함께 좀 더 그럴듯한 방법을 원하신다면 enterFrame event 에 Runtime listener를 사용하실 수도 있습니다. 그렇게 하면 시간을 체크할 수 있습니다. 혹은 1초 timer 를 계속 반복할 수도 있겠죠. 그럼 필요할 때 쉽게 pause 시키고 또 resume 시킬 수 있을 겁니다.


-------- RUNTIME METHOD --------
local startTime = os.time()
local levelTime = 60
local displayTime = display.newText(levelTime, 0, 0, "Helvetica", 20)

local function checkTime(event)
  local now = os.time()
  displayTime.text = levelTime - (now - startTime)
  if ( now > startTime + levelTime ) then
    -- code to end the level
  end
end
Runtime:addEventListener("enterFrame", checkTime)
-------- REPEATING TIMER METHOD --------
local levelTime = 60
local displayTime = display.newText( levelTime, 0, 0, "Helvetica", 20 )

local function checkTime(event)
  levelTime = levelTime - 1
  displayTime.text = levelTime
  if ( levelTime <= 0 ) then
    -- code to end the level
  end
end
timer.performWithDelay( 1000, checkTime, levelTime )



In Summary


Corona에서 time 과 date를 다루는 것은 약간 복잡한 신택스를 다뤄야 합니다. 이 튜토리얼이 그것을 이해하는데 도움이 되셨으면 합니다. 위의 예제들을 활용해서 앱 개발시 이용하시면 훨씬 수월하실 겁니다.


반응형


반응형

어제 이곳 로드 아일랜드 뉴스에는 동성결혼에 대한 얘기가 한창 이슈였습니다.

올해 이곳에서 동성결혼을 허용하는 법안을 만들 것인가 여부가 주의회에서 논의가 시작됐거든요.


이곳에 오기 전에 있었던 뉴저지도 동성결혼 허옹문제가 의회에서 논의되고 공화당 소속 주지사인 크리스티는 이 법안이 통과 되면 거부권을 행사하겠다고 발표하기도 하면서 좀 시끄러웠는데.. 여기도 그러네요.


이곳 New England 지역의 6개 주인 메인, 뉴햄프셔, 버몬트, 메사추세츠, 로드 아일랜드, 코네티넛 중 메인, 코네티컷, 메사추세츠, 뉴 햄프셔, 버몬트는 이미 이 동성 결혼이 법적으로 허락되 있고 로드 아일랜드만 관련 법안이 없는 상황입니다.


하지만 로드 아일랜드도 다른 법에 의해서 실제로 동성 결혼은 이뤄지고 있다고 하는데요. 이번에 제대로 동성 결혼 허용하는 법안에 대해 논의 한다고 합니다.


뉴스에서는 same sex marriage 라고 하는데 이 Providence Journal 에서는 gay marriage 라고 하네요.


기사의 뉘앙스로 봐서는 약간 동성결혼에 대해 긍정적이지는 않은 신문사인것 같습니다.


Nearly 200 opponents of gay marriage rally at R.I. State House


January 15, 2013 5:06 pm
By Philip Marcelo



Providence Journal photo / Philip Marcelo


R.I. Sen. Harold Metts of Providence speaks at the rally in the rotunda Tuesday afternoon.



PROVIDENCE, R.I. -- Opponents of gay marriage are rallying in the State House rotunda Tuesday afternoon, as state lawmakers convene the first hearing on this year's bill to allow gay couples to marry in Rhode Island.


게이 결혼에 반대하는 시위가 주정부 건물인(도청) rotunda 에서 화요일 오후에 있었다. 이날 주 의원들은 Rhode Island 에서 올해 게이 결혼 (동성결혼)을 허락할지에 대한 법안 심사를 하는 첫 날이었다.


With nearly 200 in attendance, church leaders announced a new coalition that is meant to serve as a counterpoint to "Rhode Islanders United for Marriage," which was announced Monday by gay marriage supporters.


200여명이 참가하였고 교회 지도자들은 월요일 게이 결혼(동성결혼) 지지자들이 발표한 "Rhode Islanders United for Marriage 에 대항한 새로운 연대를 발표했다.


The group -- "Faith Alliance to Preserve the Sanctity of Marriage as Established by God" -- includes representatives from the Catholic Church, the Knights of Columbus, the National Organization for Marriage and a number of Hispanic and Latino church groups.

Organizers said the alliance's purpose is to "safeguard God's covenant and definition of marriage." It does not seek to judge or condemn gay, lesbian, or transgender individuals.

More on R.I's gay marriage debate


"신에 의해 만들어진대로 결혼의 신성함을 보존하기 위한 믿음 연합" 이라는 이 단체는 카톨릭, Knights of Columbus, National Organization for Marriage, 그리고 일부 중남미계 교회 그룹으로 구성돼 있다. 이 단체 설립자들은 이 연합의 목표는 신과의 약속과 결혼에 대한 정의를 지키는 것이라고 말했다. 게이, 레즈비언 혹은 트랜스잰더들을 개별적으로 심판하기 위한 단체는 아니다.

More on R.I's gay marriage debate






반응형


반응형

이번에는 getTextUsingTransformations() 함수를 살펴볼 차례죠?


제 생각에 오늘로 소스 분석이 모두 끝날 것 같습니다.

사실 제가 할 작업은 지금까지 분석한 것으로도 충분하거든요.

벌써 미팅에서 결과를 공유했고 그 다음 단계가 진행중입니다.


하지만 시작했으니 마저 끝마쳐보죠.


pdf2text() 함수의 맨 마지막 return 문에 있는 코드 입니다.


return getTextUsingTransformations($texts, $transformations);


파라미터로는 getDirtyTexts() 함수에서 얻었던 $texts 가 첫번째에 있네요.

이 내용은 지난 글 말미에 보여드렸습니다.


두번째 파라미터는 getDecodedStream() 함수를 통해서 얻었던 $data 입니다.


getTextUsingTransformations($texts, $transformations) 함수를 볼까요?


function getTextUsingTransformations($texts, $transformations) {
    $document = "";
    for ($i = 0; $i < count($texts); $i++) {
        $isHex = false;
        $isPlain = false;

        $hex = "";
        $plain = "";
        for ($j = 0; $j < strlen($texts[$i]); $j++) {
            $c = $texts[$i][$j];
            switch($c) {
                case "<":
                    $hex = "";
                    $isHex = true;
                break;
                case ">":
                    $hexs = str_split($hex, 4);
                    for ($k = 0; $k < count($hexs); $k++) {
                        $chex = str_pad($hexs[$k], 4, "0");
                        if (isset($transformations[$chex]))
                            $chex = $transformations[$chex];
                        $document .= html_entity_decode("&#x".$chex.";");
                    }
                    $isHex = false;
                break;
                case "(":
                    $plain = "";
                    $isPlain = true;
                break;
                case ")":
                    $document .= $plain;
                    $isPlain = false;
                break;
                case "\\":
                    $c2 = $texts[$i][$j + 1];
                    if (in_array($c2, array("\\", "(", ")"))) $plain .= $c2;
                    elseif ($c2 == "n") $plain .= '\n';
                    elseif ($c2 == "r") $plain .= '\r';
                    elseif ($c2 == "t") $plain .= '\t';
                    elseif ($c2 == "b") $plain .= '\b';
                    elseif ($c2 == "f") $plain .= '\f';
                    elseif ($c2 >= '0' && $c2 <= '9') {
                        $oct = preg_replace("#[^0-9]#", "", substr($texts[$i], $j + 1, 3));
                        $j += strlen($oct) - 1;
                        $plain .= html_entity_decode("&#".octdec($oct).";");
                    }
                    $j++;
                break;

                default:
                    if ($isHex)
                        $hex .= $c;
                    if ($isPlain)
                        $plain .= $c;
                break;
            }
        }
        $document .= "\n";
    }

    return $document;
}


먼저 $document 변수를 만들어 놓네요. 맨 밑에 보니까 return 될 값이 들어갈 변수입니다.

즉 사람이 읽을 수 있는 완전한 데이터가 들어갈 변수죠.


다음은 $texts 의 count 만큼 for 문을 돌립니다.

그 for문 안에서 맨 먼저 하는 일은 4개의 변수를 정의해 놓는 거네요.

$isHex와 $isPlain 는 boolean 값이 들어갈 변수고 $hex와 $plain 은 어떤 문자 같은게 들어갈 변수인것 같습니다.


여기서 다시 for 문을 돌립니다. $texts 가 이중배열이라서 이렇게 작업하나 봅니다.

두번째 for 문에서는 $c 변수에 $texts[$i][$j] 를 담습니다.


그 다음에 switch 문이 나오는데... 처음에 < 와 > 를 체크하는 군요.


편의를 위해서 어제 봤던 데이터 중 일부를 아래 복사해 넣겠습니다.


(W)-36(e)7(e)7(k )46(o)7(f)
( )
(Oct)-11(o)5(ber )10(1)4( )
<00B2>
( )
(Oct)-11(o)5(ber )10(5)
( )
( )
( )
(Who)5(l)7(e)7( )-19(+ )-2(S)3(um)5( )
( )
( )
(Choice)
( )
(+)
( )
(F)20(lavor)


<00B2> 가 있네요. < 인 경우에는 $isHex 가 true 이고 > 인 경우에는 false 입니다.

그리고 > 인 경우에는 $hex 값을 array 로 바꿉니다.

str_split() 함수에서 그 일을 하죠.


<?php

$str 
"Hello Friend";

$arr1 str_split($str);
$arr2 str_split($str3);

print_r($arr1);
print_r($arr2);

?>


위 소스를 돌리면 아래 값을 얻습니다.

Array
(
    [0] => H
    [1] => e
    [2] => l
    [3] => l
    [4] => o
    [5] =>
    [6] => F
    [7] => r
    [8] => i
    [9] => e
    [10] => n
    [11] => d
)

Array
(
    [0] => Hel
    [1] => lo
    [2] => Fri
    [3] => end
)


어떤 일을 하는 함수인지 아시겠죠?

그 다음에는 $hexs 의 count 만큼  for 문을 돌립니다.

그 다음 그 각각의 값을 지난번에도 나왔던 str_pad() 함수를 사용해서 0을 4칸 붙이네요.


그 다음에 if 문에서 $transformations[$chex] 이 세팅돼 있으면 $chex 변수에 $transformations[$chex]을 대입합니다. 


그리고 나서 $document 에 값을 집어 넣는데요.

html_entity_decode() 함수를 사용합니다.


이 것은 html 을 string 으로 바꿔 주는 함수입니다.


<?php
$orig 
"I'll \"walk\" the <b>dog</b> now";

$a htmlentities($orig);

$b html_entity_decode($a);

echo 
$a// I'll &quot;walk&quot; the &lt;b&gt;dog&lt;/b&gt; now

echo $b// I'll "walk" the <b>dog</b> now
?>


위 예제를 보시면 어떤 일을 하는지 아시겠죠?


그러니까 <> 감싸여진 데이터는 hex 코드라서 이 case 문에서 그것을 처리하는 거네요.


그 다음 case 문에서는 (,) 를 체크합니다.


() 로 둘러싸인 부분은 그냥 text 죠. 사람이 읽을 수 있는..

그러니까 별다른 처리를 하지 않아도 되기 때문에 ) 인 경우에 $document 에 $plain 을 그냥 추가해 버리는 겁니다.


다음에는 \\ 를 체크하는데요.


우리가 다루는 pdf 파일에서는 이 값이 추출 되지 않았습니다.

어쨌든 내용을 보면 줄바꿈, 탭 뭐 이런 것들을 해당 sign 으로 바꿔서 $plain 에 넣는 일을 하네요. 숫자인 경우에는 거기에 맞게 또 처리를 하구요.


디폴트로는 $isHex 일 경우 $hex 에 $c를 추가하고 $isPlain 일 경우 $plain 에 $c를 추가합니다.


그리고 이렇게 만든 $document 를 이전 for 문에서 만든 $document 에 가를 하구요.

이렇게 for 문이 다 돌고 $document 에 값이 다 쌓였으면 이 값을 return 합니다.


그 return 값이 사람이 볼 수 있는 text 입니다.


이렇게 해서 얻은 결과는 아래와 같습니다.



잘 안 보이실 텐데요. 1번 글에서 업로드한 파일들을 다운 받아서 돌려 보시면 됩니다.


참고로 이 데이터를 가지고 요일별 메뉴를 display 하는 함수를 제가 만들어 봤는데요.



function menus($sources){
$resultlen  = strlen($sources);

$menuDate = substr($sources,0,48);
echo "<b><font size=6>Start Menu</font> <p><br> ". $menuDate. "<p></b>";


$result = preg_replace("/\s+/",'_',$sources);

$fs1 = strrpos($result,'_M_');

$menu1 = substr($result,$fs1,$resultlen);

$startTue = strpos($menu1,'_T_');
$startWed = strpos($menu1,'_W_');
$startThu = strrpos($menu1,'_T_');
$startFri = strpos($menu1,'_F_');
$endFri = strpos($menu1,'WEEKLY');

$Monday = str_replace('_', ' ' ,str_replace('_M_','Monday <br>',str_replace('FIT','FIT<br>',substr($menu1,0,$startTue))));
$Tuesday = str_replace('_', ' ' ,str_replace('_T_','Tuesday <br>',str_replace('FIT','FIT<br>',substr($menu1,$startTue,$startWed-$startTue))));
$Wednsday = str_replace('_', ' ' ,str_replace('_W_','Wednsday <br>',str_replace('FIT','FIT<br>',substr($menu1,$startWed,$startThu-$startWed))));
$Thuesday = str_replace('_', ' ' ,str_replace('_T_','Thusday <br>',str_replace('FIT','FIT<br>',substr($menu1,$startThu,$startFri-$startThu))));
$Friday = str_replace('_', ' ' ,str_replace('_F_','Friday <br>',str_replace('FIT','FIT<br>',substr($menu1,$startFri))));

echo "<table width=50%><tr><td>";

echo "<p> ". $Monday . "<p>";
echo "<p> ". $Tuesday . "<p>";
echo "<p> ". $Wednsday . "<p>";
echo "<p> ". $Thuesday . "<p>";
echo "<p> ". $Friday . "<p>";

echo "</td></tr></table><p><br><p> ";
}


그리고 함수 밖에서 이걸 부르면 되죠.

menus($result);

이렇게 하면 아래와 같은 결과가 나옵니다.




이렇게 해서 PDF 를 TEXT 로 변환하는 PHP 프로그램을 모두 분석해 봤습니다.

오랫만에 목욕해서 때를 싹 밀었을 때 처럼 개운하네요.


분석결과 위 소스는 제가 일하는데에서는 맞지 않아서 사용하지 않기로 했거든요.

그래서 저 결과도 깔끔하게 나온 것은 아닙니다.

참고하시구요.


다음에 또 소스 분석할 일 있으면 블로그에 정리해 놓을 께요.

이번 글은 기분 좋게 7번째 만에 마무리 했네요.





반응형


반응형

그럼 지난 글에 이어서 계속 분석해 보겠습니다.


이제 pdf2text 함수에서 getCharTransformations 함수 call 하는 부분을 볼 차례입니다.

지난번에 preg_match_all 을 사용해서 BT,ET 로 구분해서 데이터를 따로 모으고 그 데이터를 압축해제 해서 그나마 사람이 조금 알아 볼 수 있는 데이터로까지 만들었었는데요.


이 BT,ET  구분에 해당하지 않는 else 부분에서 getCharTransformations 함수를 호출합니다.


전달하는 인자는 첫번째 인자로 pdf2text 초반에 만들어 뒀던 $transformations 배열 변수하고 getDecodedStream 함수를 통해서 얻었던 $data 변수입니다.





function getCharTransformations(&$transformations, $stream) {
    preg_match_all("#([0-9]+)\s+beginbfchar(.*)endbfchar#ismU", $stream, $chars, PREG_SET_ORDER);
    preg_match_all("#([0-9]+)\s+beginbfrange(.*)endbfrange#ismU", $stream, $ranges, PREG_SET_ORDER);

    for ($j = 0; $j < count($chars); $j++) {
        $count = $chars[$j][1];
        $current = explode("\n", trim($chars[$j][2]));

        for ($k = 0; $k < $count && $k < count($current); $k++) {
            if (preg_match("#<([0-9a-f]{2,4})>\s+<([0-9a-f]{4,512})>#is", trim($current[$k]), $map))
                $transformations[str_pad($map[1], 4, "0")] = $map[2];
        }
    }
    for ($j = 0; $j < count($ranges); $j++) {
        $count = $ranges[$j][1];
        $current = explode("\n", trim($ranges[$j][2]));
        for ($k = 0; $k < $count && $k < count($current); $k++) {
            if (preg_match("#<([0-9a-f]{4})>\s+<([0-9a-f]{4})>\s+<([0-9a-f]{4})>#is", trim($current[$k]), $map)) {
                $from = hexdec($map[1]);
                $to = hexdec($map[2]);
                $_from = hexdec($map[3]);

                for ($m = $from, $n = 0; $m <= $to; $m++, $n++)
                    $transformations[sprintf("%04X", $m)] = sprintf("%04X", $_from + $n);
            } elseif (preg_match("#<([0-9a-f]{4})>\s+<([0-9a-f]{4})>\s+\[(.*)\]#ismU", trim($current[$k]), $map)) {
                $from = hexdec($map[1]);
                $to = hexdec($map[2]);
                $parts = preg_split("#\s+#", trim($map[3]));
                
                for ($m = $from, $n = 0; $m <= $to && $n < count($parts); $m++, $n++)
                    $transformations[sprintf("%04X", $m)] = sprintf("%04X", hexdec($parts[$n]));
            }
        }
    }
}

이 함수에서도 처음부터 preg_match_all 함수가 사용됩니다. 정규 표현식은 '#([0-9]+)\s+beginbfchar(.*)endbfchar#ismU' 와 '#([0-9]+)\s+beginbfrange(.*)endbfrange#ismU' 가 사용됩니다.

처음것은 숫자와 관련돼 있고 그 다음에는 뭔지는 모르지만 begin bf char 와 end bf char 가 사용됐습니다.

다른 형식의 파일들은 BT, ET 가 아니라 beginbfchar 와 endbfchar 로 블럭이 나뉘나 봅니다.

이 부분은 우리가 테스트 하는 pdf 파일형식이랑은 상관없는 것 같지만 그래도 시작했으니 마저 보겠습니다.


하여간 이 두 preg_match_all 함수를 사용해서 $chars 와 $ranges 라는 변수를 반들어 냈습니다.


그 다음에는 chars 에 대해서 먼저 for 문을 돌리는데요.

explode() 함수를 사용해서 줄바뀜 '\n'  이 일어나는 단위로 분리를 하고 trim() 으로 좌우의 공백을 없앱니다.


다음 그 분리된 값들로 다시 for 문을 돌리는데요.

preg_match 를 사용해서 다시 나눕니다. 정규표현식인 '#<([0-9a-f]{2,4})>\s+<([0-9a-f]{4,512})>#is' 를 사용해서 나누는데.. 이게 뭔뜻인지 속시원히 알면 좋겠네요.

혹시 이와 관련해서 쉽고 좋은 글 있는 곳아시는 분 계시면 알려 주세요.


하여간 이렇게 해서 만들어진 값을 $map 에 담습니다.

그리고 그 값을 $transformations 배열 변수에 원하는 형식으로 담는군요.


str_pad() 함수 관련한 예제는 아래에 있습니다.


<?php
$input 
"Alien";
echo 
str_pad($input10);                      // produces "Alien     "
echo str_pad($input10"-="STR_PAD_LEFT);  // produces "-=-=-Alien"
echo str_pad($input10"_"STR_PAD_BOTH);   // produces "__Alien___"
echo str_pad($input"___");               // produces "Alien_"
?>


어떤 string 에 공백이나 일정한 문자나 기호를 넣을 수 있는 함수 같네요.

우리가 공부하는 소스에서는 아래와 같이 이용했습니다.

$transformations[str_pad($map[1], 4, "0")] = $map[2];


그럼 $map[1] 다음에 4칸을 0으로 채운 곳에 $map[2] 를 대입한다는 얘기네요.


그 다음은 $ranges 에 대해 for 문을 돌립니다.

이 경우도 줄바꿈을 기준으로 데이터를 분리하고 위 for 문과 비슷하게 돌리는데요.


hexdec() 함수가 보입니다.


<?php
var_dump
(hexdec("See"));
var_dump(hexdec("ee"));
// both print "int(238)"

var_dump(hexdec("that")); // print "int(10)"
var_dump(hexdec("a0")); // print "int(160)"
?>


매뉴얼에는  hexadecimal string 을 decimal number 로 변환해 주는 함수로 나와 있습니다.


이 두번째 for 문은 자세히 안 보겠습니다. 지금 사용하는 pdf 파일을 decode 하는데는 사용되지 않는 부분 같으니까요.


그러면 이제 다 분석 한 거구요.

pdf2text() 함수의 마지막 줄만 남았습니다.


return getTextUsingTransformations($texts, $transformations);


이 return 값이 완전히 사람이 읽을 수 있는 텍스트로 바뀐 값입니다.


지난 글에서 살짝 사람이 읽을 수 있는 데이터로 압축을 해제해서 만든 값을 getTextUsingTransformations() 함수에서 돌려서 완전히 사람이 읽을 수 있는 글자로 만들어 주는 겁니다.


제가 얻고 싶은 대로 데이터를 가공하려면 이 getTextUsingTransformations() 함수 부분을 잘 공부해야 될 것 같습니다.


이 함수는 다음 글에서 자세히 분석해 보겠습니다.




반응형


반응형

지난번 글에 getDecodedStream 까지 분석 했습니다.

그 글 올린지 일주일이 지났네요.
회사에서 다른 일을 하느라 이 부분 분석을 못했는데요.
그 일이 어느정도 마무리 됐으니 다시 마저 분석에 들어가야 겠습니다.


지난번 다루다 끝났던 getDecodedStream 함수를 보면요.$key값이  ASCIIHexDecode, ASCII85Decode 그리고 FlateDecode 냐에 따라서 실행하는 함수가 달랐습니다.

제가 테스트 하는 PDF 문서는 FlateDecode 를 사용하더라구요.


그러면 

            if ($key == "FlateDecode"){
                $_stream = decodeFlate($_stream);
                echo $_stream . "<p>";
                }

가 실행이 될 텐데요.


그 결과값을 미리 브라우저에 찍어 봤습니다.





뭐 이런게 찍히네요.

지금까지 쪼개고 쪼개고 쪼갠 값은 $_stream 의 값을 해당 함수에 넣고 돌려서 얻은 결과가 위 데이터입니다.

사람이 볼 수 있는 데이터가 되려면 아직 멀어보이네요.

그러면 decodeFlate($_stream) 을 살펴 보겠습니다.


function decodeFlate($input) {
    return @gzuncompress($input);
}



뭐 이 함수는 아주 간단합니다. 다른 decodeAsciiHex($_stream) 이나 decodeAscii85($_stream) 은 그래도 뭔가 로직이 있는데 이 함수는 달랑 @gzuncompress() 메소드만 사용했네요.

이 메소드를 알아보겠습니다.

gzuncompress() 메소드의 PHP 매뉴얼을 보면 This function uncompress a compressed string. 라고 돼 있습니다. 그러니까 이 함수는 압축된 string 을 압축해제 하는 함수라고 합니다.

지금까지 작업한 $_stream 을 압축해제하면 저 위에 있는 화면 같은 데이터가 추출 되는군요.


<?php
$compressed   
gzcompress('Compress me'9);
$uncompressed gzuncompress($compressed);
echo 
$uncompressed;
?>


위 코드를 실행 해 보세요. 그러면 Compress me 그대로 출력 될 겁니다. 압축했다가 다시 압축을 풀었으니까요.

그럼 이제 어디로 가야 될까요? 1주일이 지나서 감이 멀어졌는데. 일단 getDecodedStream 함수에서 일을 처리하고 return 하는 부분까지 했으니까 이 함수를 호출했던 pdf2text() 함수로 다시 가야겠죠.


$data = getDecodedStream($stream, $options);


이 부분이었습니다. 저 $data 변수에는 아까 브라우저에 출력했던 그 이상한 데이터가 담겨져 있을 겁니다. $_stream 의 압축을 푼 데이터죠.

그 다음 소스는 아래 내용입니다.


            if (strlen($data)) {
                if (preg_match_all("#BT(.*)ET#ismU", $data, $textContainers)) {
                    $textContainers = @$textContainers[1];
                    getDirtyTexts($texts, $textContainers);

                } else
                    getCharTransformations($transformations, $data);
            }


strlen 함수는 C언어에서도 똑같이 쓰는 것 같은데요. 문자열의 길이를 구하는 함수입니다.

그러니까 strlen($data) 는 $data 의 길이를 구하는거고 0 이면 데이터가 없다는 겂니다. 그러면 PHP는 0 을 false 로 취급하니까 이 if 문을 처리하지 않고 건너 뛸 겁니다.

$data 에 값이 있다면 그것이 길던 짧던 상관 안하고 if 문 안의 코드를 실행합니다.

그 다음은 다시 정규식 표현을 사용하는 preg_match_all 함수가 나오네요.


$data를 BT, ET 등으로 구분해서 $textContainers 에 넣는 것 같습니다.

BT, ET 는 아까 뽑았던 이상한 데이터들에 많이 있습니다. 잘은 모르겠지만 그냥 통박을 굴려보면 Begin Text, End Text 의 준말이 아닐까 생각되네요.

그 다음에 @가 나오는데요. 이건 PHP 에서 에러 관련해서 처리하도록 하는 부호입니다.


<?php
/* Intentional file error */
$my_file = @file ('non_existent_file') or
    die (
"Failed opening file: error was '$php_errormsg'");

// this works for any expression, not just functions:
$value = @$cache[$key];
// will not issue a notice if the index $key doesn't exist.

?>


위에 관련 예제가 있습니다.

그 다음에는 getDirtyTexts($texts, $textContainers); 가 나옵니다.


새로운 함수 호출인데요. 두번째 인자는 방금 뽑아낸 $textContainers 변수를 전달하고 첫번째 인자는 pdf2text 초반에 만들어 뒀던 배열변수네요.

이 변수에 아직 아무 값도 안 담겼던 것 같은데...


그럼 이 호출된 함수를 볼까요?

function getDirtyTexts(&$texts, $textContainers) {
    for ($j = 0; $j < count($textContainers); $j++) {
        if (preg_match_all("#\[(.*)\]\s*TJ#ismU", $textContainers[$j], $parts))
            $texts = array_merge($texts, @$parts[1]);
        elseif(preg_match_all("#Td\s*(\(.*\))\s*Tj#ismU", $textContainers[$j], $parts))
            $texts = array_merge($texts, @$parts[1]);
    }
}


짧은 함수네요.

$textContainers 의 수만큼 for 문을 돌리는데 다시 preg_match_all 를 사용해서 $textContainers 파편들을 '#\[(.*)\]\s*TJ#ismU' 이 정규식 형식에 맞게 구분을 해서 $parts 에 집어넜습니다.


아까 받았던 $texts 에는 이 값들을 계속 차곡차곡 쌓아놓네요.

array_merge() 함수를 사용해서인데요. 이 함수는 한개 이상의 배열을 합하는 함수입니다.

elseif 문 안에도 똑 같은 일을 하죠. 다만 구분하는 정규표현식이 다를 뿐이죠.

즉 이 함수는 BT, ET 별로 쪼갠 값을 다시 위 정규표현식에 맞게 쪼개서 $texts 에 담는 함수입니다.


그 다음은 다시 pdf2text 함수로 돌아가야 되는데요.

BT,ET 로 나누지 않을 경우는 getCharTransformations 함수를 사용하네요.

이 함수부터는 다음 글에서 다룰께요.

마무리 하면서 getDirtyTexts($texts, $textContainers); 바로 밑에 아래 코드를 추가해 봤습니다.


                    for ($k = 0; $k < count($texts); $k++) {
                        echo $texts[$k] . "<br>";
                    }


바로 지금까지 만든 $texts 내용을 찍어 본 건데요.

아래 내용입니다.



이제 조금 사람이 알아볼 수 있는 글들이 나타나기 시작했습니다.


(W)-36(e)7(e)7(k )46(o)7(f) 에는 Week of  가 있죠?

그 중간에 숫자는 뭔지 잘 모르겠지만요. 빈괄호 ()는 space 일까요?


(Oct)-11(o)5(ber )10(1)4( ) 는 October 1 하고 빈괄호가 있네요.

이제 뭔가 보이기 시작합니다.

그럼 다음 시간에는 이 데이터를 마저 decode 해 보겠습니다.





반응형


반응형

주말에 인터넷 서핑을 하다가 지역신문 기사를 하나 번역해 봤어요.


This Colonial Dame is cutting pond ice -- who, where, why?


January 11, 2013 5:06 pm
By Sheila Lennon






이 로드 아일랜드의 여인에 대해 아시는게 있으신가요? 로드 아일랜드 농장에서 지금 무엇을 하고 있는지 아세요? 사진을 클릭해서 크게 보세요 무엇을 하고 있을까요.


편집자 주 : 지난주 출판 한 다음에 Fountain 과 Union Street 코너의 더 오래된 사진을 찾았습니다. 빌딩들도 다르고 James Lavell 이 있던 자리에 Doorley's tap 이 더 먼저 있었습니다. 여기를 보세요.



Updated Sunday:



스커트를 입고 pike pole 을 사용하는 여인은 불굴의 예술가이자 농부인 Julia Lippitt Mauran 입니다. Cranston의 Lippitt Hill Farm 에 있는 그녀의 연못에서 얼음을 떼 내고 있습니다. 저 사진은 1926년 1월 13일에 찍은 것으로 87년 전 사진입니다. 떼어낸 어름은 말이 끄는 썰매에 실려서 얼음집으로 운반됐습니다. 그곳에 얼음을 쌓아놓고 여름에 음식을 시원하게 보관 했었죠.


원래 사진에 있었던 caption 은 아래 내용입니다.


"Julia L. Mauran"이 자신의 연못에 있는 얼음을 떼어 내는 것을 지휘하고 있다. 수작업으로 이루어 졌는데 톱으로 얼음을 잘라 block 으로 만드는 작업이다. 그리고 뱀처럼 긴 통로를 통과하고 나서 썰매에 실린다. 큰 얼음공장에서 현대 기계로 자르고 나르는 방법에 비교하면 조금 원시적인 방법이다. 드러난 물에 얼고 미끄럽게 보이는 걸로 봐서 아주 추운날씨인 것을 보여준다.


(Amish Ice Cutting 사진은 예전의 방법으로 진행되는 얼음 커팅 작업 과정을 보여줍니다.)


반면에 아래 사진은 큰 얼음공장에서 현대식 기계를 사용해서 얼음을 사용하고 싣는 방법을 보여줍니다. 1925년경 Lymansville 로 추정됩니다






Julia Lippitt Mauran은 독특한 면이 있습니다.


그녀의 어머니인 Sarah Williams Lippitt 은 Roger Williams 의 자손입니다. Sarah 의 할아버지인 Brig. Gen. Christopher Lippitt은 1805년에 Cranston 의 Lippitt Hill 에 농장을 지었습니다. 그 땅은 1715년 부터 가문의 땅이었죠. Sarah 도 그곳에서 자랐습니다. (Christopher 는 1809년에 West Warwick에 Lippitt Mill 을 지었습니다.) 1859년에 Warren 의 William Tyler Mauran 과 결혼한 후 Sarah는 Providence 에 정착했습니다. 그녀의 남편은 그곳에서 보석을 가공했죠.


Julia 는 1860년에 태어났습니다. 당시 인구조사 내용을 보면 1900년도까지 그녀는 287 Butler Ave 에 살았습니다. 길의 맨 마지막 집이었는데 어머니 Sarah (1832-1911)와 남자형제인 John (은행 직원) 과 William (보석 가공사) 과 함께 살았습니다. Julia 는 선생님으로 등록이 돼 있습니다. 그녀는 목각사이자 바구니 만드는 사람이었습니다. 조각된 Oak chest 와 panel 전시회를 1893년 World's Columbian Exposition(aka Chicago World's Fair) 에서 열었습니다.








1980년에 찍은 Western Cranston 의 외곽에 있는 Hope Road 의 Lippit Hill Farm 의 farmhouse 사진입니다. 1910년도의 인구조사 내용을 보면 Julia(50)는 Cranston 의 Hope Road 에 있었습니다. 그녀의 직업은 "Farmer - Manager" 로 되어 있구요. Frank Gunzalez 라는 직원과 거주하고 있었습니다. ancestry.com에 있는 그녀의 profile 에 있는 가족들의 얘기로는 그녀는 매주 손으로 직접 버터를 만들었다고 합니다. 거주지를 직접 수선하고 88살에도 우물 지붕의 널을 수선했다고 적혀 있습니다. The big furnace of the 20 room, colonial house burns 5 good logs and she stokes it as well as chopping kindling.


그녀는 로드아일랜드의 charter member of the Colonial Dames 였습니다. The Handicraft Club의 공동설립자였구요. Providence County Garden Club 의 설립자이자 첫번째 회장이었습니다. 1997년 3월 26일 Providence Journal Bulletin story 는 그녀에 대해 좀 더 깊은 내용을 다뤘습니다.





We post a photo from Rhode Island's history every day at facebook/ProvidenceJournal.

Time Lapse Gallery: All the lead photos and links to the stories.




반응형

2012년의 Corona App 스타들

2013. 1. 12. 21:05 | Posted by 솔웅


반응형
Posted on . Written by



지난해에는 코로나로 만든 앱과 게입들이 금맥을 이루는 시기였습니다. 매주 저희 팁들은 그 주에 나온 여러 코로나 앱들 중 기록해 둘만한 앱을 하나씩 선정했었습니다. 6월부터 커뮤니티의 투표를 통해서 App of the Week 중에 그 달의 우승자를 선정하기 시작했죠. 매달 열정인 게임 유저들과 앱 유저들 그리고 코로나 커뮤니티 멤버들이 이 winner 를 선정하기 위해 수백명이 투표에 참여해 주셨습니다.


App of the Week에 선정되려면 어떻게 해야 되는지 궁금하신 분들을 위해서 방법을 알려드리자면요. 아주 간단합니다. 여러분이 개발한 앱을 Corona Showcase에 올려 주세요. 그리고 여러분 앱의 웹사이트나 페이스북 페이지에 “Made with Corona” badge를 달아 주세요. 저희 팀은 좋은 디자인과 사운드 interactivity, 독창성 그리고 유용성등을 바탕으로 신중하게 우승자를 선정합니다. 좀 더 자세한 사항은 App of the Week Guidelines를 보시면 나와 있습니다.



이제 2013년이 시작됐습니다. 이 App of the Week 을 통해서 아주 훌륭한 앱들이 더 소개될 수 있기를 기대합니다.


Inna






반응형

Multi-Element Physics Body 다루기

2013. 1. 11. 05:51 | Posted by 솔웅


반응형
Posted on . Written by



이번주의 튜토리얼은 코로나 물리 엔진에 대한 겁니다. 특히 multi-element physics bodies 부분에 대해서 다룹니다.

첫번째로 multi-element body가 무엇인지 부터 알고 넘어가야 할 것 같습니다. multi-element body 는 두개 이상의 shapes 들이 모여서 구성된 physics body 입니다. raddoll 처럼 weld joint 같은 joint 로 연결된 physical 객체들의 집합으로 하나가 된 physical 객체를 말하는 것이 아닙니다. multi-element body 는 여러 shape 들로 구성되지만 전체가 하나로 통일되고 고정된 객체로 각각의 element들이 move 하거나 flex 하지 않습니다.



Why Multi-Element Bodies?


물리엔진을 많이 사용해 보신 분들은 다 아시는 얘기 일텐데요. 간략하게 더 설명하겠습니다. Box2D 에서는 모든 physical body 들은 반드시 오목한 angle들이 없는 최대 8개 면을 가진 다각형 모양으로 그려집니다.





볼록한 다각형은 괜찮습니다 하지만 볼록한 angle들로만 돼 있고 trace 될 수 없는 body 나 8개면 이하이면서 정확하게 trace 될 수 없는 body 라면 어떨까요? 이 룰을 따르지 않는다면 충돌시 일어나는 reaction 들은 어떻게 나올지 모르게 됩니다.


해결책은 multi-element body 입니다. 여러개의 볼록한 모양이 하나의 body 로 되서 trace 되는 모양이 기본적인 모양입니다. 여러분들은 간단한 shape들을 사용해서 multi-element bodies 들을 만들어야 합니다. 최소한의 shape 들을 사용해서요. 이 작업이 좀 어려울 것 같으면 3rd party tool인 PhysicsEditor가 이 작업을 쉽게 할 수 있도록 도와 드립니다.


Part I – Per-Element Collision Control


이미 이전에 multi-element bodies 작업을 해 보신적이 있으시면요, 아마 이 기능이 아주 유용하다는 것을 아실겁니다. 하지만 이 기능에도 약간의 제한사항들이 있는데요. 이 기능의 capabilities를 먼저 살펴보죠.


  1. 각 element 들은 유니크한 collision filters들을 가질 수 있습니다. 이 기능을 multi-element body의 일 부분이 다른 객체와 충돌하거나 할 때 어떤 reaction이 일어나도록 할 수 있습니다. 다른 부분은 충돌해도 아무 일이 일어나지 않구요. 
  2. 각각의 element들은 센서로 세팅할 수도 있습니다. 다른 객체들이 그 부분을 지나갈 때 어떤 일이 일어나도록 할 수 있죠. 충돌효과를 줄 수도 있지만 그냥 지나가도록 할 수도 있고 이 때 어떤 일이 일어나도록 할 수 있습니다.
  3. 충돌시 각 element는 사전에 physics addBody() function에서 지정된 고유의 숫자를 return 할 수 있습니다. 예를 들어 첫번째 element 는 1 두번째는 2 등으로 지정할 수 있죠.


이러한 unique capability들도 있지만 아래와 같은 제한사항들도 있습니다.


  1. 일단 collision가 filter element나 body에 정의되면 Runtime 동안은 바뀔수가 없습니다.
  2. 만약 element 가 센서로 정의돼었다면 Runtime 동안 각 element 별로 바꿀 수 없습니다. 다만 전체 body 가 sensor 나 non-sensor 로 바뀔 수 있을 뿐입니다.


Overcoming These Limitations


근데 너무 실망하지는 마세요. 오늘 이 튜토리얼에서 그 두가지 제한사항들을 어떻게 극복할지도 보여드릴테니까요. 우리는 아 작업을 physics contact를 사용해서 구현할 겁니다. 최근 있었던 튜토리얼에서 이 기능을 소개해 드린적이 있는데요. 여기로 가시면 그 글을 보실 수 있습니다.


physics contact를 사용해서 사전에 결정할 수 있습니다. pre-collision listener 를 사용해서 실제 충돌이 일어날 때 어떤 일이 일어날지를 결정할 수 있단 겁니다. 여러분 앱의 로직ㅇㅔ 근거해서 전체 collision을 void  하도록 할 수 있거든요. 이 튜토리얼에서는 이 기능을 multi-element bodies 에서 활용해 보겠습니다.



아래 “space nebula,” 를 사용할 건데요. 소설을 써 보면 star-fighter 는 우선 먼저 저 모서리에 있는 shield 를 부숴야 합니다. 그 다음에 가운에에 있는 대마왕을 없애야 해요. 이 시나리오를 구혀하기 위해서는 전통적인 방법으로는 아래와 같은 제한사항들이 있습니다. 



  1. 만약에 전통적인 방법인 joint를 사용해서 구현한다면 한쪽 모서리가 파괴 됐을 때 나머지 부분들의 joint 연결이 불안정해 지게 되는 문제점이 있습니다.
  2. object.isSensor를 사용하면 all or nothing 이 됩니다. 그러니까 파괴된 한 부분만 센서로 바꿀 수 없습니다.


이런 제약사항때문에 전통적인 joint 방법이 아닌 physics contact를 사용할 겁니다. per-element collision detection을 사용해서 이 destructible shield 이슈를 해결해 보겠습니다.




Assembling the Nebula


Let’s examine how to create a multi-element body in Corona. We’ll create a 9-element body to trace the nebula. In Corona, we simply do this:

일단 코로나에서 어떻게 multi-element body를 생성하는지 공부해 봅시다. 우리는 nebula 를 trace 하기 위해 9개의 element body 를 만들겁니다. 코로나에서는 이를 위해 아래와 같이 하면 됩니다.


  • nebula image 를 스크린에 display 시킨다.
  • nebula에 대한 shape 을 정의한다. top 을 시작점으로 해서 필요한 작업을 한다. outlying pod에 대해서는 8각형을 사용해야 합니다. 왜냐하면 multi-element body 에서는 radial shape 을 offset 할 수 없거든요. 정확하게 원이 아니니까요. 팔각형으로 충돌에 대한 자연스러운 반응을 이끌어 내기에도 문제가 없을 겁니다.
  • physical body 를 add 하고 element들의 순서에 따라 API 에 각 shape 들을 pass 합니다. 순서가 중요합니다. collision detection 에서 숫자를 return 할 거거든요.


local nebula = display.newImage( "nebula.png" )
nebula.x, nebula.y = display.contentWidth/2, display.contentHeight/2
local podT = {1,-89, 14,-83, 20,-70, 14,-57, 1,-51, -12,-57, -18,-70, -12,-83}
local beamTR = {19,-63, 63,-19, 59,-14, 14,-59}
local podR = {69,-20, 82,-14, 88,-1, 82,12, 69,18, 56,12, 50,-1, 56,-14}
local beamBR = {19,61, 14,56, 58,13, 62,17}
local podB = {1,49, 14,55, 20,68, 14,81, 1,87, -12,81, -18,68, -12,55}
local beamBL = {-18,63, -64,17, -59,13, -14,58}
local podL = {-70,-20, -57,-14, -51,-1, -57,12, -70,18, -83,12, -89,-1, -83,-14}
local beamTL = {-18,-65, -14,-61, -59,-15, -64,-20}

physics
.addBody( nebula, "dynamic",
{shape=podT},
{shape=beamTR},
{shape=podR},
{shape=beamBR},
{shape=podB},
{shape=beamBL},
{shape=podL},
{shape=beamTL},
{radius=24} --radial body used for the nucleus
)
local shieldStates = { true, true, true, true, true, true, true, true }


이제 맨 마지막에 이 8개의 shield 객체들을 관리하기 위해 간단한 shieldStates table을 셋업하셔야 합니다. 이 테이블은 나중에 어떤 특정 element 를 on/off 시킬때 사용할 겁니다. 그리고 shield element 가 inact인지 destroyed 돼 있는지를 track 할 때도 사용할 거구요. 이를 위해 8개의 boolean 값을 가진 간단한 non-indexed table 을 사용합니다.



The Basic Pre-Collision Listener



그 다음으로는 기본적인 pre-collision listener를 정의할 겁니다. 이전 튜토리얼에서 설명했듯이 물리적인 접촉을 utilize 하기 위해 pre-collision listener를 사용해야 합니다. 왜냐하면 코로나에게 충돌이 일어나기 직전에 그 충돌상황을 관리하도록 해야 하기 때문이죠.


local function nebulaCollide( self,event )
  print( event.selfElement )
end
nebula.preCollision = nebulaCollide ; nebula:addEventListener( "preCollision", nebula )


이 함수는 이주 기본적인 구조로 돼 있습니다. 이제 nebula 와 어떤 것이라도 충돌하게 되면 해당 element가 정의 돼 있는 순서에 따라 event.selfElement로 그 element의 corresponding integer가 return 될 겁니다. 위에 있는 pod를 첫번째 element로 정의했죠. 그러니까 그 element는 1을 return 할 겁니다. 오른쪽 beam 은 2를 반환하고 오른쪽 pod 는 3을 반환하고 뭐 이런식으로 진행될 겁니다.


Enhancing the Pre-Collision Listener


이제 nebula 의 어떤 element 가 충돌했는지 알 수 있게 됐습니다. 이제 shieldStates table 을 이용해서 충돌을 처리할 지 말아야 할지를 처리할 수 있도록 만들어야 합니다. 만약 그 shield element 가 게임을 하다가 destroyed 됐다면 코로나에게 해당 충돌은 void 처리하라고 지시할 수 있습니다. (event.contact)


local function nebulaCollide( self,event )

--query the position (and state) from "shieldStates" table
local isElementIntact = shieldStates[event.selfElement]

if ( isElementIntact == false ) then
event.contact.isEnabled = false --use physics contact to void collision!
end
end
 

shieldStates table를 관리하는 것은 아주 간단합니다. shield pod 을 destroy 처리하려면 간단하게 아래 처럼 하면 됩니다.



shieldStates[5] = false


Building on this concept, you can now manage your nebula shields and enact other creative methods, including:

이제 여러분은 nebula shield를 관리할 수 있게 됐습니다. 그리고 아래 내용들을 포함해서 여러 효과들을 구현하실 수 있습니다.

  1. shield pod 이 destroy 됐다면 옆에 있는 beam 도 destroy 시킴.
  2. 특정 시간이 지나면 shield pod 을 rebuild 하고 옆의 beam 들도 다시 rebuild 함
  3. shieldStates table setup 을 확장해서 각 pod 의 에너지 양을 관리함.


per-element detection 과 함께 physics contact를 사용하면 전통적인 방법이 갖는 한계를 해결할 수 있다는 걸 알게 되셨을 겁니다.




Part II – Multi-Element Bodies and Sensors



이제 multi-element bodies와 관련되서 많이들 잘 못 이해하고 계시는 sensor 부분에 대해 얘기해 보겠습니다.


Corona 의 physics 에 대해서 경험이 있으신 분들은 어떤 모양이나 타입(dynamic, kinematic, or static)에서든 physical body 가 sensor 로서 동작할 수 있다는 것을 알 수 있을 겁니다. sensor 는 부딪혀서 튀긴다던가 하는 physical sense 는 없습니다.


multi-element bodies와 관련해서 많이 오해하기 쉬운게 뭐냐하면 모든 element 들은 seosor로서 colision event를 return 합니다. body 가 전체 통으로 돼 있는 객체라면 그 전체가 충돌을 체크하는 포인트가 되죠. 여기서 헛갈릴 수 있는 것이 sensor 에서는 충돌시 여러개의 began phase event를 받을 수 있게 된다는 거죠. 혹은 센서 지역 밖에 있는데도 작은 부분이 걸쳐 있어서  ended phase를 받을 수도 있습니다.


사실 센서는 원래 그러기 위해서 만들어 졌습니다. 예를 들어서 트랙을 달리는 경주용 자동차의 앞 바퀴에만 센서를 달 필요가 있는 경우가 있을 겁니다. 조종석은 트랙이 있더라고 앞바퀴만 통과하면 어떤 결과를 일으켜야 되는 경우가 있죠. 또 다른 경우는 multi-element body 전체가 센서 지역의 안에 혹은 밖에 있는 것을 체크해야 될 때도 있습니다. “jumping fish” 를 보죠. 물고기는 완전히 물 안에 존재해야만 하죠? 이것도 센서로 정의할 수 있을 까요?



Counting the Collisions






이 jumping fish 시나리오는 fish의 body 에 있는 각각의 element들에 대해 어떤 충돌이 일어나는지를 counting 함으로서 해결할 수 있습니다. 이를 위해 값들이 들어갈 테이블을 만들고 그 이름을 elementStates라고 할 겁니다. began phase 에서 해당 count를 1씩 증가시키고 ended phase 에서는 1씩 감소 시킬 겁니다. 각 element들은 센서와 함께 collision event 를 return 할 겁니다. 그래서 만약 한 element에 4개의 센서들이 overlap 된다면 count는 4가 되겠죠. 만약 그 센서들 중 3개가 밖으로 나간다면 count 는 1로 줄겠죠. element count 가 0이 될 때는 전체가 센서 밖에 나가 있다는 것을 말합니다. 그러면 물고기가 완전히 물밖으로 나간 상태고 물고기가 점프했다고 볼 수 있겠죠.



또한 우리는 elementsIn를 만들어서 물고기의 전체 element들 중에 얼마나 많이  센서 범위 안이나 밖에 있는지를 count 할 겁니다. 이 값은 절대 5를 넘지 않을 겁니다. 왜냐하면 물고기는 5개의 element들로 구성될 거기 때문이죠.

마지막으로 inWater라는 boolean flag 를 정의할 겁니다. 이로 인해서 완전히 물 속에 있는지 물 밖에 있는지를 알 수 있게 되죠. 코딩을 편하게 하기 위해서 이 세개의 item들은 fish 의 properties 로 정의할 겁니다.

아래에 기본적인 예제 코드가 있습니다.



local
fish = display.newImage( "jumpfish.png" )

fish.x, fish.y = display.contentWidth/2, display.contentHeight/2-200
local tail = {-117,12, -123,-46, -68,-13}
local bodyBack = {-89,-26, -61,-39, -20,-46, 20,-49, 42,27, -12,28, -66,16, -94,0}
local bodyFront = {20,-49, 71,-43, 107,-32, 121,-20, 126,-10, 108,5, 78,19, 43,27}
local finBack = {-39,23, -11,29, -10,41, -32,50}
local finFront = {-9,51, -11,28, 41,27, 15,42}

physics.addBody( fish, "dynamic",
{shape=tail},
{shape=bodyBack},
{shape=bodyFront},
{shape=finBack},
{shape=finFront}
)
fish.elementStates = { 0,0,0,0,0 } --table of per-element collision counts
fish.elementsIn = 0
fish.inWater = false



보시다시피 물고기의 element들은 Part 1에서 다뤘던 nebula 와 비슷하게 정의했죠? 거기에다가 추가적으로 table elementStates와 properties elementsIn and inWater 를 생성했습니다.


Managing the Count


이 물고기는 standard collision listener 가 필요합니다. pre-collision listener 가 아니라요. 이번에는 physics contact 기능을 사용하지 않을 겁니다.


local function fishCollide( self,event )

if ( event.phase == "began" ) then
if ( self.elementStates[event.selfElement] == 0 ) then
self.elementsIn = self.elementsIn+1
end
self.elementStates[event.selfElement] = self.elementStates[event.selfElement]+1
elseif ( event.phase == "ended" ) then
self.elementStates[event.selfElement] = self.elementStates[event.selfElement]-1
if ( self.elementStates[event.selfElement] == 0 ) then
self.elementsIn = self.elementsIn-1
end
end
if ( self.elementsIn == 0 and self.inWater == true ) then
self.inWater = false
print("FISH ENTIRELY OUT OF THE WATER!")
elseif ( self.elementsIn == 5 and self.inWater == false ) then
self.inWater = true
print("FISH ENTIRELY IN THE WATER!")
end
end
fish.collision = fishCollide ; fish:addEventListener( "collision", fish )

이제 단계별로 볼까요

began phase:

  1.  첫번째로 총돌한 element의 count가 0인지 체크합니다. 만약 0이면 이 element는 처음으로 센서지역으로 들어가는 element라는 것을 알수 있죠. 그리고 이 물고기의 전체 elementsIn count를 1 증가시킵니다.
  2. 그 다음으로는 특정 element의 count 를 1 증가 시킵니다.


ended phase:

  1. 충돌한 element의 count 에서 1을 뺍니다.
  2. 그 다음에 element의 count가 0인지 체크를 하죠. 만약 0이면 이 물고기는 전체 센서지역에서 완전히 밖에 있는 상태라는 것을 알 수 있습니다. 그리고 물고기의 total elementsIn count 에서 1을 줄입니다.


The conditional check:

  1. 물고기의 total elementsIn count 가 0인지 여부를 체크합니다. 이전에 물속에 있었는지 여부를 체크합니다. 두 조건 모두 pass 하면 물고기는 물밖에 완전히 나온 상태라는 것을 알 수 있죠. 그러면 inWater flag를 false로 세팅합니다.
  2. elseif 조건문은 물고기의 elementsIn count가 5인지를 체크합니다. 이전에 물에 완전히 들어가 있지 않은지 체크하죠. 두가지 조건이 pass 라면 이 물고기는 이 물고기는 완전히 물 속에 있다는 것을 알 수 있죠. 그러면 inWater flag를 true로 세팅합니다.


물고기가 완전히 물 속에 있는지 아니면 완전히 물 밖에 있는지에 대해 이렇게 sensor collision을 가지고 handle 할 수 있습니다 이 방법은 센서들을 overlapping 하고 neighboring 과도 같이 사용할 수 있습니다. 예를 들어 core body 로 물 센서 지역을 만들었고 그 위에 파도가 있다면 이 코드는 물고기의 모든 elements들을 이 센서들에 모두 적용 시킬 수도 있습니다.


In Summary


여기까지가 오늘의 튜토리얼입니다. 배우신대로 mutli-element physics bodies 기능으로 joint-assembled bodies 가 할 수 없는 효과를 줄 수 있습니다. 하지만 사용하시려면 몇개의 장애물은 넘어야 겠죠. 이 튜토리얼이 여러분이 앱을 만드시면서 닥치는 그런 장애를 극복하는데 도움이 되기를 바랍니다.


반응형

Corona tip: Shuffle it!

2013. 1. 10. 19:26 | Posted by 솔웅


반응형

Corona tip: Shuffle it!


랜덤하게 어떤 것을 뽑아내고 싶으세요? 그러면 아래처럼 Shuffle 함수를 만들어서 사용하세요.


 

local function shuffle(t)    

local rand = math.random    

assert(t, "shuffle() expected a table, got nil")    

local iterations = #t     local j        

 

for i = iterations, 2, -1 do        

j = rand(i)    

  t[i], t[j] = t[j], t[i]    

end

end

 

numbers = {1,2,3,4,5,6,7,8,9,10,11,12} -- say up to 52 if your're doing a card game   

 

shuffle(numbers)

 


간단하게 1에서 #numbers 까지의 테이블을 루프 돌리고 그 entry들의 배열을 랜덤하게 정렬하세요. 그 값들이 숫자가 아니라 문자라도 상관없습니다. 그리고 이미지라도 상관 없구요. 노래가 담긴 오디오 파일들일 수도 있겠죠. 그러면 많은 노래들을 shuffle 해서 play 시킬 수 있겠죠.






반응형


반응형

Corona tip: Is display.remove() better than object:removeSelf()?


display.remove() 가 object:removeSelf()보다 더 낫나요? 

 

display.remove는 아래와 똑 같습니다.

 

if object and object.removeSelf then   

   object:removeSelf()

end

 

그러니까 display.remove를 사용하면 두 줄을 줄일 수 있는 거죠.

 

display.remove(object)

 

이렇게 하면 코딩을 많이 줄일 수 있겠죠. 프로젝트 전체로 치면 아주 많이 줄일 수 있을 겁니다. 그 객체를 remove 하기 전에 우선 그 객체의 존재 여불를 체크해야 하는데 display.remove 는 이 작업을 자동으로 해 줍니다. 하지만 여러분이 스스로 객체를 체크하고 싶으시면  object:removeSelf()를 사용해도 상관은 없습니다.






반응형